gristlabs_grist-core/app/common/CustomWidget.ts
Paul Fitzpatrick cc9a9ae8c5 (core) support for bundling custom widgets with the Grist app
Summary:
This adds support for bundling custom widgets with the Grist app, as follows:

 * Adds a new `widgets` component to plugins mechanism.
 * When a set of widgets is provided in a plugin, the html/js/css assets for those widgets are served on the existing untrusted user content port.
 * Any bundled `grist-plugin-api.js` will be served with the Grist app's own version of that file. It is important that bundled widgets not refer to https://docs.getgrist.com for the plugin js, since they must be capable of working offline.
 * The logic for configuring that port is updated a bit.
 * I removed the CustomAttachedView class in favor of applying settings of bundled custom widgets more directly, without modification on view.

Any Grist installation via docker will need an extra step now, since there is an extra port that needs exposing for full functionality. I did add a `GRIST_TRUST_PLUGINS` option for anyone who really doesn't want to do this, and would prefer to trust the plugins and have them served on the same port.

Actually making use of bundling will be another step. It'll be important to mesh it with our SaaS's use of APP_STATIC_URL for serving most static assets.

Design sketch: https://grist.quip.com/bJlWACWzr2R9/Bundled-custom-widgets

Test Plan: added a test

Reviewers: georgegevoian

Reviewed By: georgegevoian

Differential Revision: https://phab.getgrist.com/D4069
2023-10-27 17:00:10 -04:00

90 lines
2.2 KiB
TypeScript

import sortBy = require('lodash/sortBy');
/**
* Custom widget manifest definition.
*/
export interface ICustomWidget {
/**
* Widget friendly name, used on the UI.
*/
name: string;
/**
* Widget unique id, probably in npm package format @gristlabs/custom-widget-name.
*
* There could be multiple versions of the same widget with the
* same id, e.g. a bundled version and an external version.
*/
widgetId: string;
/**
* Custom widget main page URL.
*/
url: string;
/**
* Optional desired access level.
*/
accessLevel?: AccessLevel;
/**
* If set, Grist will render the widget after `grist.ready()`.
*
* This is used to defer showing a widget on initial load until it has finished
* applying the Grist theme.
*/
renderAfterReady?: boolean;
/**
* If the widget came from a plugin, we track that here.
*/
source?: {
pluginId: string;
name: string;
};
}
/**
* Widget access level.
*/
export enum AccessLevel {
/**
* Default, no access to Grist.
*/
none = "none",
/**
* Read only access to table the widget is based on.
*/
read_table = "read table",
/**
* Full access to document on user's behalf.
*/
full = "full",
}
export function isSatisfied(current: AccessLevel, minimum: AccessLevel) {
function ordered(level: AccessLevel) {
switch(level) {
case AccessLevel.none: return 0;
case AccessLevel.read_table: return 1;
case AccessLevel.full: return 2;
default: throw new Error(`Unrecognized access level ${level}`);
}
}
return ordered(current) >= ordered(minimum);
}
/**
* Find the best match for a widgetId/pluginId combination among the
* given widgets. An exact widgetId match is required. A pluginId match
* is preferred but not required.
*/
export function matchWidget(widgets: ICustomWidget[], options: {
widgetId: string,
pluginId?: string,
}): ICustomWidget|undefined {
const prefs = sortBy(widgets, (w) => {
return [w.widgetId !== options.widgetId,
(w.source?.pluginId||'') !== options.pluginId];
});
if (prefs.length === 0) { return; }
if (prefs[0].widgetId !== options.widgetId) { return; }
return prefs[0];
}