allow bundled widgets to be hidden from dropdown, and nested (#714)

This makes a few refinements to bundling widgets:

  * A widget with `published: false` is not shown in the
    custom widget dropdown in the UI. This is so widgets
    can be bundled with the app for "native" use (like the
    calendar widget) without immediately resulting in an
    extra listing in the UI. (There are improvements we'd
    like to make to the UI to better communicate widget
    provenance and quality eventually, which would be a
    helpful alternative to just a binary flag.)

  * A relative path to the custom widget manifest is
    respected. This will make the bundling process marginally
    neater.
pull/716/head
Paul Fitzpatrick 7 months ago committed by GitHub
parent 7c4d9e2caf
commit 07bb90b5a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -551,10 +551,12 @@ export class CustomSectionConfig extends Disposable {
// Options for the select-box (all widgets definitions and Custom URL) // Options for the select-box (all widgets definitions and Custom URL)
const options = Computed.create(holder, use => [ const options = Computed.create(holder, use => [
{label: 'Custom URL', value: 'custom'}, {label: 'Custom URL', value: 'custom'},
...(use(this._widgets) || []).map(w => ({ ...(use(this._widgets) || [])
label: w.source?.name ? `${w.name} (${w.source.name})` : w.name, .filter(w => w?.published !== false)
value: (w.source?.pluginId || '') + ':' + w.widgetId, .map(w => ({
})), label: w.source?.name ? `${w.name} (${w.source.name})` : w.name,
value: (w.source?.pluginId || '') + ':' + w.widgetId,
})),
]); ]);
function buildPrompt(level: AccessLevel|null) { function buildPrompt(level: AccessLevel|null) {
if (!level) { if (!level) {

@ -31,6 +31,11 @@ export interface ICustomWidget {
*/ */
renderAfterReady?: boolean; renderAfterReady?: boolean;
/**
* If set to false, do not offer to user in UI.
*/
published?: boolean;
/** /**
* If the widget came from a plugin, we track that here. * If the widget came from a plugin, we track that here.
*/ */

@ -267,7 +267,7 @@ export function getWidgetsInPlugins(gristServer: GristServer,
gristServer.getTag() + '/widgets/' + plugin.id + '/'; gristServer.getTag() + '/widgets/' + plugin.id + '/';
places.push({ places.push({
urlBase, urlBase,
dir: plugin.path, dir: path.resolve(plugin.path, path.dirname(components.widgets)),
file: path.join(plugin.path, components.widgets), file: path.join(plugin.path, components.widgets),
name: plugin.manifest.name || plugin.id, name: plugin.manifest.name || plugin.id,
pluginId: plugin.id, pluginId: plugin.id,

@ -734,146 +734,167 @@ describe('CustomWidgets', function () {
oldEnv = new EnvironmentSnapshot(); oldEnv = new EnvironmentSnapshot();
}); });
after(async function() { afterEach(async function() {
oldEnv.restore(); oldEnv.restore();
await server.restart(); await server.restart();
await gu.reloadDoc();
}); });
it('can add widgets via plugins', async function () { for (const variant of ['flat', 'nested'] as const) {
// Double-check that using one external widget, we see it(`can add widgets via plugins (${variant} layout)`, async function () {
// just that widget listed. // Double-check that using one external widget, we see
widgets = [widget1]; // just that widget listed.
await useManifest(manifestEndpoint); widgets = [widget1];
await enableWidgetsAndShowPanel(); await useManifest(manifestEndpoint);
await toggle(); await enableWidgetsAndShowPanel();
assert.deepEqual(await options(), [ await toggle();
CUSTOM_URL, widget1.name, assert.deepEqual(await options(), [
]); CUSTOM_URL, widget1.name,
]);
// Get a temporary directory that will be cleaned up,
// and populated it as follows: // Get a temporary directory that will be cleaned up,
// plugins/ // and populated it as follows ('flat' variant)
// my-widgets/ // plugins/
// manifest.yml # a plugin manifest, listing widgets.json // my-widgets/
// widgets.json # a widget set manifest, grist-widget style // manifest.yml # a plugin manifest, listing widgets.json
// p1.html # one of the widgets in widgets.json // widgets.json # a widget set manifest, grist-widget style
// p2.html # another of the widgets in widgets.json // p1.html # one of the widgets in widgets.json
// grist-plugin-api.js # a dummy api file, to check it is overridden // p2.html # another of the widgets in widgets.json
const dir = await createTmpDir(); // grist-plugin-api.js # a dummy api file, to check it is overridden
const pluginDir = path.join(dir, 'plugins', 'my-widgets'); // In 'nested' variant, widgets.json and the files it refers to are in
await fse.mkdirp(pluginDir); // a subdirectory.
const dir = await createTmpDir();
// A plugin, with some widgets in it. const pluginDir = path.join(dir, 'plugins', 'my-widgets');
await fse.writeFile(path.join(pluginDir, 'manifest.yml'), ` const widgetDir = variant === 'nested' ? path.join(pluginDir, 'nested') : pluginDir;
name: My Widgets await fse.mkdirp(pluginDir);
components: await fse.mkdirp(widgetDir);
widgets: widgets.json
`); // A plugin, with some widgets in it.
await fse.writeFile(
// A list of a pair of custom widgets, with the widget path.join(pluginDir, 'manifest.yml'),
// source in the same directory. `name: My Widgets\n` +
await fse.writeFile( `components:\n` +
path.join(pluginDir, 'widgets.json'), ` widgets: ${variant === 'nested' ? 'nested/' : ''}widgets.json\n`
JSON.stringify([ );
{
widgetId: 'p1',
name: 'P1',
url: './p1.html',
},
{
widgetId: 'p2',
name: 'P2',
url: './p2.html',
},
]),
);
// The first widget - just contains the text P1. // A list of a pair of custom widgets, with the widget
await fse.writeFile( // source in the same directory.
path.join(pluginDir, 'p1.html'), await fse.writeFile(
'<html><body>P1</body></html>', path.join(widgetDir, 'widgets.json'),
); JSON.stringify([
{
widgetId: 'p1',
name: 'P1',
url: './p1.html',
},
{
widgetId: 'p2',
name: 'P2',
url: './p2.html',
},
{
widgetId: 'p3',
name: 'P3',
url: './p3.html',
published: false,
},
]),
);
// The first widget - just contains the text P1.
await fse.writeFile(
path.join(widgetDir, 'p1.html'),
'<html><body>P1</body></html>',
);
// The second widget. This contains the text P2 // The second widget. This contains the text P2
// if grist is defined after loading grist-plugin-api.js // if grist is defined after loading grist-plugin-api.js
// (but the js bundled with the widget just throws an // (but the js bundled with the widget just throws an
// alert). // alert).
await fse.writeFile( await fse.writeFile(
path.join(pluginDir, 'p2.html'), path.join(widgetDir, 'p2.html'),
` `
<html> <html>
<script src="./grist-plugin-api.js"></script> <head><script src="./grist-plugin-api.js"></script></head>
<body> <body>
<div id="readout"></div> <div id="readout"></div>
<script> <script>
if (typeof grist !== 'undefined') { if (typeof grist !== 'undefined') {
document.getElementById('readout').innerText = 'P2'; document.getElementById('readout').innerText = 'P2';
} }
</script> </script>
</body> </body>
</html> </html>
` `
); );
// A dummy grist-plugin-api.js - hopefully the actual
// js for the current version of Grist will be served in
// its place.
await fse.writeFile(
path.join(pluginDir, 'grist-plugin-api.js'),
'alert("Error: built in api version used");',
);
// Restart server and reload doc now plugins are in place. // The third widget - just contains the text P3.
process.env.GRIST_USER_ROOT = dir; await fse.writeFile(
await server.restart(); path.join(widgetDir, 'p3.html'),
await gu.reloadDoc(); '<html><body>P3</body></html>',
);
// Continue using one external widget. // A dummy grist-plugin-api.js - hopefully the actual
await useManifest(manifestEndpoint); // js for the current version of Grist will be served in
await enableWidgetsAndShowPanel(); // its place.
await fse.writeFile(
path.join(widgetDir, 'grist-plugin-api.js'),
'alert("Error: built in api version used");',
);
// Check we see one external widget and two bundled ones. // Restart server and reload doc now plugins are in place.
await toggle(); process.env.GRIST_USER_ROOT = dir;
assert.deepEqual(await options(), [ await server.restart();
CUSTOM_URL, widget1.name, 'P1 (My Widgets)', 'P2 (My Widgets)', await gu.reloadDoc();
]);
// Prepare to check content of widgets.
async function getWidgetText(): Promise<string> {
return gu.doInIframe(await getCustomWidgetFrame(), () => {
return driver.executeScript(
() => document.body.innerText
);
});
}
// Check built-in P1 works as expected. // Continue using one external widget.
await select(/P1/); await useManifest(manifestEndpoint);
assert.equal(await current(), 'P1 (My Widgets)'); await enableWidgetsAndShowPanel();
await gu.waitToPass(async () => {
assert.equal(await getWidgetText(), 'P1');
});
// Check external W1 works as expected. // Check we see one external widget and two bundled ones.
await toggle(); await toggle();
await select(/W1/); assert.deepEqual(await options(), [
assert.equal(await current(), 'W1'); CUSTOM_URL, widget1.name, 'P1 (My Widgets)', 'P2 (My Widgets)',
await gu.waitToPass(async () => { ]);
assert.equal(await getWidgetText(), 'W1');
}); // Prepare to check content of widgets.
async function getWidgetText(): Promise<string> {
return gu.doInIframe(await getCustomWidgetFrame(), () => {
return driver.executeScript(
() => document.body.innerText
);
});
}
// Check build-in P2 works as expected. // Check built-in P1 works as expected.
await toggle(); await select(/P1/);
await select(/P2/); assert.equal(await current(), 'P1 (My Widgets)');
assert.equal(await current(), 'P2 (My Widgets)'); await gu.waitToPass(async () => {
await gu.waitToPass(async () => { assert.equal(await getWidgetText(), 'P1');
assert.equal(await getWidgetText(), 'P2'); });
});
// Make sure widget setting is sticky. // Check external W1 works as expected.
await gu.reloadDoc(); await toggle();
await gu.waitToPass(async () => { await select(/W1/);
assert.equal(await getWidgetText(), 'P2'); assert.equal(await current(), 'W1');
await gu.waitToPass(async () => {
assert.equal(await getWidgetText(), 'W1');
});
// Check build-in P2 works as expected.
await toggle();
await select(/P2/);
assert.equal(await current(), 'P2 (My Widgets)');
await gu.waitToPass(async () => {
assert.equal(await getWidgetText(), 'P2');
});
// Make sure widget setting is sticky.
await gu.reloadDoc();
await gu.waitToPass(async () => {
assert.equal(await getWidgetText(), 'P2');
});
}); });
}); }
}); });
}); });

Loading…
Cancel
Save