2021-11-01 15:48:08 +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';
|
|
|
|
import {SearchFunc, TableData} from 'app/client/models/TableData';
|
|
|
|
import {getReferencedTableId, isRefListType} from 'app/common/gristTypes';
|
|
|
|
import {BaseFormatter} from 'app/common/ValueFormatter';
|
2021-11-01 15:48:08 +00:00
|
|
|
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;
|
2021-11-09 12:11:37 +00:00
|
|
|
public readonly isRefList: boolean;
|
2021-11-01 15:48:08 +00:00
|
|
|
|
|
|
|
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';
|
2021-11-09 12:11:37 +00:00
|
|
|
this.isRefList = isRefListType(colType);
|
2021-11-01 15:48:08 +00:00
|
|
|
}
|
|
|
|
|
2021-11-09 12:11:37 +00:00
|
|
|
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
|
2021-11-01 15:48:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (this.visibleColId === 'id') {
|
|
|
|
const n = Number(value);
|
2021-11-09 12:11:37 +00:00
|
|
|
if (Number.isInteger(n)) {
|
|
|
|
value = n;
|
|
|
|
} else {
|
|
|
|
return raw;
|
2021-11-01 15:48:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-09 12:11:37 +00:00
|
|
|
if (!this.tableData.isLoaded) {
|
|
|
|
const options: {column: string, raw?: string} = {column: this.visibleColId};
|
|
|
|
if (value !== raw) {
|
|
|
|
options.raw = raw;
|
|
|
|
}
|
|
|
|
return ['l', value, options];
|
2021-11-01 15:48:08 +00:00
|
|
|
}
|
2021-11-09 12:11:37 +00:00
|
|
|
|
|
|
|
const searchFunc: SearchFunc = (v: any) => isEqual(v, value);
|
2021-11-01 15:48:08 +00:00
|
|
|
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.
|
2021-11-09 12:11:37 +00:00
|
|
|
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;
|
|
|
|
}
|
2021-11-01 15:48:08 +00:00
|
|
|
}
|
2021-11-09 12:11:37 +00:00
|
|
|
return ['L', ...rowIds];
|
2021-11-01 15:48:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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();
|
|
|
|
}
|