From 01bcc017c373185fa34036ea4d80c5ef105d5ee2 Mon Sep 17 00:00:00 2001 From: Juan Munoz Date: Tue, 7 May 2024 17:46:04 +0200 Subject: [PATCH] feat: allow for plugins being passed down as props to `` (#1486) * feat: allow open-scd to have plugins being passed down as props Signed-off-by: Juan Munoz * test: added integration snapshot test for plugins prop Signed-off-by: Juan Munoz * chore: refactor to implement "tasks" section on issue #1418 Signed-off-by: Juan Munoz * chore: remove unnecessary default attribute from core's Plugin interface Signed-off-by: Juan Munoz * chore: change position attr to accomodate for editor plug-in positions Signed-off-by: Juan Munoz * chore: fixing prop "default" to "active" and adding snapshot test with an editor plugin Signed-off-by: Juan Munoz * chore: fixing casting of CorePlugin type into Plugin type Signed-off-by: Juan Munoz * chore: updating with snapshots from main Signed-off-by: Juan Munoz --------- Signed-off-by: Juan Munoz Co-authored-by: Steffen van den Driest <35229971+Stef3st@users.noreply.github.com> --- packages/core/foundation.ts | 2 +- packages/core/foundation/plugin.ts | 12 ++ packages/core/mixins/Plugging.ts | 71 ------------ packages/open-scd/.eslintrc.cjs | 3 + packages/open-scd/src/open-scd.ts | 107 +++++++++++------- .../__snapshots__/open-scd.test.snap.js | 32 ++++++ .../test/integration/open-scd.test.ts | 74 ++++++++++++ 7 files changed, 190 insertions(+), 111 deletions(-) create mode 100644 packages/core/foundation/plugin.ts delete mode 100644 packages/core/mixins/Plugging.ts diff --git a/packages/core/foundation.ts b/packages/core/foundation.ts index 53830f738..d15341aaf 100644 --- a/packages/core/foundation.ts +++ b/packages/core/foundation.ts @@ -27,4 +27,4 @@ export type { export { cyrb64 } from './foundation/cyrb64.js'; export { Editing } from './mixins/Editing.js'; -export { Plugging } from './mixins/Plugging.js'; +export type { Plugin, PluginSet } from './foundation/plugin.js'; diff --git a/packages/core/foundation/plugin.ts b/packages/core/foundation/plugin.ts new file mode 100644 index 000000000..300bb19ad --- /dev/null +++ b/packages/core/foundation/plugin.ts @@ -0,0 +1,12 @@ +import { targetLocales } from '../locales.js'; + +export type Plugin = { + name: string; + translations?: Record<(typeof targetLocales)[number], string>; + src: string; + icon: string; + requireDoc?: boolean; + active?: boolean; + position: ('top' | 'middle' | 'bottom') | number; +}; +export type PluginSet = { menu: Plugin[]; editor: Plugin[] }; diff --git a/packages/core/mixins/Plugging.ts b/packages/core/mixins/Plugging.ts deleted file mode 100644 index 24930ad5d..000000000 --- a/packages/core/mixins/Plugging.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { LitElement } from 'lit'; - -import { property, state } from 'lit/decorators.js'; - -import { cyrb64, LitElementConstructor } from '../foundation.js'; -import { targetLocales } from '../locales.js'; - -export type Plugin = { - name: string; - translations?: Record; - src: string; - icon: string; - requireDoc?: boolean; - active?: boolean; -}; -export type PluginSet = { menu: Plugin[]; editor: Plugin[] }; - -const pluginTags = new Map(); - -/** @returns a valid customElement tagName containing the URI hash. */ -export function pluginTag(uri: string): string { - if (!pluginTags.has(uri)) pluginTags.set(uri, `oscd-p${cyrb64(uri)}`); - return pluginTags.get(uri)!; -} - -export interface PluginMixin { - loadedPlugins: Map; - plugins: Partial; -} - -type ReturnConstructor = new (...args: any[]) => LitElement & PluginMixin; - -export function Plugging( - Base: TBase -): TBase & ReturnConstructor { - class PluggingElement extends Base { - #loadedPlugins = new Map(); - - @state() - get loadedPlugins(): Map { - return this.#loadedPlugins; - } - - #plugins: PluginSet = { menu: [], editor: [] }; - - /** - * @prop {PluginSet} plugins - Set of plugins that are used by OpenSCD - */ - @property({ type: Object }) - get plugins(): PluginSet { - return this.#plugins; - } - - set plugins(plugins: Partial) { - Object.values(plugins).forEach(kind => - kind.forEach(plugin => { - const tagName = pluginTag(plugin.src); - if (this.loadedPlugins.has(tagName)) return; - this.#loadedPlugins.set(tagName, plugin); - if (customElements.get(tagName)) return; - const url = new URL(plugin.src, window.location.href).toString(); - import(url).then(mod => customElements.define(tagName, mod.default)); - }) - ); - - this.#plugins = { menu: [], editor: [], ...plugins }; - this.requestUpdate(); - } - } - return PluggingElement; -} diff --git a/packages/open-scd/.eslintrc.cjs b/packages/open-scd/.eslintrc.cjs index dc4fad8ef..4bd12bb17 100644 --- a/packages/open-scd/.eslintrc.cjs +++ b/packages/open-scd/.eslintrc.cjs @@ -21,4 +21,7 @@ module.exports = { 'no-duplicate-imports': 'off', 'tsdoc/syntax': 'warn' }, + env: { + browser: true, + }, }; diff --git a/packages/open-scd/src/open-scd.ts b/packages/open-scd/src/open-scd.ts index 22ddc5417..51853bb47 100644 --- a/packages/open-scd/src/open-scd.ts +++ b/packages/open-scd/src/open-scd.ts @@ -43,6 +43,7 @@ import { ActionDetail, List } from '@material/mwc-list'; import { officialPlugins } from './plugins.js'; import { initializeNsdoc, Nsdoc } from './foundation/nsdoc.js'; import { UndoRedoChangedEvent } from './addons/History.js'; +import type { PluginSet, Plugin as CorePlugin } from '@openscd/core'; // HOSTING INTERFACES @@ -178,6 +179,7 @@ export type Plugin = { name: string; src: string; icon?: string; + default?: boolean; kind: PluginKind; requireDoc?: boolean; position?: MenuPosition; @@ -198,9 +200,6 @@ function withoutContent

( return { ...plugin, content: undefined }; } -function storePlugins(plugins: Array) { - localStorage.setItem('plugins', JSON.stringify(plugins.map(withoutContent))); -} export const pluginIcons: Record = { editor: 'tab', @@ -211,18 +210,6 @@ export const pluginIcons: Record = { bottom: 'play_circle', }; -function resetPlugins(): void { - storePlugins( - officialPlugins.map(plugin => { - return { - src: plugin.src, - installed: plugin.default ?? false, - official: true, - }; - }) - ); -} - const menuOrder: (PluginKind | MenuPosition)[] = [ 'editor', 'top', @@ -290,22 +277,18 @@ export class OpenSCD extends LitElement { if (src.startsWith('blob:')) URL.revokeObjectURL(src); } - constructor() { - super(); - - this.updatePlugins(); - this.requestUpdate(); - } - connectedCallback(): void { super.connectedCallback(); - this.addEventListener('reset-plugins', resetPlugins); + this.addEventListener('reset-plugins', this.resetPlugins); this.addEventListener('add-external-plugin', (e: AddExternalPluginEvent) => { this.addExternalPlugin(e.detail.plugin); }); this.addEventListener('set-plugins', (e: SetPluginsEvent) => { this.setPlugins(e.detail.indices); }); + + this.updatePlugins(); + this.requestUpdate(); } render(): TemplateResult { @@ -330,7 +313,7 @@ export class OpenSCD extends LitElement { .doc=${this.doc} .docName=${this.docName} .editCount=${this.editCount} - .plugins=${this.plugins} + .plugins=${this._sortedStoredPlugins} > @@ -340,14 +323,56 @@ export class OpenSCD extends LitElement { `; } - @state() - get plugins(): Plugin[] { + private storePlugins(plugins: Array) { + localStorage.setItem('plugins', JSON.stringify(plugins.map(withoutContent))); + this._sortedStoredPlugins = this.sortedStoredPlugins; + } + private resetPlugins(): void { + this.storePlugins( + (officialPlugins as Plugin[]).concat(this.parsedPlugins).map(plugin => { + return { + src: plugin.src, + installed: plugin.default ?? false, + official: true, + }; + }) + ); + } + + /** + * @prop {PluginSet} plugins - Set of plugins that are used by OpenSCD + */ + @property({ type: Object }) + plugins: PluginSet = { menu: [], editor: [] }; + + get parsedPlugins(): Plugin[] { + return this.plugins.menu + .map((p: CorePlugin) => ({ + ...p, + position: + typeof p.position !== 'number' + ? (p.position as MenuPosition) + : undefined, + kind: 'menu' as PluginKind, + installed: p.active ?? false, + })) + .concat( + this.plugins.editor.map((p: CorePlugin) => ({ + ...p, + position: undefined, + kind: 'editor' as PluginKind, + installed: p.active ?? false, + })) + ); + } + + private get sortedStoredPlugins(): Plugin[] { return this.storedPlugins .map(plugin => { if (!plugin.official) return plugin; - const officialPlugin = officialPlugins.find( - needle => needle.src === plugin.src - ); + const officialPlugin = (officialPlugins as Plugin[]) + .concat(this.parsedPlugins) + .find(needle => needle.src === plugin.src); return { ...officialPlugin, ...plugin, @@ -357,9 +382,8 @@ export class OpenSCD extends LitElement { .sort(menuCompare); } - set plugins(plugins: Plugin[]) { - storePlugins(plugins); - } + @state() + _sortedStoredPlugins: Plugin[] = []; private get storedPlugins(): Plugin[] { return ( @@ -385,17 +409,20 @@ export class OpenSCD extends LitElement { } private setPlugins(indices: Set) { - const newPlugins = this.plugins.map((plugin, index) => { + const newPlugins = this.sortedStoredPlugins.map((plugin, index) => { return { ...plugin, installed: indices.has(index) }; }); - storePlugins(newPlugins); + this.storePlugins(newPlugins); this.requestUpdate(); } private updatePlugins() { const stored: Plugin[] = this.storedPlugins; const officialStored = stored.filter(p => p.official); - const newOfficial: Array = officialPlugins + const newOfficial: Array = ( + officialPlugins as Plugin[] + ) + .concat(this.parsedPlugins) .filter(p => !officialStored.find(o => o.src === p.src)) .map(plugin => { return { @@ -405,13 +432,16 @@ export class OpenSCD extends LitElement { }; }); const oldOfficial = officialStored.filter( - p => !officialPlugins.find(o => p.src === o.src) + p => + !(officialPlugins as Plugin[]) + .concat(this.parsedPlugins) + .find(o => p.src === o.src) ); const newPlugins: Array = stored.filter( p => !oldOfficial.find(o => p.src === o.src) ); newOfficial.map(p => newPlugins.push(p)); - storePlugins(newPlugins); + this.storePlugins(newPlugins); } private async addExternalPlugin(plugin: Omit): Promise { @@ -419,8 +449,7 @@ export class OpenSCD extends LitElement { const newPlugins: Omit[] = this.storedPlugins; newPlugins.push(plugin); - this.plugins = newPlugins; - await this.requestUpdate(); + this.storePlugins(newPlugins); } private addContent(plugin: Omit): Plugin { diff --git a/packages/open-scd/test/integration/__snapshots__/open-scd.test.snap.js b/packages/open-scd/test/integration/__snapshots__/open-scd.test.snap.js index a866d2a4e..e8214cac5 100644 --- a/packages/open-scd/test/integration/__snapshots__/open-scd.test.snap.js +++ b/packages/open-scd/test/integration/__snapshots__/open-scd.test.snap.js @@ -17,6 +17,38 @@ snapshots["open-scd looks like its snapshot"] = `; /* end snapshot open-scd looks like its snapshot */ +snapshots["open-scd renders menu plugins passed down as props and it looks like its snapshot"] = +` + + + + + + + + + + + +`; +/* end snapshot open-scd renders menu plugins passed down as props and it looks like its snapshot */ + +snapshots["open-scd renders editor plugins passed down as props and it looks like its snapshot"] = +` + + + + + + + + + + + +`; +/* end snapshot open-scd renders editor plugins passed down as props and it looks like its snapshot */ + snapshots["open-scd layout looks like its snapshot"] = ` diff --git a/packages/open-scd/test/integration/open-scd.test.ts b/packages/open-scd/test/integration/open-scd.test.ts index c1ce20c5f..bb00bce8d 100644 --- a/packages/open-scd/test/integration/open-scd.test.ts +++ b/packages/open-scd/test/integration/open-scd.test.ts @@ -114,4 +114,78 @@ describe('open-scd', () => { expect(element.src).to.be.a('string').and.equal(emptyBlobURL); expect(async () => await fetch(emptyBlobURL)).to.throw; }); + + it('renders menu plugins passed down as props and it looks like its snapshot', async () => { + element = await fixture(html` + + + + + + `); + await element.updateComplete; + + await expect(element).shadowDom.to.equalSnapshot(); + }); + + it('renders editor plugins passed down as props and it looks like its snapshot', async () => { + element = await fixture(html` + + + + + + `); + await element.updateComplete; + + await expect(element).shadowDom.to.equalSnapshot(); + }); }).timeout(4000);