gristlabs_grist-core/app/client/models/TableData.ts
Alex Hall d63da496a8 (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
2021-11-01 19:31:52 +02:00

140 lines
5.6 KiB
TypeScript

/**
* 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 { Emitter } from 'grainjs';
export type SearchFunc = (value: any) => boolean;
/**
* TableData class to maintain a single table's data.
*/
export class TableData extends BaseTableData {
public readonly tableActionEmitter = new Emitter();
public readonly dataLoadedEmitter = new Emitter();
public readonly columnACIndexes = new ColumnACIndexes(this);
private _columnErrorCounts = new ColumnCache<number|undefined>(this);
/**
* Constructor for TableData.
* @param {DocData} docData: The root DocData object for this document.
* @param {String} tableId: The name of this table.
* @param {Object} tableData: An object equivalent to BulkAddRecord, i.e.
* ["TableData", tableId, rowIds, columnValues].
* @param {Object} columnTypes: A map of colId to colType.
*/
constructor(public readonly docData: DocData,
tableId: string, tableData: TableDataAction|null, columnTypes: ColTypeMap) {
super(tableId, tableData, columnTypes);
}
public loadData(tableData: TableDataAction|ReplaceTableData): number[] {
const oldRowIds = super.loadData(tableData);
// If called from base constructor, this.dataLoadedEmitter may be unset; in that case there
// are no subscribers anyway.
if (this.dataLoadedEmitter) {
this.dataLoadedEmitter.emit(oldRowIds, this.getRowIds());
}
return oldRowIds;
}
// Used by QuerySet to load new rows for onDemand tables.
public loadPartial(data: TableDataAction): void {
super.loadPartial(data);
// Emit dataLoaded event, to trigger ('rowChange', 'add') on the TableModel RowSource.
this.dataLoadedEmitter.emit([], data[2]);
}
// Used by QuerySet to remove unused rows for onDemand tables when a QuerySet is disposed.
public unloadPartial(rowIds: number[]): void {
super.unloadPartial(rowIds);
// Emit dataLoaded event, to trigger ('rowChange', 'rm') on the TableModel RowSource.
this.dataLoadedEmitter.emit(rowIds, []);
}
/**
* 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 {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[Number] array of row IDs.
*/
public columnSearch(colId: string, searchFunc: SearchFunc, optMaxResults?: number) {
const maxResults = optMaxResults || Number.POSITIVE_INFINITY;
const rowIds = this.getRowIds();
const valColumn = this.getColValues(colId);
const ret = [];
if (!valColumn) {
// tslint:disable-next-line:no-console
console.warn(`TableData.columnSearch called on invalid column ${this.tableId}.${colId}`);
} else {
for (let i = 0; i < rowIds.length && ret.length < maxResults; i++) {
const value = valColumn[i];
if (value && searchFunc(value)) {
ret.push(rowIds[i]);
}
}
}
return ret;
}
/**
* Counts and returns the number of error values in the given column. The count is cached to
* keep it faster for large tables, and the cache is cleared as needed on changes to the table.
*/
public countErrors(colId: string): number|undefined {
return this._columnErrorCounts.getValue(colId, () => {
const values = this.getColValues(colId);
return values && countIf(values, isRaisedException);
});
}
/**
* Sends an array of table-specific action to the server to be applied. The tableId should be
* omitted from each `action` parameter and will be inserted automatically.
*
* @param {Array} actions: Array of user actions of the form [actionType, rowId, etc], which is sent
* to the server as [actionType, **tableId**, rowId, etc]
* @param {String} optDesc: Optional description of the actions to be shown in the log.
* @returns {Array} Array of return values for all the UserActions as produced by the data engine.
*/
public sendTableActions(actions: UserAction[], optDesc?: string) {
actions.forEach((action) => action.splice(1, 0, this.tableId));
return this.docData.sendActions(actions as DocAction[], optDesc);
}
/**
* Sends a table-specific action to the server. The tableId should be omitted from the action parameter
* and will be inserted automatically.
*
* @param {Array} action: [actionType, rowId...], sent as [actionType, **tableId**, rowId...]
* @param {String} optDesc: Optional description of the actions to be shown in the log.
* @returns {Object} Return value for the UserAction as produced by the data engine.
*/
public sendTableAction(action: UserAction, optDesc?: string) {
if (!action) { return; }
action.splice(1, 0, this.tableId);
return this.docData.sendAction(action as DocAction, optDesc);
}
/**
* Emits a table-specific action received from the server as a 'tableAction' event.
*/
public receiveAction(action: DocAction): boolean {
const applied = super.receiveAction(action);
if (applied) {
this.tableActionEmitter.emit(action);
}
return applied;
}
}