mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Widget options api
Summary: Adding configuration options for CustomWidgets. Custom widgets can now store options (in JSON) in viewSection metadata. Changes in grist-plugin-api: - Adding onOptions handler, that will be invoked when the widget is ready and when the configuration is changed - Adding WidgetAPI - new API to read and save a configuration for widget. Changes in Grist: - Rewriting CustomView code, and extracting code that is responsible for showing the iframe and registering Rpc. - Adding Open Configuration button to Widget section in the Creator panel and in the section menu. - Custom Widgets can implement "configure" method, to show configuration screen when requested. Test Plan: Browser tests. Reviewers: paulfitz, dsagal Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D3185
This commit is contained in:
@@ -4,11 +4,22 @@
|
||||
import * as t from "ts-interface-checker";
|
||||
// tslint:disable:object-literal-key-quotes
|
||||
|
||||
export const RequestedInteractionOptions = t.iface([], {
|
||||
"requiredAccess": t.opt("string"),
|
||||
"hasCustomOptions": t.opt("boolean"),
|
||||
});
|
||||
|
||||
export const InteractionOptions = t.iface([], {
|
||||
"accessLevel": "string",
|
||||
});
|
||||
|
||||
export const CustomSectionAPI = t.iface([], {
|
||||
"createSection": t.func("void", t.param("inlineTarget", "RenderTarget")),
|
||||
"configure": t.func("void", t.param("customOptions", "RequestedInteractionOptions")),
|
||||
});
|
||||
|
||||
const exportedTypeSuite: t.ITypeSuite = {
|
||||
RequestedInteractionOptions,
|
||||
InteractionOptions,
|
||||
CustomSectionAPI,
|
||||
};
|
||||
export default exportedTypeSuite;
|
||||
|
||||
@@ -2,9 +2,32 @@
|
||||
* API definitions for CustomSection plugins.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Initial message sent by the CustomWidget with initial requirements.
|
||||
*/
|
||||
export interface InteractionOptionsRequest {
|
||||
/**
|
||||
* Required access level. If it wasn't granted already, Grist will prompt user to change the current access
|
||||
* level.
|
||||
*/
|
||||
requiredAccess?: string,
|
||||
/**
|
||||
* Instructs Grist to show additional menu options that will trigger onEditOptions callback, that Widget
|
||||
* can use to show custom options screen.
|
||||
*/
|
||||
hasCustomOptions?: boolean,
|
||||
}
|
||||
|
||||
import {RenderTarget} from './RenderOptions';
|
||||
/**
|
||||
* Widget configuration set and approved by Grist, sent as part of ready message.
|
||||
*/
|
||||
export interface InteractionOptions {
|
||||
/**
|
||||
* Granted access level.
|
||||
*/
|
||||
accessLevel: string
|
||||
}
|
||||
|
||||
export interface CustomSectionAPI {
|
||||
createSection(inlineTarget: RenderTarget): Promise<void>;
|
||||
configure(customOptions: InteractionOptionsRequest): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -7,17 +7,18 @@ import ImportSourceAPITI from './ImportSourceAPI-ti';
|
||||
import InternalImportSourceAPITI from './InternalImportSourceAPI-ti';
|
||||
import RenderOptionsTI from './RenderOptions-ti';
|
||||
import StorageAPITI from './StorageAPI-ti';
|
||||
import WidgetAPITI from './WidgetAPI-ti';
|
||||
|
||||
/**
|
||||
* The ts-interface-checker type suites are all exported with the "TI" suffix.
|
||||
*/
|
||||
export {
|
||||
CustomSectionAPITI, FileParserAPITI, GristAPITI, GristTableTI, ImportSourceAPITI,
|
||||
InternalImportSourceAPITI, RenderOptionsTI, StorageAPITI};
|
||||
InternalImportSourceAPITI, RenderOptionsTI, StorageAPITI, WidgetAPITI};
|
||||
|
||||
const allTypes = [
|
||||
CustomSectionAPITI, FileParserAPITI, GristAPITI, GristTableTI, ImportSourceAPITI,
|
||||
InternalImportSourceAPITI, RenderOptionsTI, StorageAPITI];
|
||||
InternalImportSourceAPITI, RenderOptionsTI, StorageAPITI, WidgetAPITI];
|
||||
|
||||
function checkDuplicates(types: Array<{[key: string]: object}>) {
|
||||
const seen = new Set<string>();
|
||||
@@ -46,5 +47,5 @@ export const checkers = createCheckers(...allTypes) as (
|
||||
'FileSource' | 'ParseFileResult' | 'ComponentKind' | 'GristAPI' | 'GristDocAPI' | 'GristTable' |
|
||||
'GristTables' | 'GristColumn' | 'GristView' | 'ImportSourceAPI' | 'ImportProcessorAPI' | 'FileContent' |
|
||||
'FileListItem' | 'URL' | 'ImportSource' | 'InternalImportSourceAPI' | 'RenderTarget' |
|
||||
'RenderOptions' | 'Storage'
|
||||
'RenderOptions' | 'Storage' | 'WidgetAPI'
|
||||
>);
|
||||
|
||||
20
app/plugin/WidgetAPI-ti.ts
Normal file
20
app/plugin/WidgetAPI-ti.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* This module was automatically generated by `ts-interface-builder`
|
||||
*/
|
||||
import * as t from "ts-interface-checker";
|
||||
// tslint:disable:object-literal-key-quotes
|
||||
|
||||
export const WidgetAPI = t.iface([], {
|
||||
"getOptions": t.func(t.union("object", "null")),
|
||||
"setOptions": t.func("void", t.param("options", t.iface([], {
|
||||
[t.indexKey]: "any",
|
||||
}))),
|
||||
"clearOptions": t.func("void"),
|
||||
"setOption": t.func("void", t.param("key", "string"), t.param("value", "any")),
|
||||
"getOption": t.func("any", t.param("key", "string")),
|
||||
});
|
||||
|
||||
const exportedTypeSuite: t.ITypeSuite = {
|
||||
WidgetAPI,
|
||||
};
|
||||
export default exportedTypeSuite;
|
||||
25
app/plugin/WidgetAPI.ts
Normal file
25
app/plugin/WidgetAPI.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* API to manage Custom Widget state.
|
||||
*/
|
||||
export interface WidgetAPI {
|
||||
/**
|
||||
* Gets all options stored by the widget. Options are stored as plain JSON object.
|
||||
*/
|
||||
getOptions(): Promise<object | null>;
|
||||
/**
|
||||
* Replaces all options stored by the widget.
|
||||
*/
|
||||
setOptions(options: {[key: string]: any}): Promise<void>;
|
||||
/**
|
||||
* Clears all the options.
|
||||
*/
|
||||
clearOptions(): Promise<void>;
|
||||
/**
|
||||
* Store single value in the Widget options object (and create it if necessary).
|
||||
*/
|
||||
setOption(key: string, value: any): Promise<void>;
|
||||
/**
|
||||
* Get single value from Widget options object.
|
||||
*/
|
||||
getOption(key: string): Promise<any>;
|
||||
}
|
||||
@@ -18,12 +18,14 @@
|
||||
|
||||
// tslint:disable:no-console
|
||||
|
||||
import { CustomSectionAPI, InteractionOptions } from './CustomSectionAPI';
|
||||
import { GristAPI, GristDocAPI, GristView, RPC_GRISTAPI_INTERFACE } from './GristAPI';
|
||||
import { RowRecord } from './GristData';
|
||||
import { ImportSource, ImportSourceAPI, InternalImportSourceAPI } from './InternalImportSourceAPI';
|
||||
import { decodeObject, mapValues } from './objtypes';
|
||||
import { RenderOptions, RenderTarget } from './RenderOptions';
|
||||
import { checkers } from './TypeCheckers';
|
||||
import { WidgetAPI } from './WidgetAPI';
|
||||
|
||||
export * from './TypeCheckers';
|
||||
export * from './FileParserAPI';
|
||||
@@ -32,6 +34,8 @@ export * from './GristTable';
|
||||
export * from './ImportSourceAPI';
|
||||
export * from './StorageAPI';
|
||||
export * from './RenderOptions';
|
||||
export * from './WidgetAPI';
|
||||
export * from './CustomSectionAPI';
|
||||
|
||||
import {IRpcLogger, Rpc} from 'grain-rpc';
|
||||
|
||||
@@ -40,6 +44,8 @@ export const rpc: Rpc = new Rpc({logger: createRpcLogger()});
|
||||
export const api = rpc.getStub<GristAPI>(RPC_GRISTAPI_INTERFACE, checkers.GristAPI);
|
||||
export const coreDocApi = rpc.getStub<GristDocAPI>('GristDocAPI@grist', checkers.GristDocAPI);
|
||||
export const viewApi = rpc.getStub<GristView>('GristView', checkers.GristView);
|
||||
export const widgetApi = rpc.getStub<WidgetAPI>('WidgetAPI', checkers.WidgetAPI);
|
||||
export const sectionApi = rpc.getStub<CustomSectionAPI>('CustomSectionAPI', checkers.CustomSectionAPI);
|
||||
|
||||
export const docApi: GristDocAPI & GristView = {
|
||||
...coreDocApi,
|
||||
@@ -98,11 +104,24 @@ export function onRecords(callback: (data: RowRecord[]) => unknown) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// For custom widgets, add a handler that will be called whenever the
|
||||
// widget options change (and on initial ready message). Handler will be
|
||||
// called with an object containing save json options, or null if no options were saved.
|
||||
// Second parameter
|
||||
export function onOptions(callback: (options: any, settings: InteractionOptions) => unknown) {
|
||||
on('message', async function(msg) {
|
||||
if (msg.settings) {
|
||||
callback(msg.options || null, msg.settings);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* creating the stub, registering an implementation that renders the file, forward the call and
|
||||
* dispose the view properly. If `mode` is `'inline'` embeds the view in the import modal, ohterwise
|
||||
* dispose the view properly. If `mode` is `'inline'` embeds the view in the import modal, otherwise
|
||||
* renders fullscreen.
|
||||
*
|
||||
* Notes: it assumes that file at `path` registers an `ImportSourceAPI` implementation under
|
||||
@@ -111,7 +130,7 @@ export function onRecords(callback: (data: RowRecord[]) => unknown) {
|
||||
*
|
||||
*/
|
||||
export async function addImporter(name: string, path: string, mode: 'fullscreen' | 'inline', options?: RenderOptions) {
|
||||
// checker is omitterd for implementation because call was alredy checked by grist.
|
||||
// checker is omitted for implementation because call was alredy checked by grist.
|
||||
rpc.registerImpl<InternalImportSourceAPI>(name, {
|
||||
async getImportSource(target: RenderTarget): Promise<ImportSource|undefined> {
|
||||
const procId = await api.render(path, mode === 'inline' ? target : 'fullscreen', options);
|
||||
@@ -131,9 +150,23 @@ export async function addImporter(name: string, path: string, mode: 'fullscreen'
|
||||
* Declare that a component is prepared to receive messages from the outside world.
|
||||
* Grist will not attempt to communicate with it until this method is called.
|
||||
*/
|
||||
export function ready(): void {
|
||||
export function ready(settings?: {
|
||||
requiredAccess?: string,
|
||||
onEditOptions: () => unknown
|
||||
}): void {
|
||||
if (settings && settings.onEditOptions) {
|
||||
rpc.registerFunc('editOptions', settings.onEditOptions);
|
||||
}
|
||||
rpc.processIncoming();
|
||||
void rpc.sendReadyMessage();
|
||||
void (async function() {
|
||||
await rpc.sendReadyMessage();
|
||||
if (settings) {
|
||||
await sectionApi.configure({
|
||||
requiredAccess : settings.requiredAccess,
|
||||
hasCustomOptions: Boolean(settings.onEditOptions)
|
||||
}).catch((err: unknown) => console.error(err));
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
function getPluginPath(location: Location) {
|
||||
|
||||
Reference in New Issue
Block a user