(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

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

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({

View File

@ -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() {

View File

@ -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<ICellItem>;
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;
this._utils = new ReferenceUtils(options.field, docData);
// 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 = 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<ACResults<ICellItem>> {
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 {

View File

@ -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<ReferenceItem>;
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;
this._utils = new ReferenceUtils(options.field, docData);
// Construct the formatter for the displayed values using the options from the target column.
this._formatter = field.createVisibleColFormatter();
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<ReferenceItem> = {
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<ReferenceItem>().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<ACResults<ReferenceItem>> {
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<ReferenceItem> = {
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,

View File

@ -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.

View File

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

View File

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