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);