gristlabs_grist-core/app/client/lib/ReferenceUtils.ts

133 lines
4.3 KiB
TypeScript
Raw Normal View History

import { DocData } from 'app/client/models/DocData';
import {ColumnRec} from 'app/client/models/entities/ColumnRec';
import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec';
import {SearchFunc, TableData} from 'app/client/models/TableData';
import {getReferencedTableId, isRefListType} from 'app/common/gristTypes';
import {BaseFormatter} from 'app/common/ValueFormatter';
import isEqual = require('lodash/isEqual');
/**
* Utilities for common operations involving Ref[List] fields.
*/
export class ReferenceUtils {
public readonly refTableId: string;
public readonly tableData: TableData;
public readonly formatter: BaseFormatter;
public readonly visibleColModel: ColumnRec;
public readonly visibleColId: string;
public readonly isRefList: boolean;
constructor(public readonly field: ViewFieldRec, docData: DocData) {
// Note that this constructor is called inside ViewFieldRec.valueParser, a ko.pureComputed,
// and there are several observables here which get used and become dependencies.
const colType = field.column().type();
const refTableId = getReferencedTableId(colType);
if (!refTableId) {
throw new Error("Non-Reference column of type " + colType);
}
this.refTableId = refTableId;
const tableData = docData.getTable(refTableId);
if (!tableData) {
throw new Error("Invalid referenced table " + refTableId);
}
this.tableData = tableData;
this.formatter = field.createVisibleColFormatter();
this.visibleColModel = field.visibleColModel();
this.visibleColId = this.visibleColModel.colId() || 'id';
this.isRefList = isRefListType(colType);
}
public parseReference(
raw: string, value: unknown
): number | string | ['l', unknown, {raw?: string, column: string}] {
if (!value || !raw) {
return 0; // default value for a reference column
}
if (this.visibleColId === 'id') {
const n = Number(value);
if (Number.isInteger(n)) {
value = n;
} else {
return raw;
}
}
if (!this.tableData.isLoaded) {
const options: {column: string, raw?: string} = {column: this.visibleColId};
if (value !== raw) {
options.raw = raw;
}
return ['l', value, options];
}
const searchFunc: SearchFunc = (v: any) => isEqual(v, value);
const matches = this.tableData.columnSearch(this.visibleColId, searchFunc, 1);
if (matches.length > 0) {
return matches[0];
} else {
// There's no matching value in the visible column, i.e. this is not a valid reference.
// We need to return a string which will become AltText.
return raw;
}
}
public parseReferenceList(
raw: string, values: unknown[]
): ['L', ...number[]] | null | string | ['l', unknown[], {raw?: string, column: string}] {
if (!values.length || !raw) {
return null; // default value for a reference list column
}
if (this.visibleColId === 'id') {
const numbers = values.map(Number);
if (numbers.every(Number.isInteger)) {
values = numbers;
} else {
return raw;
}
}
if (!this.tableData.isLoaded) {
const options: {column: string, raw?: string} = {column: this.visibleColId};
if (!(values.length === 1 && values[0] === raw)) {
options.raw = raw;
}
return ['l', values, options];
}
const rowIds: number[] = [];
for (const value of values) {
const searchFunc: SearchFunc = (v: any) => isEqual(v, value);
const matches = this.tableData.columnSearch(this.visibleColId, searchFunc, 1);
if (matches.length > 0) {
rowIds.push(matches[0]);
} else {
// There's no matching value in the visible column, i.e. this is not a valid reference.
// We need to return a string which will become AltText.
return raw;
}
}
return ['L', ...rowIds];
}
public idToText(value: unknown) {
if (typeof value === 'number') {
return this.formatter.formatAny(this.tableData.getValue(value, this.visibleColId));
}
return String(value || '');
}
public autocompleteSearch(text: string) {
const acIndex = this.tableData.columnACIndexes.getColACIndex(this.visibleColId, this.formatter);
return acIndex.search(text);
}
}
export function nocaseEqual(a: string, b: string) {
return a.trim().toLowerCase() === b.trim().toLowerCase();
}