From d63da496a8d582e5d382f4644b79ba4a71b21483 Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Mon, 1 Nov 2021 17:48:08 +0200 Subject: [PATCH] (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 --- app/client/lib/ReferenceUtils.ts | 98 ++++++++++++++++++++++ app/client/models/TableData.ts | 48 ++++------- app/client/models/entities/ViewFieldRec.ts | 20 ++++- app/client/widgets/NTextEditor.ts | 3 +- app/client/widgets/ReferenceEditor.ts | 97 ++++++--------------- app/client/widgets/ReferenceListEditor.ts | 84 ++++++------------- app/common/TableData.ts | 4 + app/common/ValueParser.ts | 3 +- test/nbrowser/gristUtils.ts | 32 +++++++ 9 files changed, 222 insertions(+), 167 deletions(-) create mode 100644 app/client/lib/ReferenceUtils.ts diff --git a/app/client/lib/ReferenceUtils.ts b/app/client/lib/ReferenceUtils.ts new file mode 100644 index 00000000..43193b98 --- /dev/null +++ b/app/client/lib/ReferenceUtils.ts @@ -0,0 +1,98 @@ +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 } 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; + + 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'; + } + + public parseValue(value: any): number | string { + if (!value) { + return 0; // This is the default value for a reference column. + } + + if (this.visibleColId === 'id') { + const n = Number(value); + if ( + n > 0 && + Number.isInteger(n) && + !( + this.tableData.isLoaded && + !this.tableData.hasRowId(n) + ) + ) { + return n; + } + return String(value); + } + + let searchFunc: SearchFunc; + if (typeof value === 'string') { + searchFunc = (v: any) => { + const formatted = this.formatter.formatAny(v); + return nocaseEqual(formatted, value); + }; + } else { + 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. + // Can't return `value` directly because it may be a number (if visibleCol is a numeric or date column) + // which would be interpreted as a row ID, i.e. a valid reference. + // So instead we format the parsed value in the style of visibleCol. + return this.formatter.formatAny(value); + } + } + + 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(); +} diff --git a/app/client/models/TableData.ts b/app/client/models/TableData.ts index 2c5ff118..35c582f4 100644 --- a/app/client/models/TableData.ts +++ b/app/client/models/TableData.ts @@ -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)); - }; -} diff --git a/app/client/models/entities/ViewFieldRec.ts b/app/client/models/entities/ViewFieldRec.ts index 6f177fc3..93594410 100644 --- a/app/client/models/entities/ViewFieldRec.ts +++ b/app/client/models/entities/ViewFieldRec.ts @@ -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; - 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|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({ diff --git a/app/client/widgets/NTextEditor.ts b/app/client/widgets/NTextEditor.ts index fdc7d8a3..e1e1e44a 100644 --- a/app/client/widgets/NTextEditor.ts +++ b/app/client/widgets/NTextEditor.ts @@ -76,7 +76,8 @@ export class NTextEditor extends NewBaseEditor { } public getCellValue(): CellValue { - return this.textInput.value; + const valueParser = this.options.field.valueParser.peek(); + return valueParser(this.getTextValue()); } public getTextValue() { diff --git a/app/client/widgets/ReferenceEditor.ts b/app/client/widgets/ReferenceEditor.ts index 7194b6d8..7391eecd 100644 --- a/app/client/widgets/ReferenceEditor.ts +++ b/app/client/widgets/ReferenceEditor.ts @@ -1,58 +1,35 @@ -import {ACResults, buildHighlightedDom, HighlightFunc} from 'app/client/lib/ACIndex'; -import {Autocomplete} from 'app/client/lib/autocomplete'; -import {ICellItem} from 'app/client/models/ColumnACIndexes'; -import {reportError} from 'app/client/models/errors'; -import {TableData} from 'app/client/models/TableData'; -import {colors, testId, vars} from 'app/client/ui2018/cssVars'; -import {icon} from 'app/client/ui2018/icons'; -import {menuCssClass} from 'app/client/ui2018/menus'; -import {Options} from 'app/client/widgets/NewBaseEditor'; -import {NTextEditor} from 'app/client/widgets/NTextEditor'; -import {CellValue} from 'app/common/DocActions'; -import {getReferencedTableId} from 'app/common/gristTypes'; -import {undef} from 'app/common/gutil'; -import {BaseFormatter} from 'app/common/ValueFormatter'; -import {styled} from 'grainjs'; +import { ACResults, buildHighlightedDom, HighlightFunc } from 'app/client/lib/ACIndex'; +import { Autocomplete } from 'app/client/lib/autocomplete'; +import { ICellItem } from 'app/client/models/ColumnACIndexes'; +import { reportError } from 'app/client/models/errors'; +import { colors, testId, vars } from 'app/client/ui2018/cssVars'; +import { icon } from 'app/client/ui2018/icons'; +import { menuCssClass } from 'app/client/ui2018/menus'; +import { Options } from 'app/client/widgets/NewBaseEditor'; +import { NTextEditor } from 'app/client/widgets/NTextEditor'; +import { nocaseEqual, ReferenceUtils } from 'app/client/lib/ReferenceUtils'; +import { undef } from 'app/common/gutil'; +import { styled } from 'grainjs'; /** * A ReferenceEditor offers an autocomplete of choices from the referenced table. */ export class ReferenceEditor extends NTextEditor { - private _tableData: TableData; - private _formatter: BaseFormatter; private _enableAddNew: boolean; private _showAddNew: boolean = false; - private _visibleCol: string; private _autocomplete?: Autocomplete; + private _utils: ReferenceUtils; constructor(options: Options) { super(options); - const field = options.field; - - // Get the table ID to which the reference points. - const refTableId = getReferencedTableId(field.column().type()); - if (!refTableId) { - throw new Error("ReferenceEditor used for non-Reference column"); - } - const docData = options.gristDoc.docData; - const tableData = docData.getTable(refTableId); - if (!tableData) { - throw new Error("ReferenceEditor: invalid referenced table"); - } - this._tableData = tableData; - - // Construct the formatter for the displayed values using the options from the target column. - this._formatter = field.createVisibleColFormatter(); + this._utils = new ReferenceUtils(options.field, docData); - // Whether we should enable the "Add New" entry to allow adding new items to the target table. - const vcol = field.visibleColModel(); + const vcol = this._utils.visibleColModel; this._enableAddNew = vcol && !vcol.isRealFormula() && !!vcol.colId(); - this._visibleCol = vcol.colId() || 'id'; - // Decorate the editor to look like a reference column value (with a "link" icon). // But not on readonly mode - here we will reuse default decoration if (!options.readonly) { @@ -60,16 +37,16 @@ export class ReferenceEditor extends NTextEditor { this.cellEditorDiv.appendChild(cssRefEditIcon('FieldReference')); } - this.textInput.value = undef(options.state, options.editValue, this._idToText(options.cellValue)); + this.textInput.value = undef(options.state, options.editValue, this._idToText()); - const needReload = (options.editValue === undefined && !tableData.isLoaded); + const needReload = (options.editValue === undefined && !this._utils.tableData.isLoaded); // The referenced table has probably already been fetched (because there must already be a // Reference widget instantiated), but it's better to avoid this assumption. - docData.fetchTable(refTableId).then(() => { + docData.fetchTable(this._utils.refTableId).then(() => { if (this.isDisposed()) { return; } if (needReload && this.textInput.value === '') { - this.textInput.value = undef(options.state, options.editValue, this._idToText(options.cellValue)); + this.textInput.value = undef(options.state, options.editValue, this._idToText()); this.resizeInput(); } if (this._autocomplete) { @@ -104,8 +81,8 @@ export class ReferenceEditor extends NTextEditor { if (selectedItem && selectedItem.rowId === 'new' && selectedItem.text === this.textInput.value) { - const colInfo = {[this._visibleCol]: this.textInput.value}; - selectedItem.rowId = await this._tableData.sendTableAction(["AddRecord", null, colInfo]); + const colInfo = {[this._utils.visibleColId]: this.textInput.value}; + selectedItem.rowId = await this._utils.tableData.sendTableAction(["AddRecord", null, colInfo]); } } @@ -115,34 +92,16 @@ export class ReferenceEditor extends NTextEditor { if (selectedItem) { // Selected from the autocomplete dropdown; so we know the *value* (i.e. rowId). return selectedItem.rowId; - } else if (nocaseEqual(this.textInput.value, this._idToText(this.options.cellValue))) { + } else if (nocaseEqual(this.textInput.value, this._idToText())) { // Unchanged from what's already in the cell. return this.options.cellValue; } - // Search for textInput's value, or else use the typed value itself (as alttext). - if (this.textInput.value === '') { - return 0; // This is the default value for a reference column. - } - const searchFunc = (value: any) => nocaseEqual(value, this.textInput.value); - const matches = this._tableData.columnSearch(this._visibleCol, this._formatter, searchFunc, 1); - if (matches.length > 0) { - return matches[0].value; - } else { - const value = this.textInput.value; - if (this._visibleCol === 'id') { - // If the value is a valid number (non-NaN), save as a numeric rowId; else as text. - return +value || value; - } - return value; - } + return super.getCellValue(); } - private _idToText(value: CellValue) { - if (typeof value === 'number') { - return this._formatter.formatAny(this._tableData.getValue(value, this._visibleCol)); - } - return String(value || ''); + private _idToText() { + return this._utils.idToText(this.options.cellValue); } /** @@ -151,8 +110,7 @@ export class ReferenceEditor extends NTextEditor { * Also see: prepForSave. */ private async _doSearch(text: string): Promise> { - const acIndex = this._tableData.columnACIndexes.getColACIndex(this._visibleCol, this._formatter); - const result = acIndex.search(text); + const result = this._utils.autocompleteSearch(text); this._showAddNew = false; if (!this._enableAddNew || !text) { return result; } @@ -186,9 +144,6 @@ export function renderACItem(text: string, highlightFunc: HighlightFunc, isAddNe ); } -function nocaseEqual(a: string, b: string) { - return a.trim().toLowerCase() === b.trim().toLowerCase(); -} const cssRefEditor = styled('div', ` & > .celleditor_text_editor, & > .celleditor_content_measure { diff --git a/app/client/widgets/ReferenceListEditor.ts b/app/client/widgets/ReferenceListEditor.ts index 714b37d1..19f0147a 100644 --- a/app/client/widgets/ReferenceListEditor.ts +++ b/app/client/widgets/ReferenceListEditor.ts @@ -1,22 +1,20 @@ -import {createGroup} from 'app/client/components/commands'; -import {ACItem, ACResults, HighlightFunc} from 'app/client/lib/ACIndex'; -import {IAutocompleteOptions} from 'app/client/lib/autocomplete'; -import {IToken, TokenField, tokenFieldStyles} from 'app/client/lib/TokenField'; -import {colors, testId} from 'app/client/ui2018/cssVars'; -import {menuCssClass} from 'app/client/ui2018/menus'; -import {createMobileButtons, getButtonMargins} from 'app/client/widgets/EditorButtons'; -import {EditorPlacement} from 'app/client/widgets/EditorPlacement'; -import {NewBaseEditor, Options} from 'app/client/widgets/NewBaseEditor'; -import {csvEncodeRow} from 'app/common/csvFormat'; -import {CellValue} from "app/common/DocActions"; -import {decodeObject, encodeObject} from 'app/plugin/objtypes'; -import {dom, styled} from 'grainjs'; -import {cssRefList, renderACItem} from 'app/client/widgets/ReferenceEditor'; -import {TableData} from 'app/client/models/TableData'; -import {BaseFormatter} from 'app/common/ValueFormatter'; -import {reportError} from 'app/client/models/errors'; -import {getReferencedTableId} from 'app/common/gristTypes'; -import {cssInvalidToken} from 'app/client/widgets/ChoiceListCell'; +import { createGroup } from 'app/client/components/commands'; +import { ACItem, ACResults, HighlightFunc } from 'app/client/lib/ACIndex'; +import { IAutocompleteOptions } from 'app/client/lib/autocomplete'; +import { IToken, TokenField, tokenFieldStyles } from 'app/client/lib/TokenField'; +import { reportError } from 'app/client/models/errors'; +import { colors, testId } from 'app/client/ui2018/cssVars'; +import { menuCssClass } from 'app/client/ui2018/menus'; +import { cssInvalidToken } from 'app/client/widgets/ChoiceListCell'; +import { createMobileButtons, getButtonMargins } from 'app/client/widgets/EditorButtons'; +import { EditorPlacement } from 'app/client/widgets/EditorPlacement'; +import { NewBaseEditor, Options } from 'app/client/widgets/NewBaseEditor'; +import { cssRefList, renderACItem } from 'app/client/widgets/ReferenceEditor'; +import { ReferenceUtils } from 'app/client/lib/ReferenceUtils'; +import { csvEncodeRow } from 'app/common/csvFormat'; +import { CellValue } from "app/common/DocActions"; +import { decodeObject, encodeObject } from 'app/plugin/objtypes'; +import { dom, styled } from 'grainjs'; class ReferenceItem implements IToken, ACItem { /** @@ -43,11 +41,8 @@ export class ReferenceListEditor extends NewBaseEditor { protected cellEditorDiv: HTMLElement; protected commandGroup: any; - private _tableData: TableData; - private _formatter: BaseFormatter; private _enableAddNew: boolean; private _showAddNew: boolean = false; - private _visibleCol: string; private _tokenField: TokenField; private _textInput: HTMLInputElement; private _dom: HTMLElement; @@ -55,34 +50,17 @@ export class ReferenceListEditor extends NewBaseEditor { private _contentSizer: HTMLElement; // Invisible element to size the editor with all the tokens private _inputSizer: HTMLElement; // Part of _contentSizer to size the text input private _alignment: string; + private _utils: ReferenceUtils; constructor(options: Options) { super(options); - const field = options.field; - - // Get the table ID to which the reference list points. - const refTableId = getReferencedTableId(field.column().type()); - if (!refTableId) { - throw new Error("ReferenceListEditor used for non-ReferenceList column"); - } - const docData = options.gristDoc.docData; - const tableData = docData.getTable(refTableId); - if (!tableData) { - throw new Error("ReferenceListEditor: invalid referenced table"); - } - this._tableData = tableData; - - // Construct the formatter for the displayed values using the options from the target column. - this._formatter = field.createVisibleColFormatter(); + this._utils = new ReferenceUtils(options.field, docData); - const vcol = field.visibleColModel(); - // Whether we should enable the "Add New" entry to allow adding new items to the target table. + const vcol = this._utils.visibleColModel; this._enableAddNew = vcol && !vcol.isRealFormula() && !!vcol.colId(); - this._visibleCol = vcol.colId() || 'id'; - const acOptions: IAutocompleteOptions = { menuCssClass: `${menuCssClass} ${cssRefList.className}`, search: this._doSearch.bind(this), @@ -98,9 +76,9 @@ export class ReferenceListEditor extends NewBaseEditor { const startRowIds: unknown[] = options.editValue || !Array.isArray(cellValue) ? [] : cellValue; // If referenced table hasn't loaded yet, hold off on initializing tokens. - const needReload = (options.editValue === undefined && !tableData.isLoaded); + const needReload = (options.editValue === undefined && !this._utils.tableData.isLoaded); const startTokens = needReload ? - [] : startRowIds.map(id => new ReferenceItem(this._idToText(id), typeof id === 'number' ? id : 'invalid')); + [] : startRowIds.map(id => new ReferenceItem(this._utils.idToText(id), typeof id === 'number' ? id : 'invalid')); this._tokenField = TokenField.ctor().create(this, { initialValue: startTokens, @@ -146,11 +124,11 @@ export class ReferenceListEditor extends NewBaseEditor { // The referenced table has probably already been fetched (because there must already be a // Reference widget instantiated), but it's better to avoid this assumption. - docData.fetchTable(refTableId).then(() => { + docData.fetchTable(this._utils.refTableId).then(() => { if (this.isDisposed()) { return; } if (needReload) { this._tokenField.setTokens( - startRowIds.map(id => new ReferenceItem(this._idToText(id), typeof id === 'number' ? id : 'invalid')) + startRowIds.map(id => new ReferenceItem(this._utils.idToText(id), typeof id === 'number' ? id : 'invalid')) ); this.resizeInput(); } @@ -210,8 +188,8 @@ export class ReferenceListEditor extends NewBaseEditor { if (newValues.length === 0) { return; } // Add the new items to the referenced table. - const colInfo = {[this._visibleCol]: newValues.map(t => t.text)}; - const rowIds = await this._tableData.sendTableAction( + const colInfo = {[this._utils.visibleColId]: newValues.map(t => t.text)}; + const rowIds = await this._utils.tableData.sendTableAction( ["BulkAddRecord", new Array(newValues.length).fill(null), colInfo] ); @@ -276,8 +254,7 @@ export class ReferenceListEditor extends NewBaseEditor { * Also see: prepForSave. */ private async _doSearch(text: string): Promise> { - const acIndex = this._tableData.columnACIndexes.getColACIndex(this._visibleCol, this._formatter); - const {items, selectIndex, highlightFunc} = acIndex.search(text); + const {items, selectIndex, highlightFunc} = this._utils.autocompleteSearch(text); const result: ACResults = { selectIndex, highlightFunc, @@ -298,13 +275,6 @@ export class ReferenceListEditor extends NewBaseEditor { return result; } - private _idToText(value: unknown) { - if (typeof value === 'number') { - return this._formatter.formatAny(this._tableData.getValue(value, this._visibleCol)); - } - return String(value || ''); - } - private _renderItem(item: ReferenceItem, highlightFunc: HighlightFunc) { return renderACItem( item.text, diff --git a/app/common/TableData.ts b/app/common/TableData.ts index e8152b65..131c1303 100644 --- a/app/common/TableData.ts +++ b/app/common/TableData.ts @@ -159,6 +159,10 @@ export class TableData extends ActionDispatcher implements SkippableRows { return colData && index !== undefined ? colData.values[index] : undefined; } + public hasRowId(rowId: number): boolean { + return this._rowMap.has(rowId); + } + /** * Given a column name, returns a function that takes a rowId and returns the value for that * column of that row. The returned function is faster than getValue() calls. diff --git a/app/common/ValueParser.ts b/app/common/ValueParser.ts index 9001773c..6ce641c7 100644 --- a/app/common/ValueParser.ts +++ b/app/common/ValueParser.ts @@ -64,10 +64,11 @@ delete parsers.DateTime; export function createParser( type: string, widgetOpts: FormatOptions, docSettings: DocumentSettings -): ((value: string) => any) | undefined { +): (value: string) => any { const cls = parsers[gristTypes.extractTypeFromColType(type)]; if (cls) { const parser = new cls(type, widgetOpts, docSettings); return parser.cleanParse.bind(parser); } + return value => value; } diff --git a/test/nbrowser/gristUtils.ts b/test/nbrowser/gristUtils.ts index 522d07f8..03f249f8 100644 --- a/test/nbrowser/gristUtils.ts +++ b/test/nbrowser/gristUtils.ts @@ -1711,6 +1711,38 @@ export async function setDateFormat(format: string) { await waitForServer(); } +/** + * Returns "Show column" setting value of a reference column. + */ +export async function getRefShowColumn(): Promise { + return driver.find('.test-fbuilder-ref-col-select').getText(); +} + +/** + * Changes "Show column" setting value of a reference column. + */ +export async function setRefShowColumn(col: string) { + await driver.find('.test-fbuilder-ref-col-select').click(); + await driver.findContent('.test-select-menu .test-select-row', col).click(); + await waitForServer(); +} + +/** + * Returns "Data from table" setting value of a reference column. + */ +export async function getRefTable(): Promise { + return driver.find('.test-fbuilder-ref-table-select').getText(); +} + +/** + * Changes "Data from table" setting value of a reference column. + */ +export async function setRefTable(table: string) { + await driver.find('.test-fbuilder-ref-table-select').click(); + await driver.findContent('.test-select-menu .test-select-row', table).click(); + await waitForServer(); +} + } // end of namespace gristUtils stackWrapOwnMethods(gristUtils);