(core) configure typedoc for generating plugin api documentation

Summary:
This annotates the plugin api sufficiently to generate some documentation
for it. See https://github.com/gristlabs/grist-help/pull/139

Contains some small code tweaks for things that caused typedoc some
trouble.

Test Plan: manual inspection of output

Reviewers: jarek

Reviewed By: jarek

Differential Revision: https://phab.getgrist.com/D3342
pull/178/head
Paul Fitzpatrick 2 years ago
parent d8af25de9d
commit c6d66e15bf

@ -69,40 +69,58 @@ export interface GristAPI {
} }
/** /**
* GristDocAPI interface is implemented by Grist, and allows getting information from and * Allows getting information from and nteracting with the Grist document to which a plugin or widget is attached.
* interacting with the Grist document to which a plugin is attached.
*/ */
export interface GristDocAPI { export interface GristDocAPI {
// Returns the docName that identifies the document. /**
* Returns an identifier for the document.
*/
getDocName(): Promise<string>; getDocName(): Promise<string>;
// Returns a sorted list of table IDs. /**
* Returns a sorted list of table IDs.
*/
listTables(): Promise<string[]>; listTables(): Promise<string[]>;
// Returns a complete table of data in the format {colId: [values]}, including the 'id' column. /**
// Do not modify the returned arrays in-place, especially if used directly (not over RPC). * Returns a complete table of data in the format {colId: [values]}, including the 'id' column.
// TODO: return type is Promise{[colId: string]: CellValue[]}> but cannot be specified because * Do not modify the returned arrays in-place, especially if used directly (not over RPC).
// ts-interface-builder does not properly support index-signature. * TODO: return type is Promise{[colId: string]: CellValue[]}> but cannot be specified because
* ts-interface-builder does not properly support index-signature.
*/
fetchTable(tableId: string): Promise<any>; fetchTable(tableId: string): Promise<any>;
// Applies an array of user actions. /**
// todo: return type should be Promise<ApplyUAResult>, but this requires importing modules from * Applies an array of user actions.
// `app/common` which is not currently supported by the build. * TODO: return type should be Promise<ApplyUAResult>, but this requires importing modules from
* `app/common` which is not currently supported by the build.
*/
applyUserActions(actions: any[][], options?: any): Promise<any>; applyUserActions(actions: any[][], options?: any): Promise<any>;
} }
/**
* Interface for the data backing a single widget.
*/
export interface GristView { export interface GristView {
// Like fetchTable, but gets data for the custom section specifically, if there is any. /**
// TODO: return type is Promise{[colId: string]: CellValue[]}> but cannot be specified because * Like [[GristDocAPI.fetchTable]], but gets data for the custom section specifically, if there is any.
// ts-interface-builder does not properly support index-signature. * TODO: return type is Promise{[colId: string]: CellValue[]}> but cannot be specified because
* ts-interface-builder does not properly support index-signature.
*/
fetchSelectedTable(): Promise<any>; fetchSelectedTable(): Promise<any>;
// Similar TODO to fetchSelectedTable for return type. /**
* Similar TODO to `fetchSelectedTable()` for return type.
*/
fetchSelectedRecord(rowId: number): Promise<any>; fetchSelectedRecord(rowId: number): Promise<any>;
// Allow custom widget to be listed as a possible source for linking with SELECT BY. /**
* Allow custom widget to be listed as a possible source for linking with SELECT BY.
*/
allowSelectBy(): Promise<void>; allowSelectBy(): Promise<void>;
// Set the list of selected rows to be used against any linked widget. Requires `allowSelectBy()`. /**
* Set the list of selected rows to be used against any linked widget. Requires `allowSelectBy()`.
*/
setSelectedRows(rowIds: number[]): Promise<void>; setSelectedRows(rowIds: number[]): Promise<void>;
} }

@ -0,0 +1 @@
Methods here are available for use in Grist custom widgets.

