From ac5452c89f4dba69e0160a17499e36ce903c1759 Mon Sep 17 00:00:00 2001 From: Paul Fitzpatrick Date: Thu, 13 Aug 2020 14:13:16 -0400 Subject: [PATCH] (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 --- app/common/DocActions.ts | 10 ++++------ app/plugin/GristAPI-ti.ts | 1 + app/plugin/GristAPI.ts | 3 +++ app/plugin/GristData.ts | 6 ++++++ app/plugin/GristTable-ti.ts | 11 +++++++++++ app/plugin/grist-plugin-api.ts | 35 ++++++++++++++++++++++++++++++++++ 6 files changed, 60 insertions(+), 6 deletions(-) create mode 100644 app/plugin/GristData.ts diff --git a/app/common/DocActions.ts b/app/common/DocActions.ts index 1b137f4a..894c84c9 100644 --- a/app/common/DocActions.ts +++ b/app/common/DocActions.ts @@ -2,6 +2,10 @@ * 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]; @@ -83,7 +87,6 @@ export function getTableId(action: DocAction): string { // Helper types used in the definitions above. -export type CellValue = number|string|boolean|null|[string, any?]; export interface ColValues { [colId: string]: CellValue; } export interface BulkColValues { [colId: string]: CellValue[]; } export interface ColInfoMap { [colId: string]: ColInfo; } @@ -98,11 +101,6 @@ export interface ColInfoWithId extends ColInfo { id: string; } -export interface RowRecord { - id: number; - [colId: string]: CellValue; -} - // 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 { diff --git a/app/plugin/GristAPI-ti.ts b/app/plugin/GristAPI-ti.ts index 096d0135..c1a599bf 100644 --- a/app/plugin/GristAPI-ti.ts +++ b/app/plugin/GristAPI-ti.ts @@ -23,6 +23,7 @@ export const GristDocAPI = t.iface([], { export const GristView = t.iface([], { "fetchSelectedTable": t.func("any"), + "fetchSelectedRecord": t.func("any", t.param("rowId", "number")), }); const exportedTypeSuite: t.ITypeSuite = { diff --git a/app/plugin/GristAPI.ts b/app/plugin/GristAPI.ts index 7560d613..405cef60 100644 --- a/app/plugin/GristAPI.ts +++ b/app/plugin/GristAPI.ts @@ -96,4 +96,7 @@ export interface GristView { // TODO: return type is Promise{[colId: string]: CellValue[]}> but cannot be specified because // ts-interface-builder does not properly support index-signature. fetchSelectedTable(): Promise; + + // Similar TODO to fetchSelectedTable for return type. + fetchSelectedRecord(rowId: number): Promise; } diff --git a/app/plugin/GristData.ts b/app/plugin/GristData.ts new file mode 100644 index 00000000..c161ecb7 --- /dev/null +++ b/app/plugin/GristData.ts @@ -0,0 +1,6 @@ +export type CellValue = number|string|boolean|null|[string, any?]; + +export interface RowRecord { + id: number; + [colId: string]: CellValue; +} diff --git a/app/plugin/GristTable-ti.ts b/app/plugin/GristTable-ti.ts index e47f2103..237d94dc 100644 --- a/app/plugin/GristTable-ti.ts +++ b/app/plugin/GristTable-ti.ts @@ -1,3 +1,6 @@ +/** + * This module was automatically generated by `ts-interface-builder` + */ import * as t from "ts-interface-checker"; // tslint:disable:object-literal-key-quotes @@ -16,9 +19,17 @@ export const GristColumn = t.iface([], { "type": "string", }); +export const APIType = t.enumtype({ + "ImportSourceAPI": 0, + "ImportProcessorAPI": 1, + "ParseOptionsAPI": 2, + "ParseFileAPI": 3, +}); + const exportedTypeSuite: t.ITypeSuite = { GristTable, GristTables, GristColumn, + APIType, }; export default exportedTypeSuite; diff --git a/app/plugin/grist-plugin-api.ts b/app/plugin/grist-plugin-api.ts index be168ca3..62d7f295 100644 --- a/app/plugin/grist-plugin-api.ts +++ b/app/plugin/grist-plugin-api.ts @@ -19,6 +19,7 @@ // tslint:disable:no-console import { GristAPI, GristDocAPI, GristView, RPC_GRISTAPI_INTERFACE } from './GristAPI'; +import { RowRecord } from './GristData'; import { ImportSource, ImportSourceAPI, InternalImportSourceAPI } from './InternalImportSourceAPI'; import { RenderOptions, RenderTarget } from './RenderOptions'; import { checkers } from './TypeCheckers'; @@ -45,6 +46,40 @@ export const docApi = { export const on = rpc.on.bind(rpc); +// For custom widgets, add a handler that will be called whenever the +// row with the cursor changes - either by switching to a different row, or +// by some value within the row potentially changing. Handler may +// in the future be called with null if the cursor moves away from +// any row. +// TODO: currently this will be called even if the content of a different row +// changes. +export function onRecord(callback: (data: RowRecord | null) => unknown) { + on('message', async function(msg) { + if (!msg.tableId || !msg.rowId) { return; } + const rec = await docApi.fetchSelectedRecord(msg.rowId); + callback(rec); + }); +} + +// For custom widgets, add a handler that will be called whenever the +// selected records change. Handler will be called with a list of records. +export function onRecords(callback: (data: RowRecord[]) => unknown) { + on('message', async function(msg) { + if (!msg.tableId || !msg.dataChange) { return; } + const data = await docApi.fetchSelectedTable(); + if (!data.id) { return; } + const rows: RowRecord[] = []; + for (let i = 0; i < data.id.length; i++) { + const row: RowRecord = {id: data.id[i]}; + for (const key of Object.keys(data)) { + row[key] = data[key][i]; + } + rows.push(row); + } + callback(rows); + }); +} + /** * Calling `addImporter(...)` adds a safeBrowser importer. It is a short-hand for forwarding calls * to an `ImportSourceAPI` implementation registered in the file at `path`. It takes care of