2024-04-26 20:34:16 +00:00
|
|
|
import {ACIndex, ACResults} from 'app/client/lib/ACIndex';
|
|
|
|
import {makeT} from 'app/client/lib/localization';
|
|
|
|
import {ICellItem} from 'app/client/models/ColumnACIndexes';
|
|
|
|
import {ColumnCache} from 'app/client/models/ColumnCache';
|
2021-12-06 12:07:52 +00:00
|
|
|
import {DocData} from 'app/client/models/DocData';
|
2021-11-09 12:11:37 +00:00
|
|
|
import {ColumnRec} from 'app/client/models/entities/ColumnRec';
|
|
|
|
import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec';
|
2021-12-06 12:07:52 +00:00
|
|
|
import {TableData} from 'app/client/models/TableData';
|
2021-11-09 12:11:37 +00:00
|
|
|
import {getReferencedTableId, isRefListType} from 'app/common/gristTypes';
|
2024-04-26 20:34:16 +00:00
|
|
|
import {EmptyRecordView} from 'app/common/PredicateFormula';
|
2021-11-09 12:11:37 +00:00
|
|
|
import {BaseFormatter} from 'app/common/ValueFormatter';
|
2024-04-26 20:34:16 +00:00
|
|
|
import {Disposable, dom, Observable} from 'grainjs';
|
|
|
|
|
|
|
|
const t = makeT('ReferenceUtils');
|
2021-11-01 15:48:08 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Utilities for common operations involving Ref[List] fields.
|
|
|
|
*/
|
2024-04-26 20:34:16 +00:00
|
|
|
export class ReferenceUtils extends Disposable {
|
2021-11-01 15:48:08 +00:00
|
|
|
public readonly refTableId: string;
|
|
|
|
public readonly tableData: TableData;
|
2022-01-13 10:04:56 +00:00
|
|
|
public readonly visibleColFormatter: BaseFormatter;
|
2021-11-01 15:48:08 +00:00
|
|
|
public readonly visibleColModel: ColumnRec;
|
|
|
|
public readonly visibleColId: string;
|
2021-11-09 12:11:37 +00:00
|
|
|
public readonly isRefList: boolean;
|
2024-04-26 20:34:16 +00:00
|
|
|
public readonly hasDropdownCondition = Boolean(this.field.dropdownCondition.peek()?.text);
|
|
|
|
|
|
|
|
private readonly _columnCache: ColumnCache<ACIndex<ICellItem>>;
|
|
|
|
private _dropdownConditionError = Observable.create<string | null>(this, null);
|
|
|
|
|
|
|
|
constructor(public readonly field: ViewFieldRec, private readonly _docData: DocData) {
|
|
|
|
super();
|
2021-11-01 15:48:08 +00:00
|
|
|
|
|
|
|
const colType = field.column().type();
|
|
|
|
const refTableId = getReferencedTableId(colType);
|
|
|
|
if (!refTableId) {
|
|
|
|
throw new Error("Non-Reference column of type " + colType);
|
|
|
|
}
|
|
|
|
this.refTableId = refTableId;
|
|
|
|
|
2024-04-26 20:34:16 +00:00
|
|
|
const tableData = _docData.getTable(refTableId);
|
2021-11-01 15:48:08 +00:00
|
|
|
if (!tableData) {
|
|
|
|
throw new Error("Invalid referenced table " + refTableId);
|
|
|
|
}
|
|
|
|
this.tableData = tableData;
|
|
|
|
|
2022-01-13 10:04:56 +00:00
|
|
|
this.visibleColFormatter = field.visibleColFormatter();
|
2021-11-01 15:48:08 +00:00
|
|
|
this.visibleColModel = field.visibleColModel();
|
|
|
|
this.visibleColId = this.visibleColModel.colId() || 'id';
|
2021-11-09 12:11:37 +00:00
|
|
|
this.isRefList = isRefListType(colType);
|
2024-04-26 20:34:16 +00:00
|
|
|
|
|
|
|
this._columnCache = new ColumnCache<ACIndex<ICellItem>>(this.tableData);
|
2021-11-01 15:48:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public idToText(value: unknown) {
|
|
|
|
if (typeof value === 'number') {
|
2022-01-13 10:04:56 +00:00
|
|
|
return this.visibleColFormatter.formatAny(this.tableData.getValue(value, this.visibleColId));
|
2021-11-01 15:48:08 +00:00
|
|
|
}
|
|
|
|
return String(value || '');
|
|
|
|
}
|
|
|
|
|
2024-04-26 20:34:16 +00:00
|
|
|
/**
|
|
|
|
* Searches the autocomplete index for the given `text`, returning
|
|
|
|
* all matching results and related metadata.
|
|
|
|
*
|
|
|
|
* If a dropdown condition is set, results are dependent on the `rowId`
|
|
|
|
* that the autocomplete dropdown is open in. Otherwise, `rowId` has no
|
|
|
|
* effect.
|
|
|
|
*/
|
|
|
|
public autocompleteSearch(text: string, rowId: number): ACResults<ICellItem> {
|
|
|
|
let acIndex: ACIndex<ICellItem>;
|
|
|
|
if (this.hasDropdownCondition) {
|
|
|
|
try {
|
|
|
|
acIndex = this._getDropdownConditionACIndex(rowId);
|
|
|
|
} catch (e) {
|
|
|
|
this._dropdownConditionError?.set(e);
|
|
|
|
return {items: [], extraItems: [], highlightFunc: () => [], selectIndex: -1};
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
acIndex = this.tableData.columnACIndexes.getColACIndex(
|
|
|
|
this.visibleColId,
|
|
|
|
this.visibleColFormatter,
|
|
|
|
);
|
|
|
|
}
|
2021-11-01 15:48:08 +00:00
|
|
|
return acIndex.search(text);
|
|
|
|
}
|
2024-04-26 20:34:16 +00:00
|
|
|
|
|
|
|
public buildNoItemsMessage() {
|
|
|
|
return dom.domComputed(use => {
|
|
|
|
const error = use(this._dropdownConditionError);
|
|
|
|
if (error) { return t('Error in dropdown condition'); }
|
|
|
|
|
|
|
|
return this.hasDropdownCondition
|
|
|
|
? t('No choices matching condition')
|
|
|
|
: t('No choices to select');
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns a column index for the visible column, filtering the items in the
|
|
|
|
* index according to the set dropdown condition.
|
|
|
|
*
|
|
|
|
* This method is similar to `this.tableData.columnACIndexes.getColACIndex`,
|
|
|
|
* but whereas that method caches indexes globally, this method does so
|
|
|
|
* locally (as a new instances of this class is created each time a Reference
|
|
|
|
* or Reference List editor is created).
|
|
|
|
*
|
|
|
|
* It's important that this method be used when a dropdown condition is set,
|
|
|
|
* as items in indexes that don't satisfy the dropdown condition need to be
|
|
|
|
* filtered.
|
|
|
|
*/
|
|
|
|
private _getDropdownConditionACIndex(rowId: number) {
|
|
|
|
return this._columnCache.getValue(
|
|
|
|
this.visibleColId,
|
|
|
|
() => this.tableData.columnACIndexes.buildColACIndex(
|
|
|
|
this.visibleColId,
|
|
|
|
this.visibleColFormatter,
|
|
|
|
this._buildDropdownConditionACFilter(rowId)
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
private _buildDropdownConditionACFilter(rowId: number) {
|
|
|
|
const dropdownConditionCompiled = this.field.dropdownConditionCompiled.get();
|
|
|
|
if (dropdownConditionCompiled?.kind !== 'success') {
|
|
|
|
throw new Error('Dropdown condition is not compiled');
|
|
|
|
}
|
|
|
|
|
|
|
|
const tableId = this.field.tableId.peek();
|
|
|
|
const table = this._docData.getTable(tableId);
|
|
|
|
if (!table) { throw new Error(`Table ${tableId} not found`); }
|
|
|
|
|
|
|
|
const {result: predicate} = dropdownConditionCompiled;
|
|
|
|
const rec = table.getRecord(rowId) || new EmptyRecordView();
|
|
|
|
return (item: ICellItem) => {
|
|
|
|
const choice = item.rowId === 'new' ? new EmptyRecordView() : this.tableData.getRecord(item.rowId);
|
|
|
|
if (!choice) { throw new Error(`Reference ${item.rowId} not found`); }
|
|
|
|
|
|
|
|
return predicate({rec, choice});
|
|
|
|
};
|
|
|
|
}
|
2021-11-01 15:48:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export function nocaseEqual(a: string, b: string) {
|
|
|
|
return a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
|
|
}
|