@ -1,5 +1,7 @@
/** /**
* Where to append the content that a plugin renders. * Where to append the content that a plugin renders.
*
* @internal
*/ */
export type RenderTarget = "fullscreen" | number; export type RenderTarget = "fullscreen" | number;

@ -4,22 +4,32 @@ import * as Types from 'app/plugin/DocApiTypes';
* Offer CRUD-style operations on a table. * Offer CRUD-style operations on a table.
*/ */
export interface TableOperations { export interface TableOperations {
// Create a record or records. /**
* Create a record or records.
*/
create(records: Types.NewRecord, options?: OpOptions): Promise<Types.MinimalRecord>; create(records: Types.NewRecord, options?: OpOptions): Promise<Types.MinimalRecord>;
create(records: Types.NewRecord[], options?: OpOptions): Promise<Types.MinimalRecord[]>; create(records: Types.NewRecord[], options?: OpOptions): Promise<Types.MinimalRecord[]>;
// Update a record or records. /**
* Update a record or records.
*/
update(records: Types.Record|Types.Record[], options?: OpOptions): Promise<void>; update(records: Types.Record|Types.Record[], options?: OpOptions): Promise<void>;
// Delete a record or records. /**
* Delete a record or records.
*/
destroy(recordId: Types.RecordId): Promise<Types.RecordId>; destroy(recordId: Types.RecordId): Promise<Types.RecordId>;
destroy(recordIds: Types.RecordId[]): Promise<Types.RecordId[]>; destroy(recordIds: Types.RecordId[]): Promise<Types.RecordId[]>;
// Add or update a record or records. /**
* Add or update a record or records.
*/
upsert(records: Types.AddOrUpdateRecord|Types.AddOrUpdateRecord[], upsert(records: Types.AddOrUpdateRecord|Types.AddOrUpdateRecord[],
options?: UpsertOptions): Promise<void>; options?: UpsertOptions): Promise<void>;
// Determine the tableId of the table. /**
* Determine the tableId of the table.
*/
getTableId(): Promise<string>; getTableId(): Promise<string>;
// TODO: offer a way to query the table. // TODO: offer a way to query the table.
@ -32,7 +42,7 @@ export interface TableOperations {
* This can be disabled. * This can be disabled.
*/ */
export interface OpOptions { export interface OpOptions {
parseStrings?: boolean; parseStrings?: boolean; /** whether to parse strings based on the column type. */
} }
/** /**
@ -40,8 +50,8 @@ export interface OpOptions {
* onMany is first, and allowEmptyRequire is false. * onMany is first, and allowEmptyRequire is false.
*/ */
export interface UpsertOptions extends OpOptions { export interface UpsertOptions extends OpOptions {
add?: boolean; // permit inserting a record add?: boolean; /** permit inserting a record */
update?: boolean; // permit updating a record update?: boolean; /** permit updating a record */
onMany?: 'none' | 'first' | 'all'; // whether to update none, one, or all matching records onMany?: 'none' | 'first' | 'all'; /** whether to update none, one, or all matching records */
allowEmptyRequire?: boolean; // allow "wildcard" operation allowEmptyRequire?: boolean; /** allow "wildcard" operation */
} }

@ -46,46 +46,103 @@ export const rpc: Rpc = new Rpc({logger: createRpcLogger()});
export const api = rpc.getStub<GristAPI>(RPC_GRISTAPI_INTERFACE, checkers.GristAPI); export const api = rpc.getStub<GristAPI>(RPC_GRISTAPI_INTERFACE, checkers.GristAPI);
export const coreDocApi = rpc.getStub<GristDocAPI>('GristDocAPI@grist', checkers.GristDocAPI); export const coreDocApi = rpc.getStub<GristDocAPI>('GristDocAPI@grist', checkers.GristDocAPI);
/**
* Interface for the records backing a custom widget.
*/
export const viewApi = rpc.getStub<GristView>('GristView', checkers.GristView); export const viewApi = rpc.getStub<GristView>('GristView', checkers.GristView);
/**
* Interface for the state of a custom widget.
*/
export const widgetApi = rpc.getStub<WidgetAPI>('WidgetAPI', checkers.WidgetAPI); export const widgetApi = rpc.getStub<WidgetAPI>('WidgetAPI', checkers.WidgetAPI);
/**
* Interface for the mapping of a custom widget.
*/
export const sectionApi = rpc.getStub<CustomSectionAPI>('CustomSectionAPI', checkers.CustomSectionAPI); export const sectionApi = rpc.getStub<CustomSectionAPI>('CustomSectionAPI', checkers.CustomSectionAPI);
/**
* Shortcut for [[GristView.allowSelectBy]].
*/
export const allowSelectBy = viewApi.allowSelectBy; export const allowSelectBy = viewApi.allowSelectBy;
/**
* Shortcut for [[GristView.setSelectedRows]].
*/
export const setSelectedRows = viewApi.setSelectedRows; export const setSelectedRows = viewApi.setSelectedRows;
/**
* Fetches data backing the widget as for [[GristView.fetchSelectedTable]],
* but decoding data by default, replacing e.g. ['D', timestamp] with
* a moment date. Option `keepEncoded` skips the decoding step.
*/
export async function fetchSelectedTable(options: {keepEncoded?: boolean} = {}) {
const table = await viewApi.fetchSelectedTable();
return options.keepEncoded ? table :
mapValues<any[], any[]>(table, (col) => col.map(decodeObject));
}
/**
* Fetches current selected record as for [[GristView.fetchSelectedRecord]],
* but decoding data by default, replacing e.g. ['D', timestamp] with
* a moment date. Option `keepEncoded` skips the decoding step.
*/
export async function fetchSelectedRecord(rowId: number, options: {keepEncoded?: boolean} = {}) {
const rec = await viewApi.fetchSelectedRecord(rowId);
return options.keepEncoded ? rec :
mapValues(rec, decodeObject);
}
/**
* A collection of methods for fetching document data. The
* fetchSelectedTable and fetchSelectedRecord methods are
* overridden to decode data by default.
*/
export const docApi: GristDocAPI & GristView = { export const docApi: GristDocAPI & GristView = {
...coreDocApi, ...coreDocApi,
...viewApi, ...viewApi,
fetchSelectedTable,
// Change fetchSelectedTable() to decode data by default, replacing e.g. ['D', timestamp] with fetchSelectedRecord,
// a moment date. New option `keepEncoded` skips the decoding step.
async fetchSelectedTable(options: {keepEncoded?: boolean} = {}) {
const table = await viewApi.fetchSelectedTable();
return options.keepEncoded ? table :
mapValues<any[], any[]>(table, (col) => col.map(decodeObject));
},
// Change fetchSelectedRecord() to decode data by default, replacing e.g. ['D', timestamp] with
// a moment date. New option `keepEncoded` skips the decoding step.
async fetchSelectedRecord(rowId: number, options: {keepEncoded?: boolean} = {}) {
const rec = await viewApi.fetchSelectedRecord(rowId);
return options.keepEncoded ? rec :
mapValues(rec, decodeObject);
},
}; };
export const on = rpc.on.bind(rpc); export const on = rpc.on.bind(rpc);
// Exposing widgetApi methods in a module scope. // Exposing widgetApi methods in a module scope.
/**
* Shortcut for [[WidgetAPI.getOption]]
*/
export const getOption = widgetApi.getOption.bind(widgetApi); export const getOption = widgetApi.getOption.bind(widgetApi);
/**
* Shortcut for [[WidgetAPI.setOption]]
*/
export const setOption = widgetApi.setOption.bind(widgetApi); export const setOption = widgetApi.setOption.bind(widgetApi);
/**
* Shortcut for [[WidgetAPI.setOptions]]
*/
export const setOptions = widgetApi.setOptions.bind(widgetApi); export const setOptions = widgetApi.setOptions.bind(widgetApi);
/**
* Shortcut for [[WidgetAPI.getOptions]]
*/
export const getOptions = widgetApi.getOptions.bind(widgetApi); export const getOptions = widgetApi.getOptions.bind(widgetApi);
/**
* Shortcut for [[WidgetAPI.clearOptions]]
*/
export const clearOptions = widgetApi.clearOptions.bind(widgetApi); export const clearOptions = widgetApi.clearOptions.bind(widgetApi);
// Get access to a table in the document. If no tableId specified, this /**
// will use the current selected table (for custom widgets). * Get access to a table in the document. If no tableId specified, this
// If a table does not exist, there will be no error until an operation * will use the current selected table (for custom widgets).
// on the table is attempted. * If a table does not exist, there will be no error until an operation
* on the table is attempted.
*/
export function getTable(tableId?: string): TableOperations { export function getTable(tableId?: string): TableOperations {
return new TableOperationsImpl({ return new TableOperationsImpl({
async getTableId() { async getTableId() {
@ -100,7 +157,9 @@ export function getTable(tableId?: string): TableOperations {
}, {}); }, {});
} }
// Get the current selected table (for custom widgets). /**
* Get the current selected table (for custom widgets).
*/
export const selectedTable: TableOperations = getTable(); export const selectedTable: TableOperations = getTable();
// Get the ID of the current selected table (for custom widgets). // Get the ID of the current selected table (for custom widgets).
@ -240,13 +299,15 @@ export function mapColumnNamesBack(data: any, options: {
return mapColumnNames(data, {...options, reverse: true}); return mapColumnNames(data, {...options, reverse: true});
} }
// 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 * For custom widgets, add a handler that will be called whenever the
// by some value within the row potentially changing. Handler may * row with the cursor changes - either by switching to a different row, or
// in the future be called with null if the cursor moves away from * by some value within the row potentially changing. Handler may
// any row. * in the future be called with null if the cursor moves away from
// TODO: currently this will be called even if the content of a different row * any row.
// changes. * TODO: currently this will be called even if the content of a different row
* changes.
*/
export function onRecord(callback: (data: RowRecord | null, mappings: WidgetColumnMap | null) => unknown) { export function onRecord(callback: (data: RowRecord | null, mappings: WidgetColumnMap | null) => unknown) {
on('message', async function(msg) { on('message', async function(msg) {
if (!msg.tableId || !msg.rowId) { return; } if (!msg.tableId || !msg.rowId) { return; }
@ -254,8 +315,11 @@ export function onRecord(callback: (data: RowRecord | null, mappings: WidgetColu
callback(rec, await getMappingsIfChanged(msg)); callback(rec, await getMappingsIfChanged(msg));
}); });
} }
// For custom widgets, add a handler that will be called whenever the
// selected records change. Handler will be called with a list of records. /**
* 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[], mappings: WidgetColumnMap | null) => unknown) { export function onRecords(callback: (data: RowRecord[], mappings: WidgetColumnMap | null) => unknown) {
on('message', async function(msg) { on('message', async function(msg) {
if (!msg.tableId || !msg.dataChange) { return; } if (!msg.tableId || !msg.dataChange) { return; }
@ -274,10 +338,13 @@ export function onRecords(callback: (data: RowRecord[], mappings: WidgetColumnMa
} }
// For custom widgets, add a handler that will be called whenever the /**
// widget options change (and on initial ready message). Handler will be * For custom widgets, add a handler that will be called whenever the
// called with an object containing save json options, or null if no options were saved. * widget options change (and on initial ready message). Handler will be
// Second parameter * called with an object containing saved json options, or null if no options were saved.
* The second parameter has information about the widgets relationship with
* the document that contains it.
*/
export function onOptions(callback: (options: any, settings: InteractionOptions) => unknown) { export function onOptions(callback: (options: any, settings: InteractionOptions) => unknown) {
on('message', async function(msg) { on('message', async function(msg) {
if (msg.settings) { if (msg.settings) {
@ -297,6 +364,7 @@ export function onOptions(callback: (options: any, settings: InteractionOptions)
* `name`. Calling `addImporter(...)` from another component than a `safeBrowser` component is not * `name`. Calling `addImporter(...)` from another component than a `safeBrowser` component is not
* currently supported. * currently supported.
* *
* @internal
*/ */
export async function addImporter(name: string, path: string, mode: 'fullscreen' | 'inline', options?: RenderOptions) { export async function addImporter(name: string, path: string, mode: 'fullscreen' | 'inline', options?: RenderOptions) {
// checker is omitted for implementation because call was already checked by grist. // checker is omitted for implementation because call was already checked by grist.
@ -315,7 +383,10 @@ export async function addImporter(name: string, path: string, mode: 'fullscreen'
}); });
} }
interface ReadyPayload extends Omit<InteractionOptionsRequest, "hasCustomOptions"> { /**
* Options when initializing connection to Grist.
*/
export interface ReadyPayload extends Omit<InteractionOptionsRequest, "hasCustomOptions"> {
/** /**
* Handler that will be called by Grist to open additional configuration panel inside the Custom Widget. * Handler that will be called by Grist to open additional configuration panel inside the Custom Widget.
*/ */
@ -354,6 +425,7 @@ export function ready(settings?: ReadyPayload): void {
})(); })();
} }
/** @internal */
function getPluginPath(location: Location) { function getPluginPath(location: Location) {
return location.pathname.replace(/^\/plugins\//, ''); return location.pathname.replace(/^\/plugins\//, '');
} }
@ -393,6 +465,7 @@ if (typeof window !== 'undefined') {
rpc.setSendMessage((data) => { return; }); rpc.setSendMessage((data) => { return; });
} }
/** @internal */
function createRpcLogger(): IRpcLogger { function createRpcLogger(): IRpcLogger {
let prefix: string; let prefix: string;
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {

@ -90,7 +90,7 @@ export class TestServerMerged implements IMochaServer {
// logging. Server code uses a global logger, so it's hard to separate out (especially so if // logging. Server code uses a global logger, so it's hard to separate out (especially so if
// we ever run different servers for different tests). // we ever run different servers for different tests).
const serverLog = process.env.VERBOSE ? 'inherit' : nodeLogFd; const serverLog = process.env.VERBOSE ? 'inherit' : nodeLogFd;
const env = { const env: Record<string, string> = {
TYPEORM_DATABASE: this._getDatabaseFile(), TYPEORM_DATABASE: this._getDatabaseFile(),
TEST_CLEAN_DATABASE: reset ? 'true' : '', TEST_CLEAN_DATABASE: reset ? 'true' : '',
GRIST_DATA_DIR: this.testDocDir, GRIST_DATA_DIR: this.testDocDir,

@ -70,7 +70,7 @@ export class GristClient {
if (this._pending.length) { if (this._pending.length) {
return this._pending.shift(); return this._pending.shift();
} }
await new Promise(resolve => this._consumer = resolve); await new Promise<void>(resolve => this._consumer = resolve);
} }
} }

@ -6,5 +6,14 @@
{ "path": "./app" }, { "path": "./app" },
{ "path": "./stubs/app" }, { "path": "./stubs/app" },
{ "path": "./test" }, { "path": "./test" },
] ],
"typedocOptions": {
"entryPoints": [
"app/plugin/grist-plugin-api.ts",
"app/plugin/TableOperations.ts",
],
"excludeInternal": "true",
"excludeNotDocumented": "true",
"out": "doc"
}
} }

Loading…
Cancel
Save