(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
pull/115/head
Alex Hall 3 years ago
parent f0da3eb3b2
commit d63da496a8

@ -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();
}

@ -1,17 +1,16 @@
/** /**
* TableData maintains a single table's data. * TableData maintains a single table's data.
*/ */
import {ColumnACIndexes} from 'app/client/models/ColumnACIndexes'; import { ColumnACIndexes } from 'app/client/models/ColumnACIndexes';
import {ColumnCache} from 'app/client/models/ColumnCache'; import { ColumnCache } from 'app/client/models/ColumnCache';
import {DocData} from 'app/client/models/DocData'; import { DocData } from 'app/client/models/DocData';
import {DocAction, ReplaceTableData, TableDataAction, UserAction} from 'app/common/DocActions'; import { DocAction, ReplaceTableData, TableDataAction, UserAction } from 'app/common/DocActions';
import {isRaisedException} from 'app/common/gristTypes'; import { isRaisedException } from 'app/common/gristTypes';
import {countIf} from 'app/common/gutil'; import { countIf } from 'app/common/gutil';
import {TableData as BaseTableData, ColTypeMap} from 'app/common/TableData'; import { TableData as BaseTableData, ColTypeMap } from 'app/common/TableData';
import {BaseFormatter} from 'app/common/ValueFormatter'; import { Emitter } from 'grainjs';
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. * 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. * Given a colId and a search function, returns a list of matching row IDs, 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.
* @param {String} colId: identifies the column to search. * @param {String} colId: identifies the column to search.
* @param {String|Function} searchTextOrFunc: If a string, then the text to search. It splits the * @param {Function} searchFunc: A function which, given a column value, returns whether to include it.
* 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 [Number] optMaxResults: if given, limit the number of returned results to this. * @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, public columnSearch(colId: string, searchFunc: SearchFunc, optMaxResults?: number) {
searchTextOrFunc: string|SearchFunc, optMaxResults?: number) {
// Search for each of the words in query, case-insensitively.
const searchFunc = (typeof searchTextOrFunc === 'function' ? searchTextOrFunc :
makeSearchFunc(searchTextOrFunc));
const maxResults = optMaxResults || Number.POSITIVE_INFINITY; const maxResults = optMaxResults || Number.POSITIVE_INFINITY;
const rowIds = this.getRowIds(); const rowIds = this.getRowIds();
@ -87,10 +78,9 @@ export class TableData extends BaseTableData {
console.warn(`TableData.columnSearch called on invalid column ${this.tableId}.${colId}`); console.warn(`TableData.columnSearch called on invalid column ${this.tableId}.${colId}`);
} else { } else {
for (let i = 0; i < rowIds.length && ret.length < maxResults; i++) { for (let i = 0; i < rowIds.length && ret.length < maxResults; i++) {
const rowId = rowIds[i]; const value = valColumn[i];
const value = String(formatter.formatAny(valColumn[i]));
if (value && searchFunc(value)) { 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; 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));
};
}

@ -1,7 +1,9 @@
import { ColumnRec, DocModel, IRowModel, refRecord, ViewSectionRec } from 'app/client/models/DocModel'; import { ColumnRec, DocModel, IRowModel, refRecord, ViewSectionRec } from 'app/client/models/DocModel';
import * as modelUtil from 'app/client/models/modelUtil'; import * as modelUtil from 'app/client/models/modelUtil';
import { ReferenceUtils } from 'app/client/lib/ReferenceUtils';
import * as UserType from 'app/client/widgets/UserType'; import * as UserType from 'app/client/widgets/UserType';
import { DocumentSettings } from 'app/common/DocumentSettings'; import { DocumentSettings } from 'app/common/DocumentSettings';
import { extractTypeFromColType } from 'app/common/gristTypes';
import { BaseFormatter, createFormatter } from 'app/common/ValueFormatter'; import { BaseFormatter, createFormatter } from 'app/common/ValueFormatter';
import { createParser } from 'app/common/ValueParser'; import { createParser } from 'app/common/ValueParser';
import { Computed, fromKo } from 'grainjs'; import { Computed, fromKo } from 'grainjs';
@ -76,7 +78,7 @@ export interface ViewFieldRec extends IRowModel<"_grist_Views_section_field"> {
documentSettings: ko.PureComputed<DocumentSettings>; 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. // Helper which adds/removes/updates field's displayCol to match the formula.
saveDisplayFormula(formula: string): Promise<void>|undefined; 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()); createFormatter(this.column().type(), this.widgetOptionsJson(), this.documentSettings());
}; };
this.valueParser = ko.pureComputed(() => this.valueParser = ko.pureComputed(() => {
createParser(this.column().type(), this.widgetOptionsJson(), this.documentSettings()) 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. // The widgetOptions to read and write: either the column's or the field's own.
this._widgetOptionsStr = modelUtil.savingComputed({ this._widgetOptionsStr = modelUtil.savingComputed({

@ -76,7 +76,8 @@ export class NTextEditor extends NewBaseEditor {
} }
public getCellValue(): CellValue { public getCellValue(): CellValue {
return this.textInput.value; const valueParser = this.options.field.valueParser.peek();
return valueParser(this.getTextValue());
} }
public getTextValue() { public getTextValue() {

@ -1,58 +1,35 @@
import {ACResults, buildHighlightedDom, HighlightFunc} from 'app/client/lib/ACIndex'; import { ACResults, buildHighlightedDom, HighlightFunc } from 'app/client/lib/ACIndex';
import {Autocomplete} from 'app/client/lib/autocomplete'; import { Autocomplete } from 'app/client/lib/autocomplete';
import {ICellItem} from 'app/client/models/ColumnACIndexes'; import { ICellItem } from 'app/client/models/ColumnACIndexes';
import {reportError} from 'app/client/models/errors'; import { reportError } from 'app/client/models/errors';
import {TableData} from 'app/client/models/TableData'; import { colors, testId, vars } from 'app/client/ui2018/cssVars';
import {colors, testId, vars} from 'app/client/ui2018/cssVars'; import { icon } from 'app/client/ui2018/icons';
import {icon} from 'app/client/ui2018/icons'; import { menuCssClass } from 'app/client/ui2018/menus';
import {menuCssClass} from 'app/client/ui2018/menus'; import { Options } from 'app/client/widgets/NewBaseEditor';
import {Options} from 'app/client/widgets/NewBaseEditor'; import { NTextEditor } from 'app/client/widgets/NTextEditor';
import {NTextEditor} from 'app/client/widgets/NTextEditor'; import { nocaseEqual, ReferenceUtils } from 'app/client/lib/ReferenceUtils';
import {CellValue} from 'app/common/DocActions'; import { undef } from 'app/common/gutil';
import {getReferencedTableId} from 'app/common/gristTypes'; import { styled } from 'grainjs';
import {undef} from 'app/common/gutil';
import {BaseFormatter} from 'app/common/ValueFormatter';
import {styled} from 'grainjs';
/** /**
* A ReferenceEditor offers an autocomplete of choices from the referenced table. * A ReferenceEditor offers an autocomplete of choices from the referenced table.
*/ */
export class ReferenceEditor extends NTextEditor { export class ReferenceEditor extends NTextEditor {
private _tableData: TableData;
private _formatter: BaseFormatter;
private _enableAddNew: boolean; private _enableAddNew: boolean;
private _showAddNew: boolean = false; private _showAddNew: boolean = false;
private _visibleCol: string;
private _autocomplete?: Autocomplete<ICellItem>; private _autocomplete?: Autocomplete<ICellItem>;
private _utils: ReferenceUtils;
constructor(options: Options) { constructor(options: Options) {
super(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 docData = options.gristDoc.docData;
const tableData = docData.getTable(refTableId); this._utils = new ReferenceUtils(options.field, docData);
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();
// Whether we should enable the "Add New" entry to allow adding new items to the target table. const vcol = this._utils.visibleColModel;
const vcol = field.visibleColModel();
this._enableAddNew = vcol && !vcol.isRealFormula() && !!vcol.colId(); 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). // 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 // But not on readonly mode - here we will reuse default decoration
if (!options.readonly) { if (!options.readonly) {
@ -60,16 +37,16 @@ export class ReferenceEditor extends NTextEditor {
this.cellEditorDiv.appendChild(cssRefEditIcon('FieldReference')); 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 // 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. // 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 (this.isDisposed()) { return; }
if (needReload && this.textInput.value === '') { 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(); this.resizeInput();
} }
if (this._autocomplete) { if (this._autocomplete) {
@ -104,8 +81,8 @@ export class ReferenceEditor extends NTextEditor {
if (selectedItem && if (selectedItem &&
selectedItem.rowId === 'new' && selectedItem.rowId === 'new' &&
selectedItem.text === this.textInput.value) { selectedItem.text === this.textInput.value) {
const colInfo = {[this._visibleCol]: this.textInput.value}; const colInfo = {[this._utils.visibleColId]: this.textInput.value};
selectedItem.rowId = await this._tableData.sendTableAction(["AddRecord", null, colInfo]); selectedItem.rowId = await this._utils.tableData.sendTableAction(["AddRecord", null, colInfo]);
} }
} }
@ -115,34 +92,16 @@ export class ReferenceEditor extends NTextEditor {
if (selectedItem) { if (selectedItem) {
// Selected from the autocomplete dropdown; so we know the *value* (i.e. rowId). // Selected from the autocomplete dropdown; so we know the *value* (i.e. rowId).
return selectedItem.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. // Unchanged from what's already in the cell.
return this.options.cellValue; return this.options.cellValue;
} }
// Search for textInput's value, or else use the typed value itself (as alttext). return super.getCellValue();
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;
}
} }
private _idToText(value: CellValue) { private _idToText() {
if (typeof value === 'number') { return this._utils.idToText(this.options.cellValue);
return this._formatter.formatAny(this._tableData.getValue(value, this._visibleCol));
}
return String(value || '');
} }
/** /**
@ -151,8 +110,7 @@ export class ReferenceEditor extends NTextEditor {
* Also see: prepForSave. * Also see: prepForSave.
*/ */
private async _doSearch(text: string): Promise<ACResults<ICellItem>> { private async _doSearch(text: string): Promise<ACResults<ICellItem>> {
const acIndex = this._tableData.columnACIndexes.getColACIndex(this._visibleCol, this._formatter); const result = this._utils.autocompleteSearch(text);
const result = acIndex.search(text);
this._showAddNew = false; this._showAddNew = false;
if (!this._enableAddNew || !text) { return result; } 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', ` const cssRefEditor = styled('div', `
& > .celleditor_text_editor, & > .celleditor_content_measure { & > .celleditor_text_editor, & > .celleditor_content_measure {

@ -1,22 +1,20 @@
import {createGroup} from 'app/client/components/commands'; import { createGroup } from 'app/client/components/commands';
import {ACItem, ACResults, HighlightFunc} from 'app/client/lib/ACIndex'; import { ACItem, ACResults, HighlightFunc } from 'app/client/lib/ACIndex';
import {IAutocompleteOptions} from 'app/client/lib/autocomplete'; import { IAutocompleteOptions } from 'app/client/lib/autocomplete';
import {IToken, TokenField, tokenFieldStyles} from 'app/client/lib/TokenField'; import { IToken, TokenField, tokenFieldStyles } from 'app/client/lib/TokenField';
import {colors, testId} from 'app/client/ui2018/cssVars'; import { reportError } from 'app/client/models/errors';
import {menuCssClass} from 'app/client/ui2018/menus'; import { colors, testId } from 'app/client/ui2018/cssVars';
import {createMobileButtons, getButtonMargins} from 'app/client/widgets/EditorButtons'; import { menuCssClass } from 'app/client/ui2018/menus';
import {EditorPlacement} from 'app/client/widgets/EditorPlacement'; import { cssInvalidToken } from 'app/client/widgets/ChoiceListCell';
import {NewBaseEditor, Options} from 'app/client/widgets/NewBaseEditor'; import { createMobileButtons, getButtonMargins } from 'app/client/widgets/EditorButtons';
import {csvEncodeRow} from 'app/common/csvFormat'; import { EditorPlacement } from 'app/client/widgets/EditorPlacement';
import {CellValue} from "app/common/DocActions"; import { NewBaseEditor, Options } from 'app/client/widgets/NewBaseEditor';
import {decodeObject, encodeObject} from 'app/plugin/objtypes'; import { cssRefList, renderACItem } from 'app/client/widgets/ReferenceEditor';
import {dom, styled} from 'grainjs'; import { ReferenceUtils } from 'app/client/lib/ReferenceUtils';
import {cssRefList, renderACItem} from 'app/client/widgets/ReferenceEditor'; import { csvEncodeRow } from 'app/common/csvFormat';
import {TableData} from 'app/client/models/TableData'; import { CellValue } from "app/common/DocActions";
import {BaseFormatter} from 'app/common/ValueFormatter'; import { decodeObject, encodeObject } from 'app/plugin/objtypes';
import {reportError} from 'app/client/models/errors'; import { dom, styled } from 'grainjs';
import {getReferencedTableId} from 'app/common/gristTypes';
import {cssInvalidToken} from 'app/client/widgets/ChoiceListCell';
class ReferenceItem implements IToken, ACItem { class ReferenceItem implements IToken, ACItem {
/** /**
@ -43,11 +41,8 @@ export class ReferenceListEditor extends NewBaseEditor {
protected cellEditorDiv: HTMLElement; protected cellEditorDiv: HTMLElement;
protected commandGroup: any; protected commandGroup: any;
private _tableData: TableData;
private _formatter: BaseFormatter;
private _enableAddNew: boolean; private _enableAddNew: boolean;
private _showAddNew: boolean = false; private _showAddNew: boolean = false;
private _visibleCol: string;
private _tokenField: TokenField<ReferenceItem>; private _tokenField: TokenField<ReferenceItem>;
private _textInput: HTMLInputElement; private _textInput: HTMLInputElement;
private _dom: HTMLElement; 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 _contentSizer: HTMLElement; // Invisible element to size the editor with all the tokens
private _inputSizer: HTMLElement; // Part of _contentSizer to size the text input private _inputSizer: HTMLElement; // Part of _contentSizer to size the text input
private _alignment: string; private _alignment: string;
private _utils: ReferenceUtils;
constructor(options: Options) { constructor(options: Options) {
super(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 docData = options.gristDoc.docData;
const tableData = docData.getTable(refTableId); this._utils = new ReferenceUtils(options.field, docData);
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();
const vcol = field.visibleColModel(); const vcol = this._utils.visibleColModel;
// Whether we should enable the "Add New" entry to allow adding new items to the target table.
this._enableAddNew = vcol && !vcol.isRealFormula() && !!vcol.colId(); this._enableAddNew = vcol && !vcol.isRealFormula() && !!vcol.colId();
this._visibleCol = vcol.colId() || 'id';
const acOptions: IAutocompleteOptions<ReferenceItem> = { const acOptions: IAutocompleteOptions<ReferenceItem> = {
menuCssClass: `${menuCssClass} ${cssRefList.className}`, menuCssClass: `${menuCssClass} ${cssRefList.className}`,
search: this._doSearch.bind(this), search: this._doSearch.bind(this),
@ -98,9 +76,9 @@ export class ReferenceListEditor extends NewBaseEditor {
const startRowIds: unknown[] = options.editValue || !Array.isArray(cellValue) ? [] : cellValue; const startRowIds: unknown[] = options.editValue || !Array.isArray(cellValue) ? [] : cellValue;
// If referenced table hasn't loaded yet, hold off on initializing tokens. // 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 ? 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<ReferenceItem>().create(this, { this._tokenField = TokenField.ctor<ReferenceItem>().create(this, {
initialValue: startTokens, 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 // 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. // 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 (this.isDisposed()) { return; }
if (needReload) { if (needReload) {
this._tokenField.setTokens( 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(); this.resizeInput();
} }
@ -210,8 +188,8 @@ export class ReferenceListEditor extends NewBaseEditor {
if (newValues.length === 0) { return; } if (newValues.length === 0) { return; }
// Add the new items to the referenced table. // Add the new items to the referenced table.
const colInfo = {[this._visibleCol]: newValues.map(t => t.text)}; const colInfo = {[this._utils.visibleColId]: newValues.map(t => t.text)};
const rowIds = await this._tableData.sendTableAction( const rowIds = await this._utils.tableData.sendTableAction(
["BulkAddRecord", new Array(newValues.length).fill(null), colInfo] ["BulkAddRecord", new Array(newValues.length).fill(null), colInfo]
); );
@ -276,8 +254,7 @@ export class ReferenceListEditor extends NewBaseEditor {
* Also see: prepForSave. * Also see: prepForSave.
*/ */
private async _doSearch(text: string): Promise<ACResults<ReferenceItem>> { private async _doSearch(text: string): Promise<ACResults<ReferenceItem>> {
const acIndex = this._tableData.columnACIndexes.getColACIndex(this._visibleCol, this._formatter); const {items, selectIndex, highlightFunc} = this._utils.autocompleteSearch(text);
const {items, selectIndex, highlightFunc} = acIndex.search(text);
const result: ACResults<ReferenceItem> = { const result: ACResults<ReferenceItem> = {
selectIndex, selectIndex,
highlightFunc, highlightFunc,
@ -298,13 +275,6 @@ export class ReferenceListEditor extends NewBaseEditor {
return result; 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) { private _renderItem(item: ReferenceItem, highlightFunc: HighlightFunc) {
return renderACItem( return renderACItem(
item.text, item.text,

@ -159,6 +159,10 @@ export class TableData extends ActionDispatcher implements SkippableRows {
return colData && index !== undefined ? colData.values[index] : undefined; 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 * 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. * column of that row. The returned function is faster than getValue() calls.

@ -64,10 +64,11 @@ delete parsers.DateTime;
export function createParser( export function createParser(
type: string, widgetOpts: FormatOptions, docSettings: DocumentSettings type: string, widgetOpts: FormatOptions, docSettings: DocumentSettings
): ((value: string) => any) | undefined { ): (value: string) => any {
const cls = parsers[gristTypes.extractTypeFromColType(type)]; const cls = parsers[gristTypes.extractTypeFromColType(type)];
if (cls) { if (cls) {
const parser = new cls(type, widgetOpts, docSettings); const parser = new cls(type, widgetOpts, docSettings);
return parser.cleanParse.bind(parser); return parser.cleanParse.bind(parser);
} }
return value => value;
} }

@ -1711,6 +1711,38 @@ export async function setDateFormat(format: string) {
await waitForServer(); await waitForServer();
} }
/**
* Returns "Show column" setting value of a reference column.
*/
export async function getRefShowColumn(): Promise<string> {
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<string> {
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 } // end of namespace gristUtils
stackWrapOwnMethods(gristUtils); stackWrapOwnMethods(gristUtils);

Loading…
Cancel
Save