(core) Value parsing for refs, parsing data entry for numbers

Summary:
Handle reference columns in ViewFieldRec.valueParser.

Extracted code for reuse from ReferenceEditor to look up values in the visible column. While I was at it, also extracted a bit of common code from ReferenceEditor and ReferenceListEditor into a new class ReferenceUtils. More refactoring could be done in this area but it's out of scope.

Changed NTextEditor to use field.valueParser, which affects numeric and reference fields. In particular this means numbers are parsed on data entry, it doesn't change anything for references.

Test Plan:
Added more CopyPaste testing to test references.

Tested entering slightly formatted numbers in NumberFormatting.

Reviewers: dsagal

Reviewed By: dsagal

Differential Revision: https://phab.getgrist.com/D3094
This commit is contained in:
Alex Hall
2021-11-01 17:48:08 +02:00
parent f0da3eb3b2
commit d63da496a8
9 changed files with 222 additions and 167 deletions

View File

@@ -1,17 +1,16 @@
/**
* TableData maintains a single table's data.
*/
import {ColumnACIndexes} from 'app/client/models/ColumnACIndexes';
import {ColumnCache} from 'app/client/models/ColumnCache';
import {DocData} from 'app/client/models/DocData';
import {DocAction, ReplaceTableData, TableDataAction, UserAction} from 'app/common/DocActions';
import {isRaisedException} from 'app/common/gristTypes';
import {countIf} from 'app/common/gutil';
import {TableData as BaseTableData, ColTypeMap} from 'app/common/TableData';
import {BaseFormatter} from 'app/common/ValueFormatter';
import {Emitter} from 'grainjs';
import { ColumnACIndexes } from 'app/client/models/ColumnACIndexes';
import { ColumnCache } from 'app/client/models/ColumnCache';
import { DocData } from 'app/client/models/DocData';
import { DocAction, ReplaceTableData, TableDataAction, UserAction } from 'app/common/DocActions';
import { isRaisedException } from 'app/common/gristTypes';
import { countIf } from 'app/common/gutil';
import { TableData as BaseTableData, ColTypeMap } from 'app/common/TableData';
import { Emitter } from 'grainjs';
export type SearchFunc = (value: string) => boolean;
export type SearchFunc = (value: any) => boolean;
/**
* TableData class to maintain a single table's data.
@@ -62,21 +61,13 @@ export class TableData extends BaseTableData {
}
/**
* Given a colId and a search string, returns a list of matches, optionally limiting their number.
* The matches are returned as { label, value } pairs, for use with auto-complete. In these, value
* is the rowId, and label is the actual value matching the query.
* Given a colId and a search function, returns a list of matching row IDs, optionally limiting their number.
* @param {String} colId: identifies the column to search.
* @param {String|Function} searchTextOrFunc: If a string, then the text to search. It splits the
* text into words, and returns values which contain each of the words. May be a function
* which, given a formatted column value, returns whether to include it.
* @param {Function} searchFunc: A function which, given a column value, returns whether to include it.
* @param [Number] optMaxResults: if given, limit the number of returned results to this.
* @returns Array[{label, value}] array of objects, suitable for use with JQueryUI's autocomplete.
* @returns Array[Number] array of row IDs.
*/
public columnSearch(colId: string, formatter: BaseFormatter,
searchTextOrFunc: string|SearchFunc, optMaxResults?: number) {
// Search for each of the words in query, case-insensitively.
const searchFunc = (typeof searchTextOrFunc === 'function' ? searchTextOrFunc :
makeSearchFunc(searchTextOrFunc));
public columnSearch(colId: string, searchFunc: SearchFunc, optMaxResults?: number) {
const maxResults = optMaxResults || Number.POSITIVE_INFINITY;
const rowIds = this.getRowIds();
@@ -87,10 +78,9 @@ export class TableData extends BaseTableData {
console.warn(`TableData.columnSearch called on invalid column ${this.tableId}.${colId}`);
} else {
for (let i = 0; i < rowIds.length && ret.length < maxResults; i++) {
const rowId = rowIds[i];
const value = String(formatter.formatAny(valColumn[i]));
const value = valColumn[i];
if (value && searchFunc(value)) {
ret.push({ label: value, value: rowId });
ret.push(rowIds[i]);
}
}
}
@@ -147,11 +137,3 @@ export class TableData extends BaseTableData {
return applied;
}
}
function makeSearchFunc(searchText: string): SearchFunc {
const searchWords = searchText.toLowerCase().split(/\s+/);
return value => {
const lower = value.toLowerCase();
return searchWords.every(w => lower.includes(w));
};
}

View File

@@ -1,7 +1,9 @@
import { ColumnRec, DocModel, IRowModel, refRecord, ViewSectionRec } from 'app/client/models/DocModel';
import * as modelUtil from 'app/client/models/modelUtil';
import { ReferenceUtils } from 'app/client/lib/ReferenceUtils';
import * as UserType from 'app/client/widgets/UserType';
import { DocumentSettings } from 'app/common/DocumentSettings';
import { extractTypeFromColType } from 'app/common/gristTypes';
import { BaseFormatter, createFormatter } from 'app/common/ValueFormatter';
import { createParser } from 'app/common/ValueParser';
import { Computed, fromKo } from 'grainjs';
@@ -76,7 +78,7 @@ export interface ViewFieldRec extends IRowModel<"_grist_Views_section_field"> {
documentSettings: ko.PureComputed<DocumentSettings>;
valueParser: ko.Computed<((value: string) => any) | undefined>;
valueParser: ko.Computed<(value: string) => any>;
// Helper which adds/removes/updates field's displayCol to match the formula.
saveDisplayFormula(formula: string): Promise<void>|undefined;
@@ -180,9 +182,19 @@ export function createViewFieldRec(this: ViewFieldRec, docModel: DocModel): void
createFormatter(this.column().type(), this.widgetOptionsJson(), this.documentSettings());
};
this.valueParser = ko.pureComputed(() =>
createParser(this.column().type(), this.widgetOptionsJson(), this.documentSettings())
);
this.valueParser = ko.pureComputed(() => {
const docSettings = this.documentSettings();
const type = this.column().type();
if (extractTypeFromColType(type) === "Ref") { // TODO reflists
const vcol = this.visibleColModel();
const vcolParser = createParser(vcol.type(), vcol.widgetOptionsJson(), docSettings);
const refUtils = new ReferenceUtils(this, docModel.docData); // uses several more observables immediately
return (s: string) => refUtils.parseValue(vcolParser(s));
} else {
return createParser(type, this.widgetOptionsJson(), docSettings);
}
});
// The widgetOptions to read and write: either the column's or the field's own.
this._widgetOptionsStr = modelUtil.savingComputed({