gristlabs_grist-core/app/common/DocActions.ts
Paul Fitzpatrick ac5452c89f (core) add grist.onRecord and grist.onRecords event handlers
Summary:
This simplifies writing custom widgets that access selected
data.  To access the record at which the cursor is set, and
get any future changes to it as the cursor moves or data
changes, it suffices now to do:

```
grist.ready();
grist.onRecord(record => /* render */);
```

Similarly to access the set of selected records, and get any
changes, it suffices now to do:

```
grist.ready();
grist.onRecords(records => /* render */);
```

The `records` argument will be a list of objects, each of which
is a single record.  This is distinct from the column-based
representation favored in Grist up ontil now.  That remains
how methods like `fetchTable` or `fetchSelectedTable` represent
their results.  In the future, methods named like `fetchRecords`
or `fetchSelectedRecords` could be added that return lists.

Test Plan: extended tests

Reviewers: dsagal

Reviewed By: dsagal

Differential Revision: https://phab.getgrist.com/D2583
2020-08-13 14:34:23 -04:00

147 lines
6.1 KiB
TypeScript

/**
* This mirrors action definitions from sandbox/grist/actions.py
*/
// Some definitions have moved to be part of plugin API.
import { CellValue } from 'app/plugin/GristData';
export { CellValue, RowRecord } from 'app/plugin/GristData';
import map = require('lodash/map');
export type AddRecord = ['AddRecord', string, number, ColValues];
export type BulkAddRecord = ['BulkAddRecord', string, number[], BulkColValues];
export type RemoveRecord = ['RemoveRecord', string, number];
export type BulkRemoveRecord = ['BulkRemoveRecord', string, number[]];
export type UpdateRecord = ['UpdateRecord', string, number, ColValues];
export type BulkUpdateRecord = ['BulkUpdateRecord', string, number[], BulkColValues];
export type ReplaceTableData = ['ReplaceTableData', string, number[], BulkColValues];
// This is the format in which data comes when we fetch a table from the sandbox.
export type TableDataAction = ['TableData', string, number[], BulkColValues];
export type AddColumn = ['AddColumn', string, string, ColInfo];
export type RemoveColumn = ['RemoveColumn', string, string];
export type RenameColumn = ['RenameColumn', string, string, string];
export type ModifyColumn = ['ModifyColumn', string, string, ColInfo];
export type AddTable = ['AddTable', string, ColInfoWithId[]];
export type RemoveTable = ['RemoveTable', string];
export type RenameTable = ['RenameTable', string, string];
export type DocAction = (
AddRecord |
BulkAddRecord |
RemoveRecord |
BulkRemoveRecord |
UpdateRecord |
BulkUpdateRecord |
ReplaceTableData |
TableDataAction |
AddColumn |
RemoveColumn |
RenameColumn |
ModifyColumn |
AddTable |
RemoveTable |
RenameTable
);
// type guards for convenience - see:
// https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards
export function isAddRecord(act: DocAction): act is AddRecord { return act[0] === 'AddRecord'; }
export function isBulkAddRecord(act: DocAction): act is BulkAddRecord { return act[0] === 'BulkAddRecord'; }
export function isRemoveRecord(act: DocAction): act is RemoveRecord { return act[0] === 'RemoveRecord'; }
export function isBulkRemoveRecord(act: DocAction): act is BulkRemoveRecord { return act[0] === 'BulkRemoveRecord'; }
export function isUpdateRecord(act: DocAction): act is UpdateRecord { return act[0] === 'UpdateRecord'; }
export function isBulkUpdateRecord(act: DocAction): act is BulkUpdateRecord { return act[0] === 'BulkUpdateRecord'; }
export function isReplaceTableData(act: DocAction): act is ReplaceTableData { return act[0] === 'ReplaceTableData'; }
export function isAddColumn(act: DocAction): act is AddColumn { return act[0] === 'AddColumn'; }
export function isRemoveColumn(act: DocAction): act is RemoveColumn { return act[0] === 'RemoveColumn'; }
export function isRenameColumn(act: DocAction): act is RenameColumn { return act[0] === 'RenameColumn'; }
export function isModifyColumn(act: DocAction): act is ModifyColumn { return act[0] === 'ModifyColumn'; }
export function isAddTable(act: DocAction): act is AddTable { return act[0] === 'AddTable'; }
export function isRemoveTable(act: DocAction): act is RemoveTable { return act[0] === 'RemoveTable'; }
export function isRenameTable(act: DocAction): act is RenameTable { return act[0] === 'RenameTable'; }
const SCHEMA_ACTIONS = new Set(['AddTable', 'RemoveTable', 'RenameTable', 'AddColumn',
'RemoveColumn', 'RenameColumn', 'ModifyColumn']);
/**
* Determines whether a given action is a schema action or not.
*/
export function isSchemaAction(action: DocAction): boolean {
return SCHEMA_ACTIONS.has(action[0]);
}
/**
* Returns the tableId from the action.
*/
export function getTableId(action: DocAction): string {
return action[1]; // It happens to always be in the same position in the action tuple.
}
// Helper types used in the definitions above.
export interface ColValues { [colId: string]: CellValue; }
export interface BulkColValues { [colId: string]: CellValue[]; }
export interface ColInfoMap { [colId: string]: ColInfo; }
export interface ColInfo {
type: string;
isFormula: boolean;
formula: string;
}
export interface ColInfoWithId extends ColInfo {
id: string;
}
// Multiple records in column-oriented format, i.e. same as BulkColValues but with a mandatory
// 'id' column. This is preferred over TableDataAction in external APIs.
export interface TableColValues {
id: number[];
[colId: string]: CellValue[];
}
// Both UserActions and DocActions are represented as [ActionName, ...actionArgs].
// TODO I think it's better to represent DocAction as a Buffer containing the marshalled action.
export type UserAction = Array<string|number|object|boolean|null|undefined>;
/**
* Gives a description for an action which involves setting values to a selection.
* @param {Array} action - The (Bulk)AddRecord/(Bulk)UpdateRecord action to describe.
* @param {Boolean} optExcludeVals - Indicates whether the values should be excluded from
* the description.
*/
export function getSelectionDesc(action: UserAction, optExcludeVals: boolean): string {
const table = action[1];
const rows = action[2];
const colValues: number[] = action[3] as any; // TODO: better typing - but code may evaporate
const columns = map(colValues, (values, col) => optExcludeVals ? col : `${col}: ${values}`);
const s = typeof rows === 'object' ? 's' : '';
return `table ${table}, row${s} ${rows}; ${columns.join(", ")}`;
}
// Convert from TableColValues (used by DocStorage and external APIs) to TableDataAction (used
// mainly by the sandbox).
export function toTableDataAction(tableId: string, colValues: TableColValues): TableDataAction {
const colData = {...colValues}; // Make a copy to avoid changing passed-in arguments.
const rowIds: number[] = colData.id;
delete colData.id;
return ['TableData', tableId, rowIds, colData];
}
// Convert from TableDataAction (used mainly by the sandbox) to TableColValues (used by DocStorage
// and external APIs).
export function fromTableDataAction(tableData: TableDataAction): TableColValues {
const rowIds: number[] = tableData[2];
const colValues: BulkColValues = tableData[3];
return {id: rowIds, ...colValues};
}