From 91dc9612c882d4be8d773f0c48329b63c904d213 Mon Sep 17 00:00:00 2001 From: Illia Solovei Date: Mon, 16 Dec 2024 23:41:45 +0100 Subject: [PATCH 01/28] bugfix: Create virtual IED does not consider multiple instances of a LNType within a function (#1595) --- packages/openscd/src/foundation.ts | 6 +-- .../VirtualTemplateIED.test.snap.js | 50 +++++++++---------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/packages/openscd/src/foundation.ts b/packages/openscd/src/foundation.ts index b30182842f..d1f92fdc0f 100644 --- a/packages/openscd/src/foundation.ts +++ b/packages/openscd/src/foundation.ts @@ -338,7 +338,7 @@ function lNodeIdentity(e: Element): string { 'lnType', ].map(name => e.getAttribute(name)); if (iedName === 'None') - return `${identity(e.parentElement)}>(${lnClass} ${lnType})`; + return `${identity(e.parentElement)}>(${lnClass} ${lnType} ${lnInst})`; return `${iedName} ${ldInst || '(Client)'}/${prefix ?? ''} ${lnClass} ${ lnInst ?? '' }`; @@ -347,7 +347,7 @@ function lNodeIdentity(e: Element): string { function lNodeSelector(tagName: SCLTag, identity: string): string { if (identity.endsWith(')')) { const [parentIdentity, childIdentity] = pathParts(identity); - const [lnClass, lnType] = childIdentity + const [lnClass, lnType, lnInst] = childIdentity .substring(1, childIdentity.length - 1) .split(' '); @@ -360,7 +360,7 @@ function lNodeSelector(tagName: SCLTag, identity: string): string { return crossProduct( parentSelectors, ['>'], - [`${tagName}[iedName="None"][lnClass="${lnClass}"][lnType="${lnType}"]`] + [`${tagName}[iedName="None"][lnClass="${lnClass}"][lnType="${lnType}"][lnInst="${lnInst}"]`] ) .map(strings => strings.join('')) .join(','); diff --git a/packages/plugins/test/unit/menu/__snapshots__/VirtualTemplateIED.test.snap.js b/packages/plugins/test/unit/menu/__snapshots__/VirtualTemplateIED.test.snap.js index 9f308faba4..b753457b04 100644 --- a/packages/plugins/test/unit/menu/__snapshots__/VirtualTemplateIED.test.snap.js +++ b/packages/plugins/test/unit/menu/__snapshots__/VirtualTemplateIED.test.snap.js @@ -1,7 +1,7 @@ /* @web/test-runner snapshot v1 */ export const snapshots = {}; -snapshots["Plugin that creates with some user input a virtual template IED - SPECIFICATION looks like the latest snapshot"] = +snapshots["Plugin that creates with some user input a virtual template IED - SPECIFICATION looks like the latest snapshot"] = ` CSWI 1 @@ -81,7 +81,7 @@ snapshots["Plugin that creates with some user input a virtual template IED - SPE graphic="control" mwc-list-item="" tabindex="-1" - value="AA1>E1>Q01>QC9>Earth_Switch>(CILO OpenSCD_CILO)" + value="AA1>E1>Q01>QC9>Earth_Switch>(CILO OpenSCD_CILO 1)" > CILO 1 @@ -90,7 +90,7 @@ snapshots["Plugin that creates with some user input a virtual template IED - SPE graphic="control" mwc-list-item="" tabindex="-1" - value="AA1>E1>Q01>QC9>Earth_Switch>(XSWI OpenSCD_XSWI_EarthSwitch)" + value="AA1>E1>Q01>QC9>Earth_Switch>(XSWI OpenSCD_XSWI_EarthSwitch 1)" > XSWI 1 @@ -148,7 +148,7 @@ snapshots["Plugin that creates with some user input a virtual template IED - SPE graphic="control" mwc-list-item="" tabindex="-1" - value="AA1>E1>Q01>QB1>Disconnector>(CSWI OpenSCD_CSWI)" + value="AA1>E1>Q01>QB1>Disconnector>(CSWI OpenSCD_CSWI 1)" > CSWI 1 @@ -157,7 +157,7 @@ snapshots["Plugin that creates with some user input a virtual template IED - SPE graphic="control" mwc-list-item="" tabindex="-1" - value="AA1>E1>Q01>QB1>Disconnector>(XSWI OpenSCD_XSWI_DIS)" + value="AA1>E1>Q01>QB1>Disconnector>(XSWI OpenSCD_XSWI_DIS 1)" > XSWI 1 @@ -166,7 +166,7 @@ snapshots["Plugin that creates with some user input a virtual template IED - SPE graphic="control" mwc-list-item="" tabindex="-1" - value="AA1>E1>Q01>QB1>Disconnector>(CILO OpenSCD_CILO)" + value="AA1>E1>Q01>QB1>Disconnector>(CILO OpenSCD_CILO 1)" > CILO 1 @@ -224,7 +224,7 @@ snapshots["Plugin that creates with some user input a virtual template IED - SPE graphic="control" mwc-list-item="" tabindex="-1" - value="AA1>E1>Q01>QA1>Circuit_Breaker>(CSWI OpenSCD_CSWI)" + value="AA1>E1>Q01>QA1>Circuit_Breaker>(CSWI OpenSCD_CSWI 1)" > CSWI 1 @@ -233,7 +233,7 @@ snapshots["Plugin that creates with some user input a virtual template IED - SPE graphic="control" mwc-list-item="" tabindex="-1" - value="AA1>E1>Q01>QA1>Circuit_Breaker>(CILO OpenSCD_CILO)" + value="AA1>E1>Q01>QA1>Circuit_Breaker>(CILO OpenSCD_CILO 1)" > CILO 1 @@ -242,7 +242,7 @@ snapshots["Plugin that creates with some user input a virtual template IED - SPE graphic="control" mwc-list-item="" tabindex="-1" - value="AA1>E1>Q01>QA1>Circuit_Breaker>(XCBR OpenSCD_XCBR)" + value="AA1>E1>Q01>QA1>Circuit_Breaker>(XCBR OpenSCD_XCBR 1)" > XCBR 1 @@ -292,7 +292,7 @@ snapshots["Plugin that creates with some user input a virtual template IED - SPE graphic="control" mwc-list-item="" tabindex="-1" - value="AA1>E1>Q01>Timed_Overcurrent>(PTOC OpenSCD_PTOC)" + value="AA1>E1>Q01>Timed_Overcurrent>(PTOC OpenSCD_PTOC 2)" > ID_ PTOC 2 @@ -301,7 +301,7 @@ snapshots["Plugin that creates with some user input a virtual template IED - SPE graphic="control" mwc-list-item="" tabindex="-1" - value="AA1>E1>Q01>Timed_Overcurrent>(PTOC OpenSCD_PTOC)" + value="AA1>E1>Q01>Timed_Overcurrent>(PTOC OpenSCD_PTOC 1)" > IDD_ PTOC 1 @@ -359,7 +359,7 @@ snapshots["Plugin that creates with some user input a virtual template IED - SPE graphic="control" mwc-list-item="" tabindex="-1" - value="AA1>E1>Q01>Distance_Protection>Zone4>(PDIS OpenSCD_PDIS)" + value="AA1>E1>Q01>Distance_Protection>Zone4>(PDIS OpenSCD_PDIS 1)" > Zone4 PDIS 1 @@ -368,7 +368,7 @@ snapshots["Plugin that creates with some user input a virtual template IED - SPE graphic="control" mwc-list-item="" tabindex="-1" - value="AA1>E1>Q01>Distance_Protection>Zon3>(PDIS OpenSCD_PDIS)" + value="AA1>E1>Q01>Distance_Protection>Zon3>(PDIS OpenSCD_PDIS 1)" > Zon3 PDIS 1 @@ -377,7 +377,7 @@ snapshots["Plugin that creates with some user input a virtual template IED - SPE graphic="control" mwc-list-item="" tabindex="-1" - value="AA1>E1>Q01>Distance_Protection>Zone2>(PDIS OpenSCD_PDIS)" + value="AA1>E1>Q01>Distance_Protection>Zone2>(PDIS OpenSCD_PDIS 1)" > Zone2 PDIS 1 @@ -386,7 +386,7 @@ snapshots["Plugin that creates with some user input a virtual template IED - SPE graphic="control" mwc-list-item="" tabindex="-1" - value="AA1>E1>Q01>Distance_Protection>Zone1>(PDIS OpenSCD_PDIS)" + value="AA1>E1>Q01>Distance_Protection>Zone1>(PDIS OpenSCD_PDIS 1)" > Zone1 PDIS 1 @@ -444,7 +444,7 @@ snapshots["Plugin that creates with some user input a virtual template IED - SPE graphic="control" mwc-list-item="" tabindex="-1" - value="AA1>E1>Q02>QB1>Disconnector>(CSWI OpenSCD_CSWI)" + value="AA1>E1>Q02>QB1>Disconnector>(CSWI OpenSCD_CSWI 1)" > CSWI 1 @@ -453,7 +453,7 @@ snapshots["Plugin that creates with some user input a virtual template IED - SPE graphic="control" mwc-list-item="" tabindex="-1" - value="AA1>E1>Q02>QB1>Disconnector>(XSWI OpenSCD_XSWI_DIS)" + value="AA1>E1>Q02>QB1>Disconnector>(XSWI OpenSCD_XSWI_DIS 1)" > XSWI 1 @@ -462,7 +462,7 @@ snapshots["Plugin that creates with some user input a virtual template IED - SPE graphic="control" mwc-list-item="" tabindex="-1" - value="AA1>E1>Q02>QB1>Disconnector>(CILO OpenSCD_CILO)" + value="AA1>E1>Q02>QB1>Disconnector>(CILO OpenSCD_CILO 1)" > CILO 1 @@ -520,7 +520,7 @@ snapshots["Plugin that creates with some user input a virtual template IED - SPE graphic="control" mwc-list-item="" tabindex="-1" - value="AA1>J1>Q01>QC9>Earth_Switch>(CSWI OpenSCD_CSWI)" + value="AA1>J1>Q01>QC9>Earth_Switch>(CSWI OpenSCD_CSWI 1)" > CSWI 1 @@ -529,7 +529,7 @@ snapshots["Plugin that creates with some user input a virtual template IED - SPE graphic="control" mwc-list-item="" tabindex="-1" - value="AA1>J1>Q01>QC9>Earth_Switch>(CILO OpenSCD_CILO)" + value="AA1>J1>Q01>QC9>Earth_Switch>(CILO OpenSCD_CILO 1)" > CILO 1 @@ -538,7 +538,7 @@ snapshots["Plugin that creates with some user input a virtual template IED - SPE graphic="control" mwc-list-item="" tabindex="-1" - value="AA1>J1>Q01>QC9>Earth_Switch>(XSWI OpenSCD_XSWI_EarthSwitch)" + value="AA1>J1>Q01>QC9>Earth_Switch>(XSWI OpenSCD_XSWI_EarthSwitch 1)" > XSWI 1 @@ -569,7 +569,7 @@ snapshots["Plugin that creates with some user input a virtual template IED - SPE `; /* end snapshot Plugin that creates with some user input a virtual template IED - SPECIFICATION looks like the latest snapshot */ -snapshots["Plugin that creates with some user input a virtual template IED - SPECIFICATION IEDs data model show selected logical nodes and its structure"] = +snapshots["Plugin that creates with some user input a virtual template IED - SPECIFICATION IEDs data model show selected logical nodes and its structure"] = ` From dd692a8d9784aaf5f8509fdad5298293195d1465 Mon Sep 17 00:00:00 2001 From: Michel Guerin <100696308+michelguerin@users.noreply.github.com> Date: Mon, 16 Dec 2024 23:42:37 +0100 Subject: [PATCH 02/28] feat(monorepo): Add contributing guide (#1588) --- .env.example | 1 + .gitignore | 3 + .../core/CONTRIBUTING.md => CONTRIBUTING.md | 190 +++++++----------- README.md | 14 +- 4 files changed, 80 insertions(+), 128 deletions(-) create mode 100644 .env.example rename packages/core/CONTRIBUTING.md => CONTRIBUTING.md (52%) diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000..4fceb86ff8 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +CHROME_PATH= \ No newline at end of file diff --git a/.gitignore b/.gitignore index 10dd2cb905..03d149d023 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ node_modules/ /lerna-debug.log .nx/cache + +# environment variables +.env \ No newline at end of file diff --git a/packages/core/CONTRIBUTING.md b/CONTRIBUTING.md similarity index 52% rename from packages/core/CONTRIBUTING.md rename to CONTRIBUTING.md index d2fbd61b22..15ef579bae 100644 --- a/packages/core/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,63 +1,71 @@ -# Contributing to OpenSCD Core +# OpenSCD Contributing Guide -Thanks for taking the time to contribute to the OpenSCD project! +Hi! We're really excited that you're interested in contributing to OpenSCD! Before submitting your contribution, please read through the following guide. -The easiest way to get in touch is to join the `#open-scd` channel kindly hosted -on [the LF Energy Slack server](https://slack.lfenergy.org/). If you say "hi" -there we will be more than happy to help you find your way around this project. +The easiest way to get in touch is to join us on the [↗ Zulip Chat](https://openscd.zulipchat.com/join/k3cyur3wx526tvafkjwubhjn/). +If you say "hi" there we will be more than happy to help you find your way around this project. ## Non-Code Contributions You don't need to be a software developer to contribute to this effort! Apart from contributions in the form of code we are also very thankful for -- [bug reports]( -https://github.com/openscd/open-scd-core/issues?q=is%3Aopen+label%3Abug) +- [bug reports](https://github.com/openscd/open-scd/issues?q=is%3Aopen+type%3ABug) alerting us of errors in the `open-scd` component or its `foundation` library functions, -- [ideas for enhancements]( -https://github.com/openscd/open-scd-core/discussions/categories/ideas) +- [ideas for enhancements](https://github.com/openscd/open-scd/issues?q=is%3Aopen+type%3AFeature) to `open-scd` or its `foundation` library, -- [contributions to discussions]( -https://github.com/openscd/open-scd-core/discussions) - we're having about which direction the project should take, and - [improvements to our wiki](https://github.com/openscd/open-scd/wiki) which contains knowledge about how to use both OpenSCD and SCL in general. ## Code Contributions -> The following is a set of guidelines for contributing to -> [OpenSCD Core](https://github.com/openscd/open-scd-core#readme), not a list of -> strict rules. Use your best judgment and feel free to propose changes to this -> document in a pull request. +> [!NOTE] +> The following is a set of guidelines for contributing to [OpenSCD](https://github.com/openscd/open-scd#readme), not a list of strict rules. +> Use your best judgment and feel free to propose changes to this document in a pull request. -### Code Structure +### Repo Setup -The OpenSCD Core project's [NPM package declaration file]( -https://github.com/openscd/open-scd-core/blob/main/package.json) -lists two entry points that can be referred to by package users: +To develop locally, fork the OpenSCD repository and clone it in your local machine. The OpenSCD repo is a [↗ monorepo](https://en.wikipedia.org/wiki/Monorepo) using pnpm workspaces. The package manager used to install and link dependencies must be [↗ npm](https://docs.npmjs.com/cli/using-npm/workspaces). -```json - "exports": { - ".": "dist/foundation.js", - "/open-scd.js": "dist/open-scd.js" - }, -``` +To find out more about the development of each packages, such as the base distribution or the plugins, please refer to their respective READMEs: +- [open-scd](packages/openscd/README.md): provides the base distribution available on [openscd.github.io](https://openscd.github.io) +- [core](packages/core/README.md): provides the agreed api of OpenSCD Core + + +To develop, follow these steps : + +1. Install [↗ Node.js](https://nodejs.org/en/download/package-manager) + +> [!IMPORTANT] +> `Node.js` version should be set to `20.x.x` as there are incompatibilities with higher version + +2. Run `npm ci` in OpenSCD's root folder. + +3. Run `npm run build` in OpenSCD's root folder. + +4. Run `npm start` in OpenSCD's root folder. -`foundation.ts` defines a host of types, utility functions, and constants which -we hope will be useful for writing plugins that edit SCL files. +> [!NOTE] +> If you run in the following error : +> `Lerna (powered by Nx) Daemon process terminated and closed the connection` +> Rerun `npm start` and it should work as expected -`open-scd.ts` defines a custom element ``, a [web component]( -https://developer.mozilla.org/en-US/docs/Web/Web_Components) -implemented as a [LitElement](https://lit.dev/docs) extended with our own -[Mixins](https://lit.dev/docs/composition/mixins). +To test, follow these steps : + +1. Install a compatible compatible [↗ playright](https://playwright.dev/docs/browsers#introduction) browser +2. Run `npx playwright install` in OpenSCD's root folder, to install a compatible `playright` browser libraries + +> [!NOTE] +> If you are using `chromium`, you might need to add : +> `CHROME_PATH=path-to-your-chromium-app` in your .env file in OpenSCD's root folder, see `.env.example`. ### Commit Messages * Use the [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) format for commit messages. - - > A commit should contain only one single change, so you should always be - > able to find a fitting type. + + > [!NOTE] + > A commit should contain only one single change, so you should always be able to find a fitting type. * Use the present tense ("feat: add feature" not "feat: added feature") * Use the imperative mood ("fix: move cursor to..." not "fix: moves cursor to...") @@ -66,84 +74,55 @@ implemented as a [LitElement](https://lit.dev/docs) extended with our own ### Contributing Workflow and Branching Strategy -We like to receive code contributions through the [Forking Workflow]( -https://www.atlassian.com/git/tutorials/comparing-workflows/forking-workflow), -which means every contributor maintains their own independent fork and sends -pull requests directly from their own copy of the repo. This enables -contributors to work as independently as possible, with the only point of -coordination happening when a maintainer merges the incoming pull request. +We like to receive code contributions through the [Forking Workflow](https://www.atlassian.com/git/tutorials/comparing-workflows/forking-workflow), which means every contributor maintains their own independent fork and sends pull requests directly from their own copy of the repo. This enables contributors to work as independently as possible, with the only point of coordination happening when a maintainer merges the incoming pull request. -A pull request should generally only ever contain at most one `fix` or `feat` -commit, and never both. If you have several different bugs to fix or features to -introduce, please create a separate pull request for each one. If a single bug -fix or feature took you several commits to achieve, please squash those commits -into one using an interactive rebase (see the great tutorial linked under -"Forking Workflow" above) before submitting your pull request. +A pull request should generally only ever contain at most one `fix` or `feat` commit, and never both. If you have several different bugs to fix or features to introduce, please create a separate pull request for each one. If a single bug fix or feature took you several commits to achieve, please squash those commits into one using an interactive rebase (see the great tutorial linked under "Forking Workflow" above) before submitting your pull request. -Please make sure that all CI checks are passing before marking your pull request -"Ready for review". +Please make sure that all CI checks are passing before marking your pull request "Ready for review". ### Filenames -If a file defines a custom element, it should always be named after its tag name -(e.g. `my-component.ts`). Otherwise, files should generally be named after the -most important symbol they export (e.g. `MyClass.ts`). +If a file defines a custom element, it should always be named after its tag name (e.g. `my-component.ts`). Otherwise, files should generally be named after the most important symbol they export (e.g. `MyClass.ts`). ### Code Style and Linting -We use eslint and prettier for formatting and linting. Both are run as part of -a `husky` pre-commit hook defined in `package.json`. Nonetheless, we recommend -you use your editor's or IED's eslint and prettier plugins for continuous -formatting and linting while writing the code in order to avoid any surprises. +We use eslint and prettier for formatting and linting. We recommend you use your editor's or IED's eslint and prettier plugins for continuous formatting and linting while writing the code in order to avoid any surprises. -Apart from the rules the linter and formatter enforce, we adopt the following -guidelines taken from the terse but broad [Deno Style Guide]( -https://deno.land/manual/contributing/style_guide) with some minor adjustments: +Apart from the rules the linter and formatter enforce, we adopt the following guidelines taken from the terse but broad [Deno Style Guide](https://deno.land/manual/contributing/style_guide) with some minor adjustments: #### TODO Comments -In general, don't commit TODO or FIXME comments. Their significance tends to get -lost in the mists of time and they cause more confusion than anything else. +In general, don't commit TODO or FIXME comments. Their significance tends to get lost in the mists of time and they cause more confusion than anything else. -If you are tempted to write a FIXME comment, please consider fixing the code -immediately instead. If this is absolutely not possible, create a bug issue -referencing your pull request which introduces the bug. +If you are tempted to write a FIXME comment, please consider fixing the code immediately instead. If this is absolutely not possible, create a bug issue referencing your pull request which introduces the bug. -If you are tempted to write a TODO comment, please consider opening an issue -describing the changes to be made instead. +If you are tempted to write a TODO comment, please consider opening an issue describing the changes to be made instead. -If you still find it helpful to introduce a TODO comment, please include an -issue or at least the author's github username in parentheses. Example: +If you still find it helpful to introduce a TODO comment, please include an issue or at least the author's github username in parentheses. Example: ```ts // TODO(ry): Add tests. // TODO(#123): Support Windows. // FIXME(#349): Sometimes panics. ``` - #### Exported functions: max 2 args, put the rest into an options object. When designing function interfaces, stick to the following rules. -1. A function that is part of the public API takes 0-2 required arguments, plus - (if necessary) an options object (so max 3 total). +1. A function that is part of the public API takes 0-2 required arguments, plus (if necessary) an options object (so max 3 total). 2. Optional parameters should generally go into the options object. - An optional parameter that's not in an options object might be acceptable if - there is only one, and it seems inconceivable that we would add more optional - parameters in the future. + An optional parameter that's not in an options object might be acceptable if there is only one, and it seems inconceivable that we would add more optional parameters in the future. 3. The 'options' argument is the only argument that is a regular 'Object'. - Other arguments can be objects, but they must be distinguishable from a - 'plain' Object runtime, by having either: + Other arguments can be objects, but they must be distinguishable from a 'plain' Object runtime, by having either: - - a distinguishing prototype (e.g. `Array`, `Map`, `Date`, `class MyThing`). - - a well-known symbol property (e.g. an iterable with `Symbol.iterator`). + - a distinguishing prototype (e.g. `Array`, `Map`, `Date`, `class MyThing`). + - a well-known symbol property (e.g. an iterable with `Symbol.iterator`). - This allows the API to evolve in a backwards compatible way, even when the - position of the options object changes. + This allows the API to evolve in a backwards compatible way, even when the position of the options object changes. ```ts, ignore // BAD: optional parameters not part of options object. (#2) @@ -230,12 +209,9 @@ export interface PWrite { } export function pwrite(options: PWrite) {} ``` - #### Export all interfaces that are used as parameters to an exported member -Whenever you are using interfaces that are included in the parameters or return -type of an exported member, you should export the interface that is used. Here -is an example: +Whenever you are using interfaces that are included in the parameters or return type of an exported member, you should export the interface that is used. Here is an example: ```ts, ignore // my-file.ts @@ -252,7 +228,6 @@ export function createPerson(name: string, age: number): Person { export { createPerson } from "./my-file.js"; export type { Person } from "./my-file.js"; ``` - #### Minimize dependencies; do not make circular imports. Try not to introduce external dependencies if you can avoid doing so. @@ -260,14 +235,11 @@ In particular, be careful not to introduce circular imports. #### If a filename starts with an underscore: `_foo.ts`, do not link to it. -There may be situations where an internal module is necessary but its API is not -meant to be stable or linked to. In this case prefix it with an underscore. By -convention, only files in its own directory should import it. +There may be situations where an internal module is necessary but its API is not meant to be stable or linked to. In this case prefix it with an underscore. By convention, only files in its own directory should import it. #### Use JSDoc for exported symbols. -We strive for complete documentation. Every exported symbol ideally should have -a documentation line. +We strive for complete documentation. Every exported symbol ideally should have a documentation line. If possible, use a single line for the JSDoc. Example: @@ -278,10 +250,7 @@ export function foo() { } ``` -It is important that documentation is easily human-readable, but there is also a -need to provide additional styling information to ensure generated documentation -is more rich text. Therefore JSDoc should generally follow markdown markup to -enrich the text. +It is important that documentation is easily human-readable, but there is also a need to provide additional styling information to ensure generated documentation is more rich text. Therefore JSDoc should generally follow markdown markup to enrich the text. While markdown supports HTML tags, it is forbidden in JSDoc blocks. @@ -292,10 +261,8 @@ For example: /** Import something from the `foundation` module. */ ``` -Do not document function arguments unless they are non-obvious of their intent -(though if they are non-obvious intent, the API should be considered anyways). -Therefore `@param` should generally not be used. If `@param` is used, it should -not include the `type` as TypeScript is already strongly-typed. +Do not document function arguments unless they are non-obvious of their intent (though if they are non-obvious intent, the API should be considered anyways). +Therefore `@param` should generally not be used. If `@param` is used, it should not include the `type` as TypeScript is already strongly-typed. ```ts /** @@ -304,8 +271,7 @@ not include the `type` as TypeScript is already strongly-typed. */ ``` -Vertical spacing should be minimized whenever possible. Therefore, single-line -comments should be written as: +Vertical spacing should be minimized whenever possible. Therefore, single-line comments should be written as: ```ts /** This is a good single-line JSDoc. */ @@ -331,14 +297,11 @@ Code examples should utilize markdown format, like so: ```` Code examples should not contain additional comments and must not be indented. -It is already inside a comment. If it needs further comments, it is not a good -example. +It is already inside a comment. If it needs further comments, it is not a good example. #### Resolve linting problems using directives -Currently, the building process uses `eslint` to lint the code. If the task -requires code that is non-conformant to linter use `eslint-disable-next-line -` directive to suppress the warning. +Currently, the building process uses `eslint` to lint the code. If the task requires code that is non-conformant to linter use `eslint-disable-next-line` directive to suppress the warning. ```typescript /** Constructor type for defining `LitElement` mixins. */ @@ -346,18 +309,15 @@ requires code that is non-conformant to linter use `eslint-disable-next-line export type LitElementConstructor = new (...args: any[]) => LitElement; ``` -This ensures the continuous integration process doesn't fail due to linting -problems, but it should be used scarcely. +This ensures the continuous integration process doesn't fail due to linting problems, but it should be used scarcely. #### Each module should come with a test module. -Every module with public functionality `foo.ts` should come with a test module -`foo.spec.ts`. This file should be a sibling to the tested module. +Every module with public functionality `foo.ts` should come with a test module `foo.spec.ts`. This file should be a sibling to the tested module. #### Top-level functions should not use arrow syntax. -Top-level functions should use the `function` keyword. Arrow syntax should be -limited to closures. +Top-level functions should use the `function` keyword. Arrow syntax should be limited to closures. Bad: @@ -374,13 +334,9 @@ export function foo(): string { return "bar"; } ``` - #### Prefer `#` over `private` -We prefer the private fields (`#`) syntax over `private` keyword of TypeScript -in the standard modules codebase. The private fields make the properties and -methods private even at runtime. On the other hand, `private` keyword of -TypeScript guarantee it private only at compile time and the fields are publicly +We prefer the private fields (`#`) syntax over `private` keyword of TypeScript in the standard modules codebase. The private fields make the properties and methods private even at runtime. On the other hand, `private` keyword of TypeScript guarantee it private only at compile time and the fields are publicly accessible at runtime. Good: @@ -399,4 +355,4 @@ class MyClass { private foo = 1; private bar() {} } -``` +``` \ No newline at end of file diff --git a/README.md b/README.md index 6fdb4a6ed8..40f7673832 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,13 @@ Open Substation Communication Designer is an editor for SCL files as described in `IEC 61850-6`. -> Try it out at [openscd.github.io](https://openscd.github.io)! +> Try it out at [↗ openscd.github.io](https://openscd.github.io)! Make sure your web browser has enough free memory! If needed, disable plug-ins and close unused browser tabs. ## Installation -In order to install OpenSCD on your local device (only for you), simply visit [openscd.github.io](https://openscd.github.io), click the "Install OpenSCD" button in your address bar (Chrome or Edge on desktop) or click the "Add OpenSCD to home screen" notification in any mobile browser. +In order to install OpenSCD on your local device (only for you), simply visit [↗ openscd.github.io](https://openscd.github.io), click the "Install OpenSCD" button in your address bar (Chrome or Edge on desktop) or click the "Add OpenSCD to home screen" notification in any mobile browser. In order to install your own instance of OpenSCD on your own webserver (e.g. on your company intranet), simply download [our latest release](https://github.com/openscd/open-scd/releases/latest) (`open-scd.zip` or `open-scd.tar.gz`) and extract the archive contents into the "webroot" directory of your web server. @@ -24,17 +24,9 @@ If you don't have your own webserver but still want your own version of OpenSCD We gather the available plug-ins from the community in the [plug-ins](docs/plug-ins.md) file. If you would like to list your plug-in here, please open a pull request. -## Development - -This repository is a [↗ monorepo](https://en.wikipedia.org/wiki/Monorepo), made up of several packages. -To find out more about the development of each packages, such as the base distribution or the plugins, please refer to their respective READMEs: -- [open-scd](packages/openscd/README.md): provides the base distribution available on [openscd.github.io](https://openscd.github.io) -- [core](packages/core/README.md): provides the agreed api of OpenSCD Core - ## Contributing -The easiest way to get in touch is to join us on the [Zulip Chat](https://openscd.zulipchat.com/join/k3cyur3wx526tvafkjwubhjn/). -If you say "hi" there we will be more than happy to help you find your way around this project. +See [Contributing Guide](CONTRIBUTING.md) ## Documentation From ce39e2b7bfcda40659f36e40659b1efd571f2a53 Mon Sep 17 00:00:00 2001 From: Christopher Lepski <139237321+clepski@users.noreply.github.com> Date: Mon, 27 Jan 2025 10:41:18 +0100 Subject: [PATCH 03/28] feat: Edit api v3 (#1615) --- .github/workflows/test-and-build.yml | 2 +- .github/workflows/test.yml | 2 +- docs/core-api/edit-api.md | 116 ++++++++- packages/core/foundation.ts | 26 +- .../core/foundation/deprecated/edit-event.ts | 119 +++++++++ packages/core/foundation/deprecated/editor.ts | 2 +- .../core/foundation/deprecated/history.ts | 7 +- .../core/foundation/edit-completed-event.ts | 2 +- packages/core/foundation/edit-event.ts | 79 ++---- packages/core/foundation/edit.ts | 79 ++++++ packages/core/foundation/handle-edit.ts | 189 ++++++++++++++ packages/openscd/src/addons/Editor.ts | 174 ++++--------- packages/openscd/src/addons/History.ts | 57 ++++- .../editor/edit-action-to-v1-converter.ts | 130 ++++++++++ .../addons/editor/edit-v1-to-v2-converter.ts | 152 +++--------- packages/openscd/test/unit/Editor.test.ts | 233 +++++++++++------- .../unit/edit-action-to-v1-converter.test.ts | 148 +++++++++++ .../test/unit/edit-v1-to-v2-converter.test.ts | 144 +++++------ 18 files changed, 1167 insertions(+), 494 deletions(-) create mode 100644 packages/core/foundation/deprecated/edit-event.ts create mode 100644 packages/core/foundation/edit.ts create mode 100644 packages/core/foundation/handle-edit.ts create mode 100644 packages/openscd/src/addons/editor/edit-action-to-v1-converter.ts create mode 100644 packages/openscd/test/unit/edit-action-to-v1-converter.test.ts diff --git a/.github/workflows/test-and-build.yml b/.github/workflows/test-and-build.yml index 3b475e0e11..3f107044f3 100644 --- a/.github/workflows/test-and-build.yml +++ b/.github/workflows/test-and-build.yml @@ -3,7 +3,7 @@ on: pull_request jobs: test-and-build: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@v2.3.1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fe51a19b74..12bfc92f87 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ on: jobs: test: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@v2.3.1 diff --git a/docs/core-api/edit-api.md b/docs/core-api/edit-api.md index e5e3a459f9..3029221682 100644 --- a/docs/core-api/edit-api.md +++ b/docs/core-api/edit-api.md @@ -1,6 +1,6 @@ -# Edit Event API +# Edit Event API v2 -Open SCD offers an API for editing the scd document which can be used with [Html Custom Events](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent). The main Open SCD components listens to events of the type `oscd-edit`, applies the changes to the `doc` and updates the `editCount` property. +Open SCD offers an API for editing the scd document which can be used with [Html Custom Events](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent). The main Open SCD components listens to events of the type `oscd-edit-v2`, applies the changes to the `doc` and updates the `editCount` property. The edits to the `doc` will be done in place, e.g. the `doc` changes but will keep the same reference. If your plugin needs to react to changes in the doc, you should listen to changes in the `editCount` property. @@ -8,6 +8,116 @@ The edits to the `doc` will be done in place, e.g. the `doc` changes but will ke Open SCD core exports a factory function for edit events, so you do not have to build them manually. +```ts +function newEditEventV2( + edit: E, + options?: EditEventOptionsV2 +): EditEventV2 + +type EditV2 = InsertV2 | SetAttributesV2 | SetTextContentV2 | RemoveV2 | EditV2[]; + +interface EditEventOptionsV2 = { + title?: string; + squash?: boolean; + createHistoryEntry?: boolean; +}; +``` + +### EditEventOptionsV2 + +* `title` set a title to be shown in the history. +* `squash` squash edit with previous history entry, this is useful if you want to create multiple edits based on an user action, but need the updated `doc` before applying each edit. Defaults to `false`. +* `createHistoryEntry` decides whether a history for the `edit` should be created. Defaults to `true`. + +### Insert + +Insert events can be used to add new nodes or move existing nodes in the document. Since a node can only have one parent, using an insert on an existing node will replace it's previous parent with the new parent, essentially moving the node to a different position in the xml tree. + +If the reference is not `null`, the node will be inserted before the reference node. The reference has to be a child node of the parent. And if the reference is `null` the node will be added as the last child of the parent. + +```ts +interface InsertV2 { + parent: Node; + node: Node; + reference: Node | null; +} +``` + +### Remove + +This event will remove the node from the document. + +```ts +interface RemoveV2 { + node: Node; +} +``` + +### SetAttributes + +Sets attributes for the element, can set both regular and namespaced attributes. + +```ts +interface SetAttributesV2 { + element: Element; + attributes: Partial>; + attributesNS: Partial>>>; +} +``` + +To set a namespaced attribute see the following example. Here we are setting the attribute `exa:type` for the namespace `https://example.com` to `secondary`. + +```ts +const setNamespacedAttributes: SetAttributesV2 = { + element, + attributes: {}, + attributesNS: { + "https://example.com": { + "exa:type": "secondary" + } + } +} +``` + +### SetTextContent + +Sets the text content of the element, removes any other children. To remove text content you can pass `null` as value for `textContent`. + +```ts +interface SetTextContentV2 { + element: Element; + textContent: string; +} +``` + +### Complex edits + +Complex edits can be used to apply multiple edits as a single event. This will create a single entry in the history. You can create complex edit events by passing an array of edit events to the `newEditEventV2` factory function. + +```ts +import { newEditEventV2 } from '@openscd/core'; + +const complexEditEvent = newEditEventV2([ insert, update, remove ]); + +someComponent.dispatchEvent(complexEditEvent); + +``` + +## History + +All edit events with the option `createHistoryEntry` will create a history log entry and can be undone and redone through the history addon. + + +# Archives + +## Edit Event API v1 (deprecated) + +The edit event API v1 is still available and listens to events of the type `oscd-edit`. + +## Event factory + +Open SCD core exports a factory function for edit events, so you do not have to build them manually. + ```ts function newEditEvent( edit: E, @@ -158,7 +268,7 @@ With open SCD version **v0.36.0** and higher some editor action features are no --- -# Archives - Editor Action API (deprecated) +## Editor Action API (deprecated) ### Event factory diff --git a/packages/core/foundation.ts b/packages/core/foundation.ts index c7cd164a43..4b34b7b25d 100644 --- a/packages/core/foundation.ts +++ b/packages/core/foundation.ts @@ -13,7 +13,7 @@ export { isNamespaced, isUpdate, isRemove, -} from './foundation/edit-event.js'; +} from './foundation/deprecated/edit-event.js'; export type { EditEvent, Edit, @@ -22,7 +22,31 @@ export type { NamespacedAttributeValue, Update, Remove, +} from './foundation/deprecated/edit-event.js'; + +export type { + EditV2, + InsertV2, + RemoveV2, + SetTextContentV2, + SetAttributesV2, +} from './foundation/edit.js'; +export { + isEditV2, + isRemoveV2, + isInsertV2, + isComplexV2, + isSetAttributesV2, + isSetTextContentV2 +} from './foundation/edit.js'; +export type { + EditEventV2, + EditEventOptionsV2, + EditDetailV2 } from './foundation/edit-event.js'; +export { newEditEventV2 } from './foundation/edit-event.js'; + +export { handleEditV2 } from './foundation/handle-edit.js'; export { cyrb64 } from './foundation/cyrb64.js'; diff --git a/packages/core/foundation/deprecated/edit-event.ts b/packages/core/foundation/deprecated/edit-event.ts new file mode 100644 index 0000000000..4abf0dae56 --- /dev/null +++ b/packages/core/foundation/deprecated/edit-event.ts @@ -0,0 +1,119 @@ +/** + * @deprecated Use the new edit event V2 API instead. + */ +export type Initiator = 'user' | 'system' | 'undo' | 'redo' | string; + +/** + * @deprecated Use the new edit event V2 API instead. + */ +export type Insert = { + parent: Node; + node: Node; + reference: Node | null; +}; + +/** + * @deprecated Use the new edit event V2 API instead. + */ +export type NamespacedAttributeValue = { + value: string | null; + namespaceURI: string | null; +}; +/** + * @deprecated Use the new edit event V2 API instead. + */ +export type AttributeValue = string | null | NamespacedAttributeValue; +/** + * @deprecated Use the new edit event V2 API instead. + */ +export type Update = { + element: Element; + attributes: Partial>; +}; + +/** + * @deprecated Use the new edit event V2 API instead. + */ +export type Remove = { + node: Node; +}; + +/** + * @deprecated Use the new edit event V2 API instead. + */ +export type Edit = Insert | Update | Remove | Edit[]; + +/** + * @deprecated Use the new edit event V2 API instead. + */ +export function isComplex(edit: Edit): edit is Edit[] { + return edit instanceof Array; +} + +/** + * @deprecated Use the new edit event V2 API instead. + */ +export function isInsert(edit: Edit): edit is Insert { + return (edit as Insert).parent !== undefined; +} + +/** + * @deprecated Use the new edit event V2 API instead. + */ +export function isNamespaced( + value: AttributeValue +): value is NamespacedAttributeValue { + return value !== null && typeof value !== 'string'; +} + +/** + * @deprecated Use the new edit event V2 API instead. + */ +export function isUpdate(edit: Edit): edit is Update { + return (edit as Update).element !== undefined; +} + +/** + * @deprecated Use the new edit event V2 API instead. + */ +export function isRemove(edit: Edit): edit is Remove { + return ( + (edit as Insert).parent === undefined && (edit as Remove).node !== undefined + ); +} + +/** + * @deprecated Use the new edit event V2 API instead. + */ +export interface EditEventDetail { + edit: E; + initiator: Initiator; +} + +/** + * @deprecated Use the new edit event V2 API instead. + */ +export type EditEvent = CustomEvent; + +/** + * @deprecated Use the new edit event V2 API instead. + */ +export function newEditEvent( + edit: E, + initiator: Initiator = 'user' +): EditEvent { + return new CustomEvent('oscd-edit', { + composed: true, + bubbles: true, + detail: { + edit: edit, + initiator: initiator, + }, + }); +} + +declare global { + interface ElementEventMap { + ['oscd-edit']: EditEvent; + } +} diff --git a/packages/core/foundation/deprecated/editor.ts b/packages/core/foundation/deprecated/editor.ts index 5aa7350bac..5f7b53c2dc 100644 --- a/packages/core/foundation/deprecated/editor.ts +++ b/packages/core/foundation/deprecated/editor.ts @@ -1,4 +1,4 @@ -import { Initiator } from '../edit-event.js'; +import { Initiator } from './edit-event.js'; /** Inserts `new.element` to `new.parent` before `new.reference`. */ export interface Create { diff --git a/packages/core/foundation/deprecated/history.ts b/packages/core/foundation/deprecated/history.ts index d6c6d6c648..b78f919432 100644 --- a/packages/core/foundation/deprecated/history.ts +++ b/packages/core/foundation/deprecated/history.ts @@ -1,4 +1,4 @@ -import { Edit } from '../edit-event.js'; +import { EditV2 } from '../edit.js'; type InfoEntryKind = 'info' | 'warning' | 'error'; @@ -12,8 +12,9 @@ export interface LogDetailBase { /** The [[`LogEntry`]] for a committed [[`EditorAction`]]. */ export interface CommitDetail extends LogDetailBase { kind: 'action'; - redo: Edit; - undo: Edit; + redo: EditV2; + undo: EditV2; + squash?: boolean; } /** A [[`LogEntry`]] for notifying the user. */ export interface InfoDetail extends LogDetailBase { diff --git a/packages/core/foundation/edit-completed-event.ts b/packages/core/foundation/edit-completed-event.ts index b2128ded9a..ed8ad8df08 100644 --- a/packages/core/foundation/edit-completed-event.ts +++ b/packages/core/foundation/edit-completed-event.ts @@ -1,4 +1,4 @@ -import { Edit, Initiator } from './edit-event.js'; +import { Edit, Initiator } from './deprecated/edit-event.js'; import { EditorAction } from './deprecated/editor.js'; diff --git a/packages/core/foundation/edit-event.ts b/packages/core/foundation/edit-event.ts index 1cd6649932..79b1a62155 100644 --- a/packages/core/foundation/edit-event.ts +++ b/packages/core/foundation/edit-event.ts @@ -1,78 +1,35 @@ -export type Initiator = 'user' | 'system' | 'undo' | 'redo' | string; +import { EditV2 } from './edit.js'; -/** Intent to `parent.insertBefore(node, reference)` */ -export type Insert = { - parent: Node; - node: Node; - reference: Node | null; -}; - -export type NamespacedAttributeValue = { - value: string | null; - namespaceURI: string | null; -}; -export type AttributeValue = string | null | NamespacedAttributeValue; -/** Intent to set or remove (if null) attributes on element */ -export type Update = { - element: Element; - attributes: Partial>; -}; - -/** Intent to remove a node from its ownerDocument */ -export type Remove = { - node: Node; +export type EditDetailV2 = EditEventOptionsV2 & { + edit: E; }; -/** Represents the user's intent to change an XMLDocument */ -export type Edit = Insert | Update | Remove | Edit[]; - -export function isComplex(edit: Edit): edit is Edit[] { - return edit instanceof Array; -} - -export function isInsert(edit: Edit): edit is Insert { - return (edit as Insert).parent !== undefined; -} - -export function isNamespaced( - value: AttributeValue -): value is NamespacedAttributeValue { - return value !== null && typeof value !== 'string'; -} - -export function isUpdate(edit: Edit): edit is Update { - return (edit as Update).element !== undefined; -} +export type EditEventV2 = CustomEvent< + EditDetailV2 +>; -export function isRemove(edit: Edit): edit is Remove { - return ( - (edit as Insert).parent === undefined && (edit as Remove).node !== undefined - ); +type BaseEditEventOptionsV2 = { + title?: string; + squash?: boolean; } -export interface EditEventDetail { - edit: E; - initiator: Initiator; -} - -export type EditEvent = CustomEvent; +export type EditEventOptionsV2 = BaseEditEventOptionsV2 & { + createHistoryEntry?: boolean; +}; -export function newEditEvent( +export function newEditEventV2( edit: E, - initiator: Initiator = 'user' -): EditEvent { - return new CustomEvent('oscd-edit', { + options?: EditEventOptionsV2 +): EditEventV2 { + return new CustomEvent>('oscd-edit-v2', { composed: true, bubbles: true, - detail: { - edit: edit, - initiator: initiator, - }, + detail: { ...options, edit }, }); } declare global { interface ElementEventMap { - ['oscd-edit']: EditEvent; + ['oscd-edit-v2']: EditEventV2; } } diff --git a/packages/core/foundation/edit.ts b/packages/core/foundation/edit.ts new file mode 100644 index 0000000000..cce63f144d --- /dev/null +++ b/packages/core/foundation/edit.ts @@ -0,0 +1,79 @@ +/** Intent to `parent.insertBefore(node, reference)` */ +export type InsertV2 = { + parent: Node; + node: Node; + reference: Node | null; +}; + +/** Intent to remove a `node` from its `ownerDocument` */ +export type RemoveV2 = { + node: Node; +}; + +/** Intent to set the `textContent` of `element` */ +export type SetTextContentV2 = { + element: Element; + textContent: string; +}; + +/** Intent to set or remove (if `null`) `attributes`(-`NS`) on `element` */ +export type SetAttributesV2 = { + element: Element; + attributes: Partial>; + attributesNS: Partial>>>; +}; + +/** Intent to change some XMLDocuments */ +export type EditV2 = + | InsertV2 + | SetAttributesV2 + | SetTextContentV2 + | RemoveV2 + | EditV2[]; + +export function isComplexV2(edit: EditV2): edit is EditV2[] { + return edit instanceof Array; +} + +export function isSetTextContentV2(edit: EditV2): edit is SetTextContentV2 { + return ( + (edit as SetTextContentV2).element !== undefined && + (edit as SetTextContentV2).textContent !== undefined + ); +} + +export function isRemoveV2(edit: EditV2): edit is RemoveV2 { + return ( + (edit as InsertV2).parent === undefined && (edit as RemoveV2).node !== undefined + ); +} + +export function isSetAttributesV2(edit: EditV2): edit is SetAttributesV2 { + return ( + (edit as SetAttributesV2).element !== undefined && + (edit as SetAttributesV2).attributes !== undefined && + (edit as SetAttributesV2).attributesNS !== undefined + ); +} + +export function isInsertV2(edit: EditV2): edit is InsertV2 { + return ( + (edit as InsertV2).parent !== undefined && + (edit as InsertV2).node !== undefined && + (edit as InsertV2).reference !== undefined + ); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function isEditV2(edit: any): edit is EditV2 { + if (isComplexV2(edit)) { + return !edit.some((e) => !isEditV2(e)); + } + + return ( + isSetAttributesV2(edit) || + isSetTextContentV2(edit) || + isInsertV2(edit) || + isRemoveV2(edit) + ); +} diff --git a/packages/core/foundation/handle-edit.ts b/packages/core/foundation/handle-edit.ts new file mode 100644 index 0000000000..9122dc5d02 --- /dev/null +++ b/packages/core/foundation/handle-edit.ts @@ -0,0 +1,189 @@ +import { + EditV2, + InsertV2, + isComplexV2, + isInsertV2, + isRemoveV2, + isSetAttributesV2, + isSetTextContentV2, + RemoveV2, + SetAttributesV2, + SetTextContentV2, +} from './edit.js'; + +function handleSetTextContent({ + element, + textContent, +}: SetTextContentV2): (SetTextContentV2 | InsertV2)[] { + const { childNodes } = element; + + const restoreChildNodes: InsertV2[] = Array.from(childNodes).map((node) => ({ + parent: element, + node, + reference: null, + })); + + element.textContent = textContent; + + const undoTextContent: SetTextContentV2 = { element, textContent: '' }; + + return [undoTextContent, ...restoreChildNodes]; +} + +function uniqueNSPrefix(element: Element, ns: string): string { + let i = 1; + const attributes = Array.from(element.attributes); + const hasSamePrefix = (attribute: Attr) => + attribute.prefix === `ens${i}` && attribute.namespaceURI !== ns; + const nsOrNull = new Set([null, ns]); + const differentNamespace = (prefix: string) => + !nsOrNull.has(element.lookupNamespaceURI(prefix)); + while (differentNamespace(`ens${i}`) || attributes.find(hasSamePrefix)) + i += 1; + return `ens${i}`; +} + +const xmlAttributeName = /^(?!xml|Xml|xMl|xmL|XMl|xML|XmL|XML)[A-Za-z_][A-Za-z0-9-_.]*(:[A-Za-z_][A-Za-z0-9-_.]*)?$/; + +function validName(name: string): boolean { + return xmlAttributeName.test(name); +} + +function handleSetAttributes({ + element, + attributes, + attributesNS, +}: SetAttributesV2): SetAttributesV2 { + const oldAttributes = { ...attributes }; + const oldAttributesNS = { ...attributesNS }; + + // save element's non-prefixed attributes for undo + Object.keys(attributes) + .reverse() + .forEach((name) => { + oldAttributes[name] = element.getAttribute(name); + }); + + // change element's non-prefixed attributes + for (const entry of Object.entries(attributes)) { + try { + const [name, value] = entry as [string, string | null]; + if (value === null) element.removeAttribute(name); + else element.setAttribute(name, value); + } catch (_e) { + // undo nothing if update didn't work on this attribute + delete oldAttributes[entry[0]]; + } + } + + // save element's namespaced attributes for undo + Object.entries(attributesNS).forEach(([ns, attrs]) => { + Object.keys(attrs!) + .filter(validName) + .reverse() + .forEach((name) => { + oldAttributesNS[ns] = { + ...oldAttributesNS[ns], + [name]: element.getAttributeNS(ns, name.split(':').pop()!), + }; + }); + Object.keys(attrs!) + .filter((name) => !validName(name)) + .forEach((name) => { + delete oldAttributesNS[ns]![name]; + }); + }); + + // change element's namespaced attributes + for (const nsEntry of Object.entries(attributesNS)) { + const [ns, attrs] = nsEntry as [ + string, + Partial>, + ]; + for (const entry of Object.entries(attrs).filter(([name]) => + validName(name), + )) { + try { + const [name, value] = entry as [string, string | null]; + if (value === null) { + element.removeAttributeNS(ns, name.split(':').pop()!); + } else { + let qualifiedName = name; + if (!qualifiedName.includes(':')) { + let prefix = element.lookupPrefix(ns); + if (!prefix) prefix = uniqueNSPrefix(element, ns); + qualifiedName = `${prefix}:${name}`; + } + element.setAttributeNS(ns, qualifiedName, value); + } + } catch (_e) { + delete oldAttributesNS[ns]![entry[0]]; + } + } +} + +return { + element, + attributes: oldAttributes, + attributesNS: oldAttributesNS, +}; +} + +function handleRemove({ node }: RemoveV2): InsertV2 | [] { + const { parentNode: parent, nextSibling: reference } = node; + node.parentNode?.removeChild(node); + if (parent) + return { + node, + parent, + reference, + }; + return []; +} + +function handleInsert({ + parent, + node, + reference, +}: InsertV2): InsertV2 | RemoveV2 | [] { + try { + const { parentNode, nextSibling } = node; + + /** + * This is a workaround for converted edit api v1 events, + * because if multiple edits are converted, they are converted before the changes from the previous edits are applied to the document + * so if you first remove an element and then add a clone with changed attributes, the reference will be the element to remove since it hasnt been removed yet + */ + if (!parent.contains(reference)) { + reference = null; + } + + parent.insertBefore(node, reference); + + if (parentNode) { + // undo: move child node back to original place + return { + node, + parent: parentNode, + reference: nextSibling, + }; + } + + // undo: remove orphaned node + return { node }; + } catch (_e) { + // undo nothing if insert doesn't work on these nodes + return []; + } +} + +/** Applies an Edit, returning the corresponding 'undo' Edit. */ +export function handleEditV2(edit: EditV2): EditV2 { + if (isInsertV2(edit)) return handleInsert(edit); + if (isRemoveV2(edit)) return handleRemove(edit); + if (isSetAttributesV2(edit)) return handleSetAttributes(edit); + if (isSetTextContentV2(edit)) return handleSetTextContent(edit); + if (isComplexV2(edit)) return edit.map((edit) => handleEditV2(edit)).reverse(); + + return []; +} diff --git a/packages/openscd/src/addons/Editor.ts b/packages/openscd/src/addons/Editor.ts index a2ea531dae..70a07fca52 100644 --- a/packages/openscd/src/addons/Editor.ts +++ b/packages/openscd/src/addons/Editor.ts @@ -1,4 +1,17 @@ -import { OpenEvent, newEditCompletedEvent, newEditEvent } from '@openscd/core'; +import { + EditV2, + EditEventV2, + OpenEvent, + newEditCompletedEvent, + newEditEvent, + handleEditV2, + isInsertV2, + isRemoveV2, + isSetAttributesV2, + isSetTextContentV2, + isComplexV2, + newEditEventV2 +} from '@openscd/core'; import { property, LitElement, @@ -31,6 +44,7 @@ import { Update, } from '@openscd/core'; +import { convertEditActiontoV1 } from './editor/edit-action-to-v1-converter.js'; import { convertEditV1toV2 } from './editor/edit-v1-to-v2-converter.js'; @customElement('oscd-editor') @@ -48,21 +62,21 @@ export class OscdEditor extends LitElement { }) host!: HTMLElement; - private getLogText(edit: Edit): { title: string, message?: string } { - if (isInsert(edit)) { + private getLogText(edit: EditV2): { title: string, message?: string } { + if (isInsertV2(edit)) { const name = edit.node instanceof Element ? edit.node.tagName : get('editing.node'); return { title: get('editing.created', { name }) }; - } else if (isUpdate(edit)) { + } else if (isSetAttributesV2(edit) || isSetTextContentV2(edit)) { const name = edit.element.tagName; return { title: get('editing.updated', { name }) }; - } else if (isRemove(edit)) { + } else if (isRemoveV2(edit)) { const name = edit.node instanceof Element ? edit.node.tagName : get('editing.node'); return { title: get('editing.deleted', { name }) }; - } else if (isComplex(edit)) { + } else if (isComplexV2(edit)) { const message = edit.map(e => this.getLogText(e)).map(({ title }) => title).join(', '); return { title: get('editing.complex'), message }; } @@ -71,10 +85,26 @@ export class OscdEditor extends LitElement { } private onAction(event: EditorActionEvent) { - const edit = convertEditV1toV2(event.detail.action); - const initiator = event.detail.initiator; + const edit = convertEditActiontoV1(event.detail.action); + const editV2 = convertEditV1toV2(edit); - this.host.dispatchEvent(newEditEvent(edit, initiator)); + this.host.dispatchEvent(newEditEventV2(editV2)); + } + + handleEditEvent(event: EditEvent) { + /** + * This is a compatibility fix for plugins based on open energy tools edit events + * because their edit event look slightly different + * see https://github.com/OpenEnergyTools/open-scd-core/blob/main/foundation/edit-event-v1.ts for details + */ + if (isOpenEnergyEditEvent(event)) { + event = convertOpenEnergyEditEventToEditEvent(event); + } + + const edit = event.detail.edit; + const editV2 = convertEditV1toV2(edit); + + this.host.dispatchEvent(newEditEventV2(editV2)); } /** @@ -108,8 +138,10 @@ export class OscdEditor extends LitElement { // Deprecated editor action API, use 'oscd-edit' instead. this.host.addEventListener('editor-action', this.onAction.bind(this)); - + // Deprecated edit event API, use 'oscd-edit-v2' instead. this.host.addEventListener('oscd-edit', event => this.handleEditEvent(event)); + + this.host.addEventListener('oscd-edit-v2', event => this.handleEditEventV2(event)); this.host.addEventListener('open-doc', this.onOpenDoc); this.host.addEventListener('oscd-open', this.handleOpenDoc); } @@ -118,34 +150,23 @@ export class OscdEditor extends LitElement { return html``; } - async handleEditEvent(event: EditEvent) { - /** - * This is a compatibility fix for plugins based on open energy tools edit events - * because their edit event look slightly different - * see https://github.com/OpenEnergyTools/open-scd-core/blob/main/foundation/edit-event-v1.ts for details - */ - if (isOpenEnergyEditEvent(event)) { - event = convertOpenEnergyEditEventToEditEvent(event); - } - + async handleEditEventV2(event: EditEventV2) { const edit = event.detail.edit; - const undoEdit = handleEdit(edit); - this.dispatchEvent( - newEditCompletedEvent(event.detail.edit, event.detail.initiator) - ); + const undoEdit = handleEditV2(edit); - const shouldCreateHistoryEntry = event.detail.initiator !== 'redo' && event.detail.initiator !== 'undo'; + const shouldCreateHistoryEntry = event.detail.createHistoryEntry !== false; if (shouldCreateHistoryEntry) { const { title, message } = this.getLogText(edit); this.dispatchEvent(newLogEvent({ kind: 'action', - title, + title: event.detail.title ?? title, message, redo: edit, undo: undoEdit, + squash: event.detail.squash })); } @@ -154,107 +175,6 @@ export class OscdEditor extends LitElement { } } -function handleEdit(edit: Edit): Edit { - if (isInsert(edit)) return handleInsert(edit); - if (isUpdate(edit)) return handleUpdate(edit); - if (isRemove(edit)) return handleRemove(edit); - if (isComplex(edit)) return edit.map(handleEdit).reverse(); - return []; -} - -function localAttributeName(attribute: string): string { - return attribute.includes(':') ? attribute.split(':', 2)[1] : attribute; -} - -function handleInsert({ - parent, - node, - reference, -}: Insert): Insert | Remove | [] { - try { - const { parentNode, nextSibling } = node; - - /** - * This is a workaround for converted edit api v1 events, - * because if multiple edits are converted, they are converted before the changes from the previous edits are applied to the document - * so if you first remove an element and then add a clone with changed attributes, the reference will be the element to remove since it hasnt been removed yet - */ - if (!parent.contains(reference)) { - reference = null; - } - - parent.insertBefore(node, reference); - if (parentNode) - return { - node, - parent: parentNode, - reference: nextSibling, - }; - return { node }; - } catch (e) { - // do nothing if insert doesn't work on these nodes - return []; - } -} - -function handleUpdate({ element, attributes }: Update): Update { - const oldAttributes = { ...attributes }; - Object.entries(attributes) - .reverse() - .forEach(([name, value]) => { - let oldAttribute: AttributeValue; - if (isNamespaced(value!)) - oldAttribute = { - value: element.getAttributeNS( - value.namespaceURI, - localAttributeName(name) - ), - namespaceURI: value.namespaceURI, - }; - else - oldAttribute = element.getAttributeNode(name)?.namespaceURI - ? { - value: element.getAttribute(name), - namespaceURI: element.getAttributeNode(name)!.namespaceURI!, - } - : element.getAttribute(name); - oldAttributes[name] = oldAttribute; - }); - for (const entry of Object.entries(attributes)) { - try { - const [attribute, value] = entry as [string, AttributeValue]; - if (isNamespaced(value)) { - if (value.value === null) - element.removeAttributeNS( - value.namespaceURI, - localAttributeName(attribute) - ); - else element.setAttributeNS(value.namespaceURI, attribute, value.value); - } else if (value === null) element.removeAttribute(attribute); - else element.setAttribute(attribute, value); - } catch (e) { - // do nothing if update doesn't work on this attribute - delete oldAttributes[entry[0]]; - } - } - return { - element, - attributes: oldAttributes, - }; -} - -function handleRemove({ node }: Remove): Insert | [] { - const { parentNode: parent, nextSibling: reference } = node; - node.parentNode?.removeChild(node); - if (parent) - return { - node, - parent, - reference, - }; - return []; -} - function isOpenEnergyEditEvent(event: CustomEvent): boolean { const eventDetail = event.detail as Edit; return isComplex(eventDetail) || isInsert(eventDetail) || isUpdate(eventDetail) || isRemove(eventDetail); diff --git a/packages/openscd/src/addons/History.ts b/packages/openscd/src/addons/History.ts index a6db95729a..2b330facdf 100644 --- a/packages/openscd/src/addons/History.ts +++ b/packages/openscd/src/addons/History.ts @@ -38,7 +38,7 @@ import { import { getFilterIcon, iconColors } from '../icons/icons.js'; import { Plugin } from '../plugin.js'; -import { newEditEvent } from '@openscd/core'; +import { EditV2, isComplexV2, newEditEventV2 } from '@openscd/core'; export const historyStateEvent = 'history-state'; export interface HistoryState { @@ -213,7 +213,7 @@ export class OscdHistory extends LitElement { if (!this.canUndo) return false; const undoEdit = (this.history[this.editCount]).undo; - this.host.dispatchEvent(newEditEvent(undoEdit, 'undo')); + this.host.dispatchEvent(newEditEventV2(undoEdit, { createHistoryEntry: false })); this.setEditCount(this.previousAction); return true; @@ -222,7 +222,7 @@ export class OscdHistory extends LitElement { if (!this.canRedo) return false; const redoEdit = (this.history[this.nextAction]).redo; - this.host.dispatchEvent(newEditEvent(redoEdit, 'redo')); + this.host.dispatchEvent(newEditEventV2(redoEdit, { createHistoryEntry: false })); this.setEditCount(this.nextAction); return true; @@ -238,11 +238,60 @@ export class OscdHistory extends LitElement { this.history.splice(this.nextAction); } - this.history.push(entry); + this.addHistoryEntry(entry); this.setEditCount(this.history.length - 1); this.requestUpdate('history', []); } + private addHistoryEntry(entry: CommitEntry) { + const shouldSquash = Boolean(entry.squash) && this.history.length > 0; + + if (shouldSquash) { + const previousEntry = this.history.pop() as CommitEntry; + const squashedEntry = this.squashHistoryEntries(entry, previousEntry); + this.history.push(squashedEntry); + } else { + this.history.push(entry); + } + } + + private squashHistoryEntries(current: CommitEntry, previous: CommitEntry): CommitEntry { + const undo = this.squashUndo(current.undo, previous.undo); + const redo = this.squashRedo(current.redo, previous.redo); + + return { + ...current, + undo, + redo + }; + } + + private squashUndo(current: EditV2, previous: EditV2): EditV2 { + const isCurrentComplex = isComplexV2(current); + const isPreviousComplex = isComplexV2(previous); + + const previousUndos: EditV2[] = (isPreviousComplex ? previous : [ previous ]) as EditV2[]; + const currentUndos: EditV2[] = (isCurrentComplex ? current : [ current ]) as EditV2[]; + + return [ + ...currentUndos, + ...previousUndos + ]; + } + + private squashRedo(current: EditV2, previous: EditV2): EditV2 { + const isCurrentComplex = isComplexV2(current); + const isPreviousComplex = isComplexV2(previous); + + const previousRedos: EditV2[] = (isPreviousComplex ? previous : [ previous ]) as EditV2[]; + const currentRedos: EditV2[] = (isCurrentComplex ? current : [ current ]) as EditV2[]; + + return [ + ...previousRedos, + ...currentRedos + ]; + } + private onReset() { this.log = []; this.history = []; diff --git a/packages/openscd/src/addons/editor/edit-action-to-v1-converter.ts b/packages/openscd/src/addons/editor/edit-action-to-v1-converter.ts new file mode 100644 index 0000000000..8dadf86bed --- /dev/null +++ b/packages/openscd/src/addons/editor/edit-action-to-v1-converter.ts @@ -0,0 +1,130 @@ +import { + Create, + Delete, + EditorAction, + isCreate, + isDelete, + isMove, + isReplace, + isSimple, + isUpdate, + Move, + Replace, + SimpleAction, + Update +} from '@openscd/core/foundation/deprecated/editor.js'; +import { Edit, Insert, Remove, Update as UpdateV2 } from '@openscd/core'; +import { getReference, SCLTag } from '../../foundation.js'; + + +export function convertEditActiontoV1(action: EditorAction): Edit { + if (isSimple(action)) { + return convertSimpleAction(action); + } else { + return action.actions.map(convertSimpleAction); + } +} + +function convertSimpleAction(action: SimpleAction): Edit { + if (isCreate(action)) { + return convertCreate(action); + } else if (isDelete(action)) { + return convertDelete(action); + } else if (isUpdate(action)) { + return convertUpdate(action); + } else if (isMove(action)) { + return convertMove(action); + } else if (isReplace(action)) { + return convertReplace(action); + } + + throw new Error('Unknown action type'); +} + +function convertCreate(action: Create): Insert { + let reference: Node | null = null; + if ( + action.new.reference === undefined && + action.new.element instanceof Element && + action.new.parent instanceof Element + ) { + reference = getReference( + action.new.parent, + action.new.element.tagName + ); + } else { + reference = action.new.reference ?? null; + } + + return { + parent: action.new.parent, + node: action.new.element, + reference + }; +} + +function convertDelete(action: Delete): Remove { + return { + node: action.old.element + }; +} + +function convertUpdate(action: Update): UpdateV2 { + const oldAttributesToRemove: Record = {}; + Array.from(action.element.attributes).forEach(attr => { + oldAttributesToRemove[attr.name] = null; + }); + + const attributes = { + ...oldAttributesToRemove, + ...action.newAttributes + }; + + return { + element: action.element, + attributes + }; +} + +function convertMove(action: Move): Insert { + if (action.new.reference === undefined) { + action.new.reference = getReference( + action.new.parent, + action.old.element.tagName + ); + } + + return { + parent: action.new.parent, + node: action.old.element, + reference: action.new.reference ?? null + } +} + +function convertReplace(action: Replace): Edit { + const oldChildren = action.old.element.children; + // We have to clone the children, because otherwise undoing the action would remove the children from the old element, because append removes the old parent + const copiedChildren = Array.from(oldChildren).map(e => e.cloneNode(true)); + + const newNode = action.new.element.cloneNode(true) as Element; + newNode.append(...Array.from(copiedChildren)); + const parent = action.old.element.parentElement; + + if (!parent) { + throw new Error('Replace action called without parent in old element'); + } + + const reference = action.old.element.nextSibling; + + const remove: Remove = { node: action.old.element }; + const insert: Insert = { + parent, + node: newNode, + reference + }; + + return [ + remove, + insert + ]; +} diff --git a/packages/openscd/src/addons/editor/edit-v1-to-v2-converter.ts b/packages/openscd/src/addons/editor/edit-v1-to-v2-converter.ts index f33d76d27f..fd5909f4e5 100644 --- a/packages/openscd/src/addons/editor/edit-v1-to-v2-converter.ts +++ b/packages/openscd/src/addons/editor/edit-v1-to-v2-converter.ts @@ -1,130 +1,36 @@ -import { - Create, - Delete, - EditorAction, - isCreate, - isDelete, - isMove, - isReplace, - isSimple, - isUpdate, - Move, - Replace, - SimpleAction, - Update -} from '@openscd/core/foundation/deprecated/editor.js'; -import { Edit, Insert, Remove, Update as UpdateV2 } from '@openscd/core'; -import { getReference, SCLTag } from '../../foundation.js'; - - -export function convertEditV1toV2(action: EditorAction): Edit { - if (isSimple(action)) { - return convertSimpleAction(action); - } else { - return action.actions.map(convertSimpleAction); - } -} - -function convertSimpleAction(action: SimpleAction): Edit { - if (isCreate(action)) { - return convertCreate(action); - } else if (isDelete(action)) { - return convertDelete(action); - } else if (isUpdate(action)) { - return convertUpdate(action); - } else if (isMove(action)) { - return convertMove(action); - } else if (isReplace(action)) { - return convertReplace(action); - } - - throw new Error('Unknown action type'); -} - -function convertCreate(action: Create): Insert { - let reference: Node | null = null; - if ( - action.new.reference === undefined && - action.new.element instanceof Element && - action.new.parent instanceof Element - ) { - reference = getReference( - action.new.parent, - action.new.element.tagName - ); +import { Edit, EditV2, isComplex, isInsert, isNamespaced, isRemove, isUpdate, Update } from '@openscd/core'; + +export function convertEditV1toV2(edit: Edit): EditV2 { + if (isComplex(edit)) { + return edit.map(convertEditV1toV2); + } else if (isRemove(edit)) { + return edit as EditV2; + } else if (isInsert(edit)) { + return edit as EditV2; + } else if (isUpdate(edit)) { + return convertUpdate(edit); } else { - reference = action.new.reference ?? null; + throw new Error('Unknown edit type'); } - - return { - parent: action.new.parent, - node: action.new.element, - reference - }; } -function convertDelete(action: Delete): Remove { - return { - node: action.old.element - }; -} - -function convertUpdate(action: Update): UpdateV2 { - const oldAttributesToRemove: Record = {}; - Array.from(action.element.attributes).forEach(attr => { - oldAttributesToRemove[attr.name] = null; +function convertUpdate(edit: Update): EditV2 { + const attributes: Partial> = {}; + const attributesNS: Partial< + Record>> + > = {}; + + Object.entries(edit.attributes).forEach(([key, value]) => { + if (isNamespaced(value!)) { + const ns = value.namespaceURI; + if (!ns) return; + + if (!attributesNS[ns]) { + attributesNS[ns] = {}; + } + attributesNS[ns]![key] = value.value; + } else attributes[key] = value; }); - const attributes = { - ...oldAttributesToRemove, - ...action.newAttributes - }; - - return { - element: action.element, - attributes - }; -} - -function convertMove(action: Move): Insert { - if (action.new.reference === undefined) { - action.new.reference = getReference( - action.new.parent, - action.old.element.tagName - ); - } - - return { - parent: action.new.parent, - node: action.old.element, - reference: action.new.reference ?? null - } -} - -function convertReplace(action: Replace): Edit { - const oldChildren = action.old.element.children; - // We have to clone the children, because otherwise undoing the action would remove the children from the old element, because append removes the old parent - const copiedChildren = Array.from(oldChildren).map(e => e.cloneNode(true)); - - const newNode = action.new.element.cloneNode(true) as Element; - newNode.append(...Array.from(copiedChildren)); - const parent = action.old.element.parentElement; - - if (!parent) { - throw new Error('Replace action called without parent in old element'); - } - - const reference = action.old.element.nextSibling; - - const remove: Remove = { node: action.old.element }; - const insert: Insert = { - parent, - node: newNode, - reference - }; - - return [ - remove, - insert - ]; + return { element: edit.element, attributes, attributesNS }; } diff --git a/packages/openscd/test/unit/Editor.test.ts b/packages/openscd/test/unit/Editor.test.ts index f26079472a..0b1cbd6d18 100644 --- a/packages/openscd/test/unit/Editor.test.ts +++ b/packages/openscd/test/unit/Editor.test.ts @@ -2,7 +2,17 @@ import { html, fixture, expect } from '@open-wc/testing'; import '../../src/addons/Editor.js'; import { OscdEditor } from '../../src/addons/Editor.js'; -import { Insert, newEditEvent, Remove, Update } from '@openscd/core'; +import { + Insert, + InsertV2, + newEditEvent, + newEditEventV2, + Remove, + Update, + SetAttributesV2, + SetTextContentV2, + RemoveV2 +} from '@openscd/core'; import { CommitDetail, LogDetail } from '@openscd/core/foundation/deprecated/history.js'; @@ -17,6 +27,7 @@ describe('OSCD-Editor', () => { let bay2: Element; let bay4: Element; let bay5: Element; + let bayWithoutTextContent: Element; let lnode1: Element; let lnode2: Element; @@ -39,6 +50,7 @@ describe('OSCD-Editor', () => { + `, 'application/xml', @@ -54,6 +66,7 @@ describe('OSCD-Editor', () => { bay2 = scd.querySelector('Bay[name="b2"]')!; bay4 = scd.querySelector('Bay[name="b4"]')!; bay5 = scd.querySelector('Bay[name="b5"]')!; + bayWithoutTextContent = scd.querySelector('Bay[name="bWithoutTextContent"]')!; lnode1 = scd.querySelector('LNode[name="l1"]')!; lnode2 = scd.querySelector('LNode[name="l2"]')!; }); @@ -63,13 +76,13 @@ describe('OSCD-Editor', () => { const newNode = scd.createElement('Bay'); newNode.setAttribute('name', 'b3'); - const insert: Insert = { + const insert: InsertV2 = { parent: voltageLevel1, node: newNode, reference: null }; - host.dispatchEvent(newEditEvent(insert)); + host.dispatchEvent(newEditEventV2(insert)); const newNodeFromScd = scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b3"]'); @@ -80,13 +93,13 @@ describe('OSCD-Editor', () => { const newNode = scd.createElement('Bay'); newNode.setAttribute('name', 'b3'); - const insert: Insert = { + const insert: InsertV2 = { parent: voltageLevel1, node: newNode, reference: bay1 }; - host.dispatchEvent(newEditEvent(insert)); + host.dispatchEvent(newEditEventV2(insert)); const newNodeFromScd = scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b3"]'); @@ -94,13 +107,13 @@ describe('OSCD-Editor', () => { }); it('should move node when inserting existing node', () => { - const insertMove: Insert = { + const insertMove: InsertV2 = { parent: voltageLevel1, node: bay2, reference: null }; - host.dispatchEvent(newEditEvent(insertMove)); + host.dispatchEvent(newEditEventV2(insertMove)); expect(scd.querySelector('VoltageLevel[name="v2"] > Bay[name="b2"]')).to.be.null; expect(scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b2"]')).to.deep.equal(bay2); @@ -111,12 +124,12 @@ describe('OSCD-Editor', () => { node: bay1 }; - host.dispatchEvent(newEditEvent(remove)); + host.dispatchEvent(newEditEventV2(remove)); expect(scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b1"]')).to.be.null; }); - describe('Update', () => { + describe('SetAttributes', () => { it('should add new attributes and leave old attributes', () => { const bay1NewAttributes = { desc: 'new description', @@ -125,12 +138,13 @@ describe('OSCD-Editor', () => { const oldAttributes = elementAttributesToMap(bay1); - const update: Update = { + const update: SetAttributesV2 = { element: bay1, - attributes: bay1NewAttributes + attributes: bay1NewAttributes, + attributesNS: {} }; - host.dispatchEvent(newEditEvent(update)); + host.dispatchEvent(newEditEventV2(update)); const updatedElement = scd.querySelector('Bay[name="b1"]')!; @@ -147,12 +161,13 @@ describe('OSCD-Editor', () => { kind: null }; - const update: Update = { + const update: SetAttributesV2 = { element: bay1, - attributes: bay1NewAttributes + attributes: bay1NewAttributes, + attributesNS: {} }; - host.dispatchEvent(newEditEvent(update)); + host.dispatchEvent(newEditEventV2(update)); const updatedElement = scd.querySelector('Bay[name="b1"]')!; @@ -168,12 +183,13 @@ describe('OSCD-Editor', () => { const oldAttributes = elementAttributesToMap(bay1); - const update: Update = { + const update: SetAttributesV2 = { element: bay1, - attributes: bay1NewAttributes + attributes: bay1NewAttributes, + attributesNS: {} }; - host.dispatchEvent(newEditEvent(update)); + host.dispatchEvent(newEditEventV2(update)); const updatedElement = scd.querySelector(`Bay[name="${bay1NewAttributes.name}"]`)!; @@ -187,67 +203,77 @@ describe('OSCD-Editor', () => { describe('namespaced attributes', () => { it('should update attribute with namespace', () => { - const update: Update = { + const update: SetAttributesV2 = { element: lnode1, - attributes: { - type: { value: 'newType', namespaceURI: 'xsi' } + attributes: { }, + attributesNS: { + [nsXsi]: { type: 'newType' } } }; - host.dispatchEvent(newEditEvent(update)); + host.dispatchEvent(newEditEventV2(update)); - expect(lnode1.getAttributeNS('xsi', 'type')).to.equal('newType'); + expect(lnode1.getAttributeNS(nsXsi, 'type')).to.equal('newType'); }); it('should handle multiple namespaces', () => { - const update: Update = { + const update: SetAttributesV2 = { element: lnode1, - attributes: { - type: { value: 'newTypeXSI', namespaceURI: nsXsi } + attributes: { }, + attributesNS: { + [nsXsi]: { type: 'newTypeXSI' } } }; - host.dispatchEvent(newEditEvent(update)); + host.dispatchEvent(newEditEventV2(update)); - const update2: Update = { + const update2: SetAttributesV2 = { element: lnode1, - attributes: { - type: { value: 'newTypeTD', namespaceURI: nsTd } + attributes: { }, + attributesNS: { + [nsTd]: { type: 'newTypeTD' } } }; - host.dispatchEvent(newEditEvent(update2)); + host.dispatchEvent(newEditEventV2(update2)); expect(lnode1.getAttributeNS(nsXsi, 'type')).to.equal('newTypeXSI'); expect(lnode1.getAttributeNS(nsTd, 'type')).to.equal('newTypeTD'); }); it('should remove namespaced attribute', () => { - const update: Update = { + const update: SetAttributesV2 = { element: lnode2, - attributes: { - type: { value: null, namespaceURI: nsXsi } + attributes: { }, + attributesNS: { + [nsXsi]: { type: null } } }; - host.dispatchEvent(newEditEvent(update)); + host.dispatchEvent(newEditEventV2(update)); expect(lnode2.getAttributeNS(nsXsi, 'type')).to.be.null; expect(lnode2.getAttributeNS(nsTd, 'type')).to.equal('typeTD'); }); it('should add and remove multiple normal and namespaced attributes', () => { - const update: Update = { + const update: SetAttributesV2 = { element: lnode2, attributes: { - type: { value: null, namespaceURI: nsXsi }, - kind: { value: 'td-kind', namespaceURI: nsTd }, normalAttribute: 'normalValue', lnClass: null + }, + attributesNS: { + [nsXsi]: { + type: null + }, + [nsTd]: { + kind: 'td-kind' + } } }; - host.dispatchEvent(newEditEvent(update)); + host.dispatchEvent(newEditEventV2(update)); expect(lnode2.getAttributeNS(nsXsi, 'type')).to.be.null; expect(lnode2.getAttributeNS(nsTd, 'kind')).to.equal('td-kind'); @@ -256,29 +282,43 @@ describe('OSCD-Editor', () => { }); }); + describe('SetTextContent', () => { + it('should set text content', () => { + const update: SetTextContentV2 = { + element: bay1, + textContent: 'new text' + }; + + host.dispatchEvent(newEditEventV2(update)); + + expect(bay1.textContent).to.equal('new text'); + }); + }); + describe('Complex action', () => { it('should apply each edit from a complex edit', () => { const newNode = scd.createElement('Bay'); newNode.setAttribute('name', 'b3'); - const insert: Insert = { + const insert: InsertV2 = { parent: voltageLevel1, node: newNode, reference: bay1 }; - const remove: Remove = { + const remove: RemoveV2 = { node: bay2 }; - const update: Update = { + const update: SetAttributesV2 = { element: bay1, attributes: { desc: 'new description' - } + }, + attributesNS: {} }; - host.dispatchEvent(newEditEvent([insert, remove, update])); + host.dispatchEvent(newEditEventV2([insert, remove, update])); expect(scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b3"]')).to.deep.equal(newNode); expect(scd.querySelector('VoltageLevel[name="v2"] > Bay[name="b2"]')).to.be.null; @@ -296,12 +336,12 @@ describe('OSCD-Editor', () => { }); }); - it('should log edit for user event', () => { - const remove: Remove = { + it('should log edit by default', () => { + const remove: RemoveV2 = { node: bay2, }; - host.dispatchEvent(newEditEvent(remove, 'user')); + host.dispatchEvent(newEditEventV2(remove)); expect(log).to.have.lengthOf(1); const logEntry = log[0] as CommitDetail; @@ -310,17 +350,6 @@ describe('OSCD-Editor', () => { expect(logEntry.redo).to.deep.equal(remove); }); - it('should not log edit for undo or redo event', () => { - const remove: Remove = { - node: bay2, - }; - - host.dispatchEvent(newEditEvent(remove, 'redo')); - host.dispatchEvent(newEditEvent(remove, 'undo')); - - expect(log).to.have.lengthOf(0); - }); - describe('validate after edit', () => { let hasTriggeredValidate = false; beforeEach(() => { @@ -332,11 +361,11 @@ describe('OSCD-Editor', () => { }); it('should dispatch validate event after edit', async () => { - const remove: Remove = { + const remove: RemoveV2 = { node: bay2, }; - host.dispatchEvent(newEditEvent(remove)); + host.dispatchEvent(newEditEventV2(remove)); await element.updateComplete; @@ -361,75 +390,108 @@ describe('OSCD-Editor', () => { const newNode = scd.createElement('Bay'); newNode.setAttribute('name', 'b3'); - const insert: Insert = { + const insert: InsertV2 = { parent: voltageLevel1, node: newNode, reference: null }; - host.dispatchEvent(newEditEvent(insert)); + host.dispatchEvent(newEditEventV2(insert)); - const undoInsert = log[0].undo as Remove; + const undoInsert = log[0].undo as RemoveV2; - host.dispatchEvent(newEditEvent(undoInsert)); + host.dispatchEvent(newEditEventV2(undoInsert)); expect(scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b3"]')).to.be.null; }); it('should undo remove', () => { - const remove: Remove = { + const remove: RemoveV2 = { node: bay4 }; - host.dispatchEvent(newEditEvent(remove)); + host.dispatchEvent(newEditEventV2(remove)); - const undoRemove = log[0].undo as Insert; + const undoRemove = log[0].undo as InsertV2; - host.dispatchEvent(newEditEvent(undoRemove)); + host.dispatchEvent(newEditEventV2(undoRemove)); const bay4FromScd = scd.querySelector('VoltageLevel[name="v2"] > Bay[name="b4"]'); expect(bay4FromScd).to.deep.equal(bay4); }); - it('should undo update', () => { - const update: Update = { + it('should undo set attributes', () => { + const update: SetAttributesV2 = { element: bay1, attributes: { desc: 'new description', kind: 'superbay' - } + }, + attributesNS: {} }; - host.dispatchEvent(newEditEvent(update)); + host.dispatchEvent(newEditEventV2(update)); - const undoUpdate = log[0].undo as Update; + const undoUpdate = log[0].undo as SetAttributesV2; - host.dispatchEvent(newEditEvent(undoUpdate)); + host.dispatchEvent(newEditEventV2(undoUpdate)); expect(bay1.getAttribute('desc')).to.be.null; expect(bay1.getAttribute('kind')).to.equal('bay'); }); + it('should undo set textcontent', () => { + const update: SetTextContentV2 = { + element: bayWithoutTextContent, + textContent: 'new text' + }; + + host.dispatchEvent(newEditEventV2(update)); + + const undoUpdate = log[0].undo as SetTextContentV2; + + host.dispatchEvent(newEditEventV2(undoUpdate)); + + expect(bayWithoutTextContent.textContent).to.be.empty; + }); + + it('should restore children when undoing set textcontent', () => { + const update: SetTextContentV2 = { + element: bay2, + textContent: 'new text' + }; + + host.dispatchEvent(newEditEventV2(update)); + + expect(bay2.children).to.be.empty; + + const undoUpdate = log[0].undo as SetTextContentV2; + + host.dispatchEvent(newEditEventV2(undoUpdate)); + + expect(bay2.children[0]).to.deep.equal(lnode2); + }); + it('should redo previously undone action', () => { const newNode = scd.createElement('Bay'); newNode.setAttribute('name', 'b3'); - const insert: Insert = { + const insert: InsertV2 = { parent: voltageLevel1, node: newNode, reference: null }; - host.dispatchEvent(newEditEvent(insert)); + host.dispatchEvent(newEditEventV2(insert)); const undoIsert = log[0].undo; const redoInsert = log[0].redo; - host.dispatchEvent(newEditEvent(undoIsert)); + host.dispatchEvent(newEditEventV2(undoIsert)); expect(scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b3"]')).to.be.null; - host.dispatchEvent(newEditEvent(redoInsert)); + host.dispatchEvent(newEditEventV2(redoInsert)); expect(scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b3"]')).to.deep.equal(newNode); }); @@ -438,35 +500,36 @@ describe('OSCD-Editor', () => { const newNode = scd.createElement('Bay'); newNode.setAttribute('name', 'b3'); - const insert: Insert = { + const insert: InsertV2 = { parent: voltageLevel1, node: newNode, reference: bay1 }; - const remove: Remove = { + const remove: RemoveV2 = { node: bay2 }; - const update: Update = { + const update: SetAttributesV2 = { element: bay1, attributes: { desc: 'new description' - } + }, + attributesNS: {} }; - host.dispatchEvent(newEditEvent([insert, remove, update])); + host.dispatchEvent(newEditEventV2([insert, remove, update])); const undoComplex = log[0].undo; const redoComplex = log[0].redo; - host.dispatchEvent(newEditEvent(undoComplex)); + host.dispatchEvent(newEditEventV2(undoComplex)); expect(scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b3"]')).to.be.null; expect(scd.querySelector('VoltageLevel[name="v2"] > Bay[name="b2"]')).to.deep.equal(bay2); expect(bay1.getAttribute('desc')).to.be.null; - host.dispatchEvent(newEditEvent(redoComplex)); + host.dispatchEvent(newEditEventV2(redoComplex)); expect(scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b3"]')).to.deep.equal(newNode); expect(scd.querySelector('VoltageLevel[name="v2"] > Bay[name="b2"]')).to.be.null; diff --git a/packages/openscd/test/unit/edit-action-to-v1-converter.test.ts b/packages/openscd/test/unit/edit-action-to-v1-converter.test.ts new file mode 100644 index 0000000000..b7ef2bc2e4 --- /dev/null +++ b/packages/openscd/test/unit/edit-action-to-v1-converter.test.ts @@ -0,0 +1,148 @@ +import { html, fixture, expect } from '@open-wc/testing'; + +import { + Create, + Delete, + EditorAction, + isCreate, + isDelete, + isMove, + isReplace, + isSimple, + isUpdate, + Move, + Replace, + SimpleAction, + Update, + createUpdateAction +} from '@openscd/core/foundation/deprecated/editor.js'; +import { Edit, Insert, Remove, Update as UpdateV2 } from '@openscd/core'; + +import { convertEditActiontoV1 } from '../../src/addons/editor/edit-action-to-v1-converter.js'; + + +describe('edit-action-to-v1-converter', () => { + const doc = new DOMParser().parseFromString( + ` + + + + + + `, + 'application/xml' + ); + const substation = doc.querySelector('Substation')!; + const substation2 = doc.querySelector('Substation[name="sub2"]')!; + const bay = doc.querySelector('Bay')!; + + it('should convert delete to remove', () => { + const deleteAction: Delete = { + old: { + parent: substation, + element: bay + } + }; + + const remove = convertEditActiontoV1(deleteAction); + + const expectedRemove: Remove = { + node: bay + }; + + expect(remove).to.deep.equal(expectedRemove); + }); + + it('should convert create to insert', () => { + const newBay = doc.createElement('Bay'); + newBay.setAttribute('name', 'bay2'); + + const createAction: Create = { + new: { + parent: substation, + element: newBay + } + }; + + const insert = convertEditActiontoV1(createAction); + + const expectedInsert: Insert = { + parent: substation, + node: newBay, + reference: null + }; + + expect(insert).to.deep.equal(expectedInsert); + }); + + it('should convert update to updateV2', () => { + const newAttributes = { + name: 'newBayName', + }; + const updateAction = createUpdateAction(bay, newAttributes); + + const updateV2 = convertEditActiontoV1(updateAction); + + const expectedUpdateV2: UpdateV2 = { + element: bay, + attributes: { + ...newAttributes, + desc: null + } + }; + + expect(updateV2).to.deep.equal(expectedUpdateV2); + }); + + it('should convert move to insert', () => { + const moveAction: Move = { + old: { + parent: substation, + element: bay, + reference: null + }, + new: { + parent: substation2, + reference: null + } + }; + + const insert = convertEditActiontoV1(moveAction); + + const expectedInsert: Insert = { + parent: substation2, + node: bay, + reference: null + }; + + expect(insert).to.deep.equal(expectedInsert); + }); + + it('should convert replace to complex action with remove and insert', () => { + const ied = doc.createElement('IED'); + ied.setAttribute('name', 'ied'); + + const replace: Replace = { + old: { + element: bay + }, + new: { + element: ied + } + }; + + const [ remove, insert ] = convertEditActiontoV1(replace) as Edit[]; + + const expectedRemove: Remove = { + node: bay + }; + const expectedInsert: Insert = { + parent: substation, + node: ied, + reference: bay.nextSibling + }; + + expect(remove).to.deep.equal(expectedRemove); + expect(insert).to.deep.equal(expectedInsert); + }); +}); diff --git a/packages/openscd/test/unit/edit-v1-to-v2-converter.test.ts b/packages/openscd/test/unit/edit-v1-to-v2-converter.test.ts index 6676a1ffed..da9bf79415 100644 --- a/packages/openscd/test/unit/edit-v1-to-v2-converter.test.ts +++ b/packages/openscd/test/unit/edit-v1-to-v2-converter.test.ts @@ -16,15 +16,17 @@ import { Update, createUpdateAction } from '@openscd/core/foundation/deprecated/editor.js'; -import { Edit, Insert, Remove, Update as UpdateV2 } from '@openscd/core'; +import { Edit, Insert, InsertV2, Remove, Update as UpdateV1, RemoveV2, SetAttributesV2 } from '@openscd/core'; -import { convertEditV1toV2 } from '../../src/addons/editor/edit-v1-to-v2-converter.js'; +import { convertEditV1toV2 } from '../../src/addons/editor/edit-v1-to-v2-converter'; describe('edit-v1-to-v2-converter', () => { + const nsXsi = 'urn:example.com'; + const doc = new DOMParser().parseFromString( ` - + @@ -35,114 +37,90 @@ describe('edit-v1-to-v2-converter', () => { const substation = doc.querySelector('Substation')!; const substation2 = doc.querySelector('Substation[name="sub2"]')!; const bay = doc.querySelector('Bay')!; - - it('should convert delete to remove', () => { - const deleteAction: Delete = { - old: { - parent: substation, - element: bay - } + + it('should keep remove as is', () => { + const remove: Remove = { + node: bay }; - - const remove = convertEditV1toV2(deleteAction); - - const expectedRemove: Remove = { + + const removeV2 = convertEditV1toV2(remove); + + const expectedRemoveV2: RemoveV2 = { node: bay }; - - expect(remove).to.deep.equal(expectedRemove); + + expect(removeV2).to.deep.equal(expectedRemoveV2); }); - it('should convert create to insert', () => { + it('should keep insert as is', () => { const newBay = doc.createElement('Bay'); newBay.setAttribute('name', 'bay2'); - const createAction: Create = { - new: { - parent: substation, - element: newBay - } - }; - - const insert = convertEditV1toV2(createAction); - - const expectedInsert: Insert = { + const insert: Insert = { + node: newBay, parent: substation, + reference: null + }; + + const insertV2 = convertEditV1toV2(insert); + + const expectedInsertV2: InsertV2 = { node: newBay, + parent: substation, reference: null }; - - expect(insert).to.deep.equal(expectedInsert); + + expect(insertV2).to.deep.equal(expectedInsertV2); }); - - it('should convert update to updateV2', () => { + + it('should convert update to set attributes', () => { const newAttributes = { name: 'newBayName', }; - const updateAction = createUpdateAction(bay, newAttributes); - - const updateV2 = convertEditV1toV2(updateAction); - - const expectedUpdateV2: UpdateV2 = { + const update: UpdateV1 = { element: bay, - attributes: { - ...newAttributes, - desc: null - } + attributes: newAttributes + } + + const setAttributesV2 = convertEditV1toV2(update); + + const expectedSetAttributesV2: SetAttributesV2 = { + element: bay, + attributes: newAttributes, + attributesNS: {} }; - - expect(updateV2).to.deep.equal(expectedUpdateV2); + + expect(setAttributesV2).to.deep.equal(expectedSetAttributesV2); }); - it('should convert move to insert', () => { - const moveAction: Move = { - old: { - parent: substation, - element: bay, - reference: null - }, - new: { - parent: substation2, - reference: null + it('shoudl convert update with namespaced attributes', () => { + const newAttributes = { + name: 'newBayName', + type: { + value: 'new value', + namespaceURI: nsXsi } }; - const insert = convertEditV1toV2(moveAction); - - const expectedInsert: Insert = { - parent: substation2, - node: bay, - reference: null - }; - - expect(insert).to.deep.equal(expectedInsert); - }); + const update: UpdateV1 = { + element: bay, + attributes: newAttributes + } - it('should convert replace to complex action with remove and insert', () => { - const ied = doc.createElement('IED'); - ied.setAttribute('name', 'ied'); + const setAttributesV2 = convertEditV1toV2(update); - const replace: Replace = { - old: { - element: bay + const expectedSetAttributesV2: SetAttributesV2 = { + element: bay, + attributes: { + name: 'newBayName' }, - new: { - element: ied + attributesNS: { + [nsXsi]: { + type: 'new value' + } } }; - const [ remove, insert ] = convertEditV1toV2(replace) as Edit[]; - - const expectedRemove: Remove = { - node: bay - }; - const expectedInsert: Insert = { - parent: substation, - node: ied, - reference: bay.nextSibling - }; - - expect(remove).to.deep.equal(expectedRemove); - expect(insert).to.deep.equal(expectedInsert); + expect(setAttributesV2).to.deep.equal(expectedSetAttributesV2); }); }); From 3aef22affe7c7f26b3795ce5cc1945699b554526 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 31 Jan 2025 12:08:30 +0100 Subject: [PATCH 04/28] chore(main): release 0.37.0 (#1612) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 8 ++++++++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 43efde8599..12b331c729 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,5 +1,5 @@ { "packages/openscd": "0.36.0", "packages/core": "0.1.3", - ".": "0.36.0" + ".": "0.37.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 80601e883e..3cae83f4ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [0.37.0](https://github.com/openscd/open-scd/compare/v0.36.0...v0.37.0) (2025-01-27) + + +### Features + +* Edit api v3 ([#1615](https://github.com/openscd/open-scd/issues/1615)) ([ce39e2b](https://github.com/openscd/open-scd/commit/ce39e2b7bfcda40659f36e40659b1efd571f2a53)) +* **monorepo:** Add contributing guide ([#1588](https://github.com/openscd/open-scd/issues/1588)) ([dd692a8](https://github.com/openscd/open-scd/commit/dd692a8d9784aaf5f8509fdad5298293195d1465)) + ## [0.36.0](https://github.com/openscd/open-scd/compare/v0.35.0...v0.36.0) (2024-11-14) diff --git a/package-lock.json b/package-lock.json index 589cc66969..88a41c4e38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "openscd-monorepo", - "version": "0.36.0", + "version": "0.37.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openscd-monorepo", - "version": "0.36.0", + "version": "0.37.0", "license": "Apache-2.0", "workspaces": [ "packages/*" diff --git a/package.json b/package.json index b58f8fa103..6cefbfdfe6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openscd-monorepo", - "version": "0.36.0", + "version": "0.37.0", "description": "OpenSCD base distribution and plugins", "private": true, "workspaces": [ From 65d6f733836f260329f29e388d1ebcaed5a51c55 Mon Sep 17 00:00:00 2001 From: Christopher Lepski <139237321+clepski@users.noreply.github.com> Date: Fri, 31 Jan 2025 15:06:45 +0100 Subject: [PATCH 05/28] chore: Raise core version to 0.1.4 (#1616) * chore: Raise core version to 0.1.4 * chore: Bump uploard-artifact to v4 --- .github/workflows/save-pr-data-to-file.yml | 4 ++-- .release-please-manifest.json | 4 ++-- packages/core/package.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/save-pr-data-to-file.yml b/.github/workflows/save-pr-data-to-file.yml index e216eeef2b..16dbb839e6 100644 --- a/.github/workflows/save-pr-data-to-file.yml +++ b/.github/workflows/save-pr-data-to-file.yml @@ -18,7 +18,7 @@ jobs: run: echo ${{ github.event.pull_request.node_id }} > PR_NODE_ID.txt && echo ${{ github.event.action }} > EVENT_ACTION.txt - name: Archive PR node_id and event_action if: github.event_name == 'pull_request' || github.event_name == 'pull_request_target' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: PR_DATA path: | @@ -29,7 +29,7 @@ jobs: run: echo ${{ github.event.pull_request.node_id }} > PR_NODE_ID.txt && echo ${{ github.event.review.state }} > REVIEW_STATE.txt - name: Archive PR and review_state if: github.event_name == 'pull_request_review' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: REVIEW_DATA path: | diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 12b331c729..b8504b5303 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,5 +1,5 @@ { - "packages/openscd": "0.36.0", - "packages/core": "0.1.3", + "packages/openscd": "0.37.0", + "packages/core": "0.1.4", ".": "0.37.0" } diff --git a/packages/core/package.json b/packages/core/package.json index 8c5eba0ffb..03e5a0953b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@openscd/core", - "version": "0.1.3", + "version": "0.1.4", "description": "The core editor component of open-scd, without any extensions pre-installed.", "author": "Open-SCD", "license": "Apache-2.0", From d3b2a0a7b2d08d0ce5484567ebfe6c6d4e548c5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Russ?= Date: Tue, 4 Feb 2025 15:02:33 +0100 Subject: [PATCH 06/28] Feat: Programatic Plugin Activation (#1611) --- package-lock.json | 2 +- packages/core/foundation.ts | 8 + packages/core/foundation/scl.ts | 89 +++ packages/core/package.json | 3 +- packages/distribution/snowpack.config.mjs | 1 - packages/openscd/src/addons/Layout.ts | 87 ++- packages/openscd/src/open-scd.ts | 549 ++++++++------- packages/openscd/src/plugin.ts | 5 +- .../__snapshots__/open-scd.test.snap.js | 656 ++++++++++++------ packages/openscd/test/unit/Plugging.test.ts | 15 +- 10 files changed, 916 insertions(+), 499 deletions(-) create mode 100644 packages/core/foundation/scl.ts diff --git a/package-lock.json b/package-lock.json index 88a41c4e38..e4af216cc6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30515,7 +30515,7 @@ }, "packages/core": { "name": "@openscd/core", - "version": "0.1.2", + "version": "0.1.3", "license": "Apache-2.0", "dependencies": { "@lit/localize": "^0.11.4", diff --git a/packages/core/foundation.ts b/packages/core/foundation.ts index 4b34b7b25d..80bae0d195 100644 --- a/packages/core/foundation.ts +++ b/packages/core/foundation.ts @@ -58,3 +58,11 @@ export type { EditCompletedEvent, EditCompletedDetail, } from './foundation/edit-completed-event.js'; + +/** @returns the cartesian product of `arrays` */ +export function crossProduct(...arrays: T[][]): T[][] { + return arrays.reduce( + (a, b) => a.flatMap(d => b.map(e => [d, e].flat())), + [[]] + ); +} diff --git a/packages/core/foundation/scl.ts b/packages/core/foundation/scl.ts new file mode 100644 index 0000000000..966d47eadd --- /dev/null +++ b/packages/core/foundation/scl.ts @@ -0,0 +1,89 @@ +import { crossProduct } from '../foundation.js'; + +function getDataModelChildren(parent: Element): Element[] { + if (['LDevice', 'Server'].includes(parent.tagName)) + return Array.from(parent.children).filter( + child => + child.tagName === 'LDevice' || + child.tagName === 'LN0' || + child.tagName === 'LN' + ); + + const id = + parent.tagName === 'LN' || parent.tagName === 'LN0' + ? parent.getAttribute('lnType') + : parent.getAttribute('type'); + + return Array.from( + parent.ownerDocument.querySelectorAll( + `LNodeType[id="${id}"] > DO, DOType[id="${id}"] > SDO, DOType[id="${id}"] > DA, DAType[id="${id}"] > BDA` + ) + ); +} + +export function existFcdaReference(fcda: Element, ied: Element): boolean { + const [ldInst, prefix, lnClass, lnInst, doName, daName, fc] = [ + 'ldInst', + 'prefix', + 'lnClass', + 'lnInst', + 'doName', + 'daName', + 'fc', + ].map(attr => fcda.getAttribute(attr)); + + const sinkLdInst = ied.querySelector(`LDevice[inst="${ldInst}"]`); + if (!sinkLdInst) return false; + + const prefixSelctors = prefix + ? [`[prefix="${prefix}"]`] + : ['[prefix=""]', ':not([prefix])']; + const lnInstSelectors = lnInst + ? [`[inst="${lnInst}"]`] + : ['[inst=""]', ':not([inst])']; + + const anyLnSelector = crossProduct( + ['LN0', 'LN'], + prefixSelctors, + [`[lnClass="${lnClass}"]`], + lnInstSelectors + ) + .map(strings => strings.join('')) + .join(','); + + const sinkAnyLn = ied.querySelector(anyLnSelector); + if (!sinkAnyLn) return false; + + const doNames = doName?.split('.'); + if (!doNames) return false; + + let parent: Element | undefined = sinkAnyLn; + for (const doNameAttr of doNames) { + parent = getDataModelChildren(parent).find( + child => child.getAttribute('name') === doNameAttr + ); + if (!parent) return false; + } + + const daNames = daName?.split('.'); + const someFcInSink = getDataModelChildren(parent).some( + da => da.getAttribute('fc') === fc + ); + if (!daNames && someFcInSink) return true; + if (!daNames) return false; + + let sinkFc = ''; + for (const daNameAttr of daNames) { + parent = getDataModelChildren(parent).find( + child => child.getAttribute('name') === daNameAttr + ); + + if (parent?.getAttribute('fc')) sinkFc = parent.getAttribute('fc')!; + + if (!parent) return false; + } + + if (sinkFc !== fc) return false; + + return true; +} diff --git a/packages/core/package.json b/packages/core/package.json index 03e5a0953b..d2606d6d96 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -13,6 +13,7 @@ ], "exports": { ".": "./dist/foundation.js", + "./foundation/scl.js": "./dist/foundation/scl.js", "./foundation/deprecated/editor.js": "./dist/foundation/deprecated/editor.js", "./foundation/deprecated/open-event.js": "./dist/foundation/deprecated/open-event.js", "./foundation/deprecated/settings.js": "./dist/foundation/deprecated/settings.js", @@ -159,4 +160,4 @@ "prettier --write" ] } -} +} \ No newline at end of file diff --git a/packages/distribution/snowpack.config.mjs b/packages/distribution/snowpack.config.mjs index 6ea30e8b56..cbe594598d 100644 --- a/packages/distribution/snowpack.config.mjs +++ b/packages/distribution/snowpack.config.mjs @@ -1,5 +1,4 @@ export default { - plugins: ['@snowpack/plugin-typescript'], packageOptions: { external: [ '@web/dev-server-core', diff --git a/packages/openscd/src/addons/Layout.ts b/packages/openscd/src/addons/Layout.ts index 41d56a8cd7..68055f25f7 100644 --- a/packages/openscd/src/addons/Layout.ts +++ b/packages/openscd/src/addons/Layout.ts @@ -52,7 +52,8 @@ import '@material/mwc-dialog'; import '@material/mwc-switch'; import '@material/mwc-select'; import '@material/mwc-textfield'; -import { EditCompletedEvent } from '@openscd/core'; +import { nothing } from 'lit'; + @customElement('oscd-layout') export class OscdLayout extends LitElement { @@ -61,6 +62,8 @@ export class OscdLayout extends LitElement { return html`
this.pluginDownloadUI.show()} + @oscd-activate-editor=${this.handleActivateEditorByEvent} + @oscd-run-menu=${this.handleRunMenuByEvent} > ${this.renderHeader()} ${this.renderAside()} ${this.renderContent()} @@ -155,6 +158,7 @@ export class OscdLayout extends LitElement { }, disabled: (): boolean => !this.historyState.canUndo, kind: 'static', + content: () => html``, }, { icon: 'redo', @@ -165,6 +169,7 @@ export class OscdLayout extends LitElement { }, disabled: (): boolean => !this.historyState.canRedo, kind: 'static', + content: () => html``, }, ...validators, { @@ -175,6 +180,7 @@ export class OscdLayout extends LitElement { this.dispatchEvent(newHistoryUIEvent(true, HistoryUIKind.log)); }, kind: 'static', + content: () => html``, }, { icon: 'history', @@ -184,6 +190,7 @@ export class OscdLayout extends LitElement { this.dispatchEvent(newHistoryUIEvent(true, HistoryUIKind.history)); }, kind: 'static', + content: () => html``, }, { icon: 'rule', @@ -193,6 +200,7 @@ export class OscdLayout extends LitElement { this.dispatchEvent(newHistoryUIEvent(true, HistoryUIKind.diagnostic)); }, kind: 'static', + content: () => html``, }, 'divider', ...middleMenu, @@ -203,6 +211,7 @@ export class OscdLayout extends LitElement { this.dispatchEvent(newSettingsUIEvent(true)); }, kind: 'static', + content: () => html``, }, ...bottomMenu, { @@ -210,6 +219,7 @@ export class OscdLayout extends LitElement { name: 'plugins.heading', action: (): void => this.pluginUI.show(), kind: 'static', + content: () => html``, }, ]; } @@ -333,7 +343,10 @@ export class OscdLayout extends LitElement { ); }, disabled: (): boolean => plugin.requireDoc! && this.doc === null, - content: plugin.content, + content: () => { + if(plugin.content){ return plugin.content(); } + return html``; + }, kind: kind, } }) @@ -358,21 +371,24 @@ export class OscdLayout extends LitElement { ); }, disabled: (): boolean => this.doc === null, - content: plugin.content, + content: plugin.content ?? (() => html``), kind: 'validator', } }); } private renderMenuItem(me: MenuItem | 'divider'): TemplateResult { - if (me === 'divider') { return html`
  • `; } - if (me.actionItem){ return html``; } + const isDivider = me === 'divider'; + const hasActionItem = me !== 'divider' && me.actionItem; + if (isDivider) { return html`
  • `; } + if (hasActionItem){ return html``; } return html` ${me.icon} ${get(me.name)} @@ -380,7 +396,7 @@ export class OscdLayout extends LitElement { ? html`${me.hint}` : ''} - ${me.content ?? ''} + ${me.content ? me.content() : nothing} `; } @@ -456,24 +472,32 @@ export class OscdLayout extends LitElement { } + private calcActiveEditors(){ + const hasActiveDoc = Boolean(this.doc); + + return this.editors + .filter(editor => { + // this is necessary because `requireDoc` can be undefined + // and that is not the same as false + const doesNotRequireDoc = editor.requireDoc === false + return doesNotRequireDoc || hasActiveDoc + }) + } + /** Renders the enabled editor plugins and a tab bar to switch between them*/ protected renderContent(): TemplateResult { - const hasActiveDoc = Boolean(this.doc); - const activeEditors = this.editors - .filter(editor => { - // this is necessary because `requireDoc` can be undefined - // and that is not the same as false - const doesNotRequireDoc = editor.requireDoc === false - return doesNotRequireDoc || hasActiveDoc - }) - .map(this.renderEditorTab) + const activeEditors = this.calcActiveEditors() + .map(this.renderEditorTab) const hasActiveEditors = activeEditors.length > 0; if(!hasActiveEditors){ return html``; } return html` - (this.activeTab = e.detail.index)}> + ${activeEditors} ${renderEditorContent(this.editors, this.activeTab, this.doc)} @@ -487,10 +511,39 @@ export class OscdLayout extends LitElement { const content = editor?.content; if(!content) { return html`` } - return html`${content}`; + return html`${content()}`; } } + private handleActivatedEditorTabByUser(e: CustomEvent): void { + const tabIndex = e.detail.index; + this.activateTab(tabIndex); + } + + private handleActivateEditorByEvent(e: CustomEvent<{name: string, src: string}>): void { + const {name, src} = e.detail; + const editors = this.calcActiveEditors() + const wantedEditorIndex = editors.findIndex(editor => editor.name === name || editor.src === src) + if(wantedEditorIndex < 0){ return; } // TODO: log error + + this.activateTab(wantedEditorIndex); + } + + private activateTab(index: number){ + this.activeTab = index; + } + + private handleRunMenuByEvent(e: CustomEvent<{name: string}>): void { + + // TODO: this is a workaround, fix it + this.menuUI.open = true; + const menuEntry = this.menuUI.querySelector(`[data-name="${e.detail.name}"]`) as HTMLElement + const menuElement = menuEntry.nextElementSibling + if(!menuElement){ return; } // TODO: log error + + (menuElement as unknown as MenuPlugin).run() + } + /** * Renders the landing buttons (open project and new project) * it no document loaded we display the menu item that are in the position diff --git a/packages/openscd/src/open-scd.ts b/packages/openscd/src/open-scd.ts index 5c47f77936..6b8eb582d3 100644 --- a/packages/openscd/src/open-scd.ts +++ b/packages/openscd/src/open-scd.ts @@ -1,3 +1,4 @@ +//#region import import { customElement, html, @@ -52,176 +53,45 @@ import { InstalledOfficialPlugin, MenuPosition, PluginKind, Plugin } from "./plu import { ConfigurePluginEvent, ConfigurePluginDetail, newConfigurePluginEvent } from './plugin.events.js'; import { newLogEvent } from '@openscd/core/foundation/deprecated/history'; -// HOSTING INTERFACES +//#endregion import -export interface MenuItem { - icon: string; - name: string; - hint?: string; - actionItem?: boolean; - action?: (event: CustomEvent) => void; - disabled?: () => boolean; - content?: TemplateResult; - kind: string; -} - -export interface Validator { - validate: () => Promise; -} - -export interface MenuPlugin { - run: () => Promise; -} - -export function newResetPluginsEvent(): CustomEvent { - return new CustomEvent('reset-plugins', { bubbles: true, composed: true }); -} - -export interface AddExternalPluginDetail { - plugin: Omit; -} - -export type AddExternalPluginEvent = CustomEvent; - -export function newAddExternalPluginEvent( - plugin: Omit -): AddExternalPluginEvent { - return new CustomEvent('add-external-plugin', { - bubbles: true, - composed: true, - detail: { plugin }, - }); -} - -export interface SetPluginsDetail { - indices: Set; -} - -export type SetPluginsEvent = CustomEvent; - -export function newSetPluginsEvent(indices: Set): SetPluginsEvent { - return new CustomEvent('set-plugins', { - bubbles: true, - composed: true, - detail: { indices }, - }); -} - -// PLUGGING INTERFACES -const pluginTags = new Map(); -/** - * Hashes `uri` using cyrb64 analogous to - * https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js . - * @returns a valid customElement tagName containing the URI hash. - */ -function pluginTag(uri: string): string { - if (!pluginTags.has(uri)) { - let h1 = 0xdeadbeef, - h2 = 0x41c6ce57; - for (let i = 0, ch; i < uri.length; i++) { - ch = uri.charCodeAt(i); - h1 = Math.imul(h1 ^ ch, 2654435761); - h2 = Math.imul(h2 ^ ch, 1597334677); - } - h1 = - Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ - Math.imul(h2 ^ (h2 >>> 13), 3266489909); - h2 = - Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ - Math.imul(h1 ^ (h1 >>> 13), 3266489909); - pluginTags.set( - uri, - 'oscd-plugin' + - ((h2 >>> 0).toString(16).padStart(8, '0') + - (h1 >>> 0).toString(16).padStart(8, '0')) - ); - } - return pluginTags.get(uri)!; -} - -/** - * This is a template literal tag function. See: - * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates - * - * Passes its arguments to LitElement's `html` tag after combining the first and - * last expressions with the first two and last two static strings. - * Throws unless the first and last expressions are identical strings. - * - * We need this to get around the expression location limitations documented in - * https://lit.dev/docs/templates/expressions/#expression-locations - * - * After upgrading to Lit 2 we can use their static HTML functions instead: - * https://lit.dev/docs/api/static-html/ - */ -function staticTagHtml( - oldStrings: ReadonlyArray, - ...oldArgs: unknown[] -): TemplateResult { - const args = [...oldArgs]; - const firstArg = args.shift(); - const lastArg = args.pop(); - - if (firstArg !== lastArg) - throw new Error( - `Opening tag <${firstArg}> does not match closing tag .` - ); - - const strings = [...oldStrings] as string[] & { raw: string[] }; - const firstString = strings.shift(); - const secondString = strings.shift(); - - const lastString = strings.pop(); - const penultimateString = strings.pop(); - - strings.unshift(`${firstString}${firstArg}${secondString}`); - strings.push(`${penultimateString}${lastArg}${lastString}`); - - return html(strings, ...args); -} - - -function withoutContent

    ( - plugin: P -): P { - return { ...plugin, content: undefined }; -} - -export const pluginIcons: Record = { - editor: 'tab', - menu: 'play_circle', - validator: 'rule_folder', - top: 'play_circle', - middle: 'play_circle', - bottom: 'play_circle', -}; - -const menuOrder: (PluginKind | MenuPosition)[] = [ - 'editor', - 'top', - 'validator', - 'middle', - 'bottom', -]; - -function menuCompare(a: Plugin, b: Plugin): -1 | 0 | 1 { - if (a.kind === b.kind && a.position === b.position) return 0; - const earlier = menuOrder.find(kind => - [a.kind, b.kind, a.position, b.position].includes(kind) - ); - return [a.kind, a.position].includes(earlier) ? -1 : 1; -} - -function compareNeedsDoc(a: Plugin, b: Plugin): -1 | 0 | 1 { - if (a.requireDoc === b.requireDoc) return 0; - return a.requireDoc ? 1 : -1; -} - -const loadedPlugins = new Set(); /** The `` custom element is the main entry point of the * Open Substation Configuration Designer. */ @customElement('open-scd') export class OpenSCD extends LitElement { + + render(): TemplateResult { + return html` + + + + + + + + + + + `; + } + + @property({ attribute: false }) doc: XMLDocument | null = null; /** The name of the current [[`doc`]] */ @@ -234,7 +104,7 @@ export class OpenSCD extends LitElement { editCount: -1, canRedo: false, canUndo: false, - } + } /** Object containing all *.nsdoc files and a function extracting element's label form them*/ @property({ attribute: false }) @@ -311,57 +181,22 @@ export class OpenSCD extends LitElement { connectedCallback(): void { super.connectedCallback(); + this.loadPlugins() + + // TODO: let Lit handle the event listeners, move to render() this.addEventListener('reset-plugins', this.resetPlugins); this.addEventListener('set-plugins', (e: SetPluginsEvent) => { this.setPlugins(e.detail.indices); }); - - this.updatePlugins(); - this.requestUpdate(); - this.addEventListener(historyStateEvent, (e: CustomEvent) => { this.historyState = e.detail; this.requestUpdate(); }); } - render(): TemplateResult { - return html` - - - - - - - - - - - `; - } - private storePlugins(plugins: Array) { - localStorage.setItem( - 'plugins', - JSON.stringify(plugins.map(withoutContent)) - ); - this.requestUpdate(); - } + + /** * @@ -421,9 +256,8 @@ export class OpenSCD extends LitElement { this.storePlugins( (builtinPlugins as Plugin[]).concat(this.parsedPlugins).map(plugin => { return { - src: plugin.src, + ...plugin, installed: plugin.default ?? false, - official: true, }; }) ); @@ -462,42 +296,49 @@ export class OpenSCD extends LitElement { return allPlugnis } - private get sortedStoredPlugins(): Plugin[] { - const mergedPlugins = this.storedPlugins.map(plugin => { - if (!plugin.official){ return plugin }; + @state() private storedPlugins: Plugin[] = []; + private updateStoredPlugins(newPlugins: Plugin[]) { + // + // Generate content of each plugin + // + const plugins = newPlugins.map(plugin => { + const isInstalled = plugin.src && plugin.installed + if(!isInstalled) { return plugin } + + return this.addContent(plugin) + }) + + // + // Merge built-in plugins + // + const mergedPlugins = plugins.map(plugin => { + const isBuiltIn = !plugin?.official + if (!isBuiltIn){ return plugin }; - const officialPlugin = (builtinPlugins as Plugin[]) - .concat(this.parsedPlugins) - .find(needle => needle.src === plugin.src); + const builtInPlugin = [...builtinPlugins, ...this.parsedPlugins] + .find(p => p.src === plugin.src); return { - ...officialPlugin, + ...builtInPlugin, ...plugin, }; }) - - return mergedPlugins - .sort(compareNeedsDoc) - .sort(menuCompare); + this.storePlugins(mergedPlugins); } - private get storedPlugins(): Plugin[] { + private storePlugins(plugins: Plugin[]) { + this.storedPlugins = plugins + const pluginConfigs = JSON.stringify(plugins.map(withoutContent)) + localStorage.setItem('plugins', pluginConfigs); + } + private getPluginConfigsFromLocalStorage(): Plugin[] { const pluginsConfigStr = localStorage.getItem('plugins') ?? '[]' - const storedPlugins = JSON.parse(pluginsConfigStr) as Plugin[] - - const plugins = storedPlugins.map(plugin => { - const isInstalled = plugin.src && plugin.installed - if(!isInstalled) { return plugin } - - return this.addContent(plugin) - }) - - return plugins - + return JSON.parse(pluginsConfigStr) as Plugin[] } + protected get locale(): string { return navigator.language || 'en-US'; } @@ -513,43 +354,44 @@ export class OpenSCD extends LitElement { } private setPlugins(indices: Set) { - const newPlugins = this.sortedStoredPlugins.map((plugin, index) => { + const newPlugins = this.storedPlugins.map((plugin, index) => { return { ...plugin, installed: indices.has(index) }; }); - this.storePlugins(newPlugins); + this.updateStoredPlugins(newPlugins); } - private updatePlugins() { + private loadPlugins(){ + const localPluginConfigs = this.getPluginConfigsFromLocalStorage() - const stored: Plugin[] = this.storedPlugins; - const officialStored = stored.filter(p => p.official); - const newOfficial: Array = ( - builtinPlugins as Plugin[] - ) - .concat(this.parsedPlugins) - .filter(p => !officialStored.find(o => o.src === p.src)) - .map(plugin => { - return { - src: plugin.src, - installed: plugin.default ?? false, - official: true as const, - }; - }); + const overwritesOfBultInPlugins = localPluginConfigs.filter((p) => { + return builtinPlugins.some(b => b.src === p.src) + }) - const oldOfficial = officialStored.filter( - p => - !(builtinPlugins 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)); - this.storePlugins(newPlugins); + const userInstalledPlugins = localPluginConfigs.filter((p) => { + return !builtinPlugins.some(b => b.src === p.src) + }) + + const mergedBuiltInPlugins = builtinPlugins.map((builtInPlugin) => { + const noopOverwrite = {} + const overwrite = overwritesOfBultInPlugins + .find(p => p.src === builtInPlugin.src) + ?? noopOverwrite + + return { + ...builtInPlugin, + ...overwrite, + installed: true, // TODO: is this correct? should we decide it based on something? + } + }) + + const mergedPlugins = [...mergedBuiltInPlugins, ...userInstalledPlugins] + + // TODO: kind is string and enum, figour out later + // @ts-expect-error + this.updateStoredPlugins(mergedPlugins) } private async addExternalPlugin( @@ -563,19 +405,22 @@ export class OpenSCD extends LitElement { } private addContent(plugin: Omit): Plugin { - const tag = pluginTag(plugin.src); + const tag = this.pluginTag(plugin.src); - if (!loadedPlugins.has(tag)) { - loadedPlugins.add(tag); - import(plugin.src).then(mod => customElements.define(tag, mod.default)); + if (!this.loadedPlugins.has(tag)) { + this.loadedPlugins.add(tag); + import(plugin.src).then((mod) => { + customElements.define(tag, mod.default) + }) } - return { ...plugin, - content: staticTagHtml`<${tag} + content: () => { + return staticTagHtml`<${tag} .doc=${this.doc} .docName=${this.docName} .editCount=${this.historyState.editCount} + .plugins=${this.storedPlugins} .docId=${this.docId} .pluginId=${plugin.src} .nsdoc=${this.nsdoc} @@ -587,9 +432,46 @@ export class OpenSCD extends LitElement { validator: plugin.kind === 'validator', editor: plugin.kind === 'editor', })}" - >`, + >` + }, }; } + + @state() private loadedPlugins = new Set(); + + // PLUGGING INTERFACES + @state() private pluginTags = new Map(); + /** + * Hashes `uri` using cyrb64 analogous to + * https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js . + * @returns a valid customElement tagName containing the URI hash. + */ + private pluginTag(uri: string): string { + if (!this.pluginTags.has(uri)) { + let h1 = 0xdeadbeef, + h2 = 0x41c6ce57; + for (let i = 0, ch; i < uri.length; i++) { + ch = uri.charCodeAt(i); + h1 = Math.imul(h1 ^ ch, 2654435761); + h2 = Math.imul(h2 ^ ch, 1597334677); + } + h1 = + Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ + Math.imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = + Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ + Math.imul(h1 ^ (h1 >>> 13), 3266489909); + this.pluginTags.set( + uri, + 'oscd-plugin' + + ((h2 >>> 0).toString(16).padStart(8, '0') + + (h1 >>> 0).toString(16).padStart(8, '0')) + ); + } + return this.pluginTags.get(uri)!; + } + + } declare global { @@ -599,3 +481,140 @@ declare global { 'set-plugins': CustomEvent; } } + + +// HOSTING INTERFACES + +export interface MenuItem { + icon: string; + name: string; + hint?: string; + actionItem?: boolean; + action?: (event: CustomEvent) => void; + disabled?: () => boolean; + content: () => TemplateResult; + kind: string; +} + +export interface Validator { + validate: () => Promise; +} + +export interface MenuPlugin { + run: () => Promise; +} + +export function newResetPluginsEvent(): CustomEvent { + return new CustomEvent('reset-plugins', { bubbles: true, composed: true }); +} + +export interface AddExternalPluginDetail { + plugin: Omit; +} + +export type AddExternalPluginEvent = CustomEvent; + +export function newAddExternalPluginEvent( + plugin: Omit +): AddExternalPluginEvent { + return new CustomEvent('add-external-plugin', { + bubbles: true, + composed: true, + detail: { plugin }, + }); +} + +export interface SetPluginsDetail { + indices: Set; +} + +export type SetPluginsEvent = CustomEvent; + +export function newSetPluginsEvent(indices: Set): SetPluginsEvent { + return new CustomEvent('set-plugins', { + bubbles: true, + composed: true, + detail: { indices }, + }); +} + + + + +/** + * This is a template literal tag function. See: + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates + * + * Passes its arguments to LitElement's `html` tag after combining the first and + * last expressions with the first two and last two static strings. + * Throws unless the first and last expressions are identical strings. + * + * We need this to get around the expression location limitations documented in + * https://lit.dev/docs/templates/expressions/#expression-locations + * + * After upgrading to Lit 2 we can use their static HTML functions instead: + * https://lit.dev/docs/api/static-html/ + */ +function staticTagHtml( + oldStrings: ReadonlyArray, + ...oldArgs: unknown[] +): TemplateResult { + const args = [...oldArgs]; + const firstArg = args.shift(); + const lastArg = args.pop(); + + if (firstArg !== lastArg) + throw new Error( + `Opening tag <${firstArg}> does not match closing tag .` + ); + + const strings = [...oldStrings] as string[] & { raw: string[] }; + const firstString = strings.shift(); + const secondString = strings.shift(); + + const lastString = strings.pop(); + const penultimateString = strings.pop(); + + strings.unshift(`${firstString}${firstArg}${secondString}`); + strings.push(`${penultimateString}${lastArg}${lastString}`); + + return html(strings, ...args); +} + + +function withoutContent

    ( + plugin: P +): P { + return { ...plugin, content: undefined }; +} + +export const pluginIcons: Record = { + editor: 'tab', + menu: 'play_circle', + validator: 'rule_folder', + top: 'play_circle', + middle: 'play_circle', + bottom: 'play_circle', +}; + +const menuOrder: (PluginKind | MenuPosition)[] = [ + 'editor', + 'top', + 'validator', + 'middle', + 'bottom', +]; + +function menuCompare(a: Plugin, b: Plugin): -1 | 0 | 1 { + if (a.kind === b.kind && a.position === b.position) return 0; + const earlier = menuOrder.find(kind => + [a.kind, b.kind, a.position, b.position].includes(kind) + ); + return [a.kind, a.position].includes(earlier) ? -1 : 1; +} + +function compareNeedsDoc(a: Plugin, b: Plugin): -1 | 0 | 1 { + if (a.requireDoc === b.requireDoc) return 0; + return a.requireDoc ? 1 : -1; +} + diff --git a/packages/openscd/src/plugin.ts b/packages/openscd/src/plugin.ts index 4b9dfba41e..60e4fc3a02 100644 --- a/packages/openscd/src/plugin.ts +++ b/packages/openscd/src/plugin.ts @@ -10,10 +10,10 @@ export type Plugin = { position?: MenuPosition; installed: boolean; official?: boolean; - content?: TemplateResult; + content?: () => TemplateResult; }; -export type InstalledOfficialPlugin = { +export type InstalledOfficialPlugin = Plugin & { src: string; official: true; installed: boolean; @@ -23,3 +23,4 @@ export type InstalledOfficialPlugin = { export type PluginKind = 'editor' | 'menu' | 'validator'; export const menuPosition = ['top', 'middle', 'bottom'] as const; export type MenuPosition = (typeof menuPosition)[number]; + diff --git a/packages/openscd/test/integration/__snapshots__/open-scd.test.snap.js b/packages/openscd/test/integration/__snapshots__/open-scd.test.snap.js index a43b37e707..2029591739 100644 --- a/packages/openscd/test/integration/__snapshots__/open-scd.test.snap.js +++ b/packages/openscd/test/integration/__snapshots__/open-scd.test.snap.js @@ -85,6 +85,7 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like + + developer_board + + + Create Virtual IED + + + + + + + play_circle + + + Update desc (ABB) + + + + + + + play_circle + + + Update desc (SEL) + + + + + + + + sim_card_download + + + Export Communication Section + + + +

  • - help + history_toggle_off - Help + Show SCL History - - + + - history_toggle_off + help - Show SCL History + Help - - + +
  • @@ -448,11 +539,12 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like @@ -463,11 +555,12 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like @@ -478,7 +571,7 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like @@ -509,11 +603,12 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like @@ -524,7 +619,7 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like @@ -571,7 +667,7 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like @@ -602,11 +699,12 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like @@ -640,7 +738,7 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like
  • - - link - - Top Mock Plugin - - - - link - - Middle Mock Plugin - - @@ -793,7 +862,7 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like @@ -824,11 +894,12 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like @@ -839,7 +910,7 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like @@ -908,22 +980,23 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like - link + history_toggle_off - Bottom Mock Plugin + Show SCL History Help - - - history_toggle_off - - Show SCL History - + + developer_board + + + Create Virtual IED + + + + + + + play_circle + + + Update desc (ABB) + + + + + + + play_circle + + + Update desc (SEL) + + + + + + + + sim_card_download + + + Export Communication Section + + + +
  • - help + history_toggle_off - Help + Show SCL History - - + + - history_toggle_off + help - Show SCL History + Help - - + +
  • @@ -1545,11 +1693,12 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik @@ -1560,11 +1709,12 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik @@ -1575,7 +1725,7 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik @@ -1606,11 +1757,12 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik @@ -1621,7 +1773,7 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik @@ -1668,7 +1821,7 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik @@ -1699,11 +1853,12 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik @@ -1712,21 +1867,6 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik Cleanup - - - link - - Mock Editor Plugin - @@ -1875,7 +2016,7 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik @@ -1906,11 +2048,12 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik @@ -1921,7 +2064,7 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik @@ -1990,35 +2134,35 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik
  • - help + history_toggle_off - Help + Show SCL History - history_toggle_off + help - Show SCL History + Help + + developer_board + + + Create Virtual IED + +
    + + + + + play_circle + + + Update desc (ABB) + + + + + + + play_circle + + + Update desc (SEL) + + + + + + + + sim_card_download + + + Export Communication Section + + + +
  • - help + history_toggle_off - Help + Show SCL History - - + + - history_toggle_off + help - Show SCL History + Help - - + +
  • @@ -2612,11 +2847,12 @@ snapshots["open-scd layout looks like its snapshot"] = @@ -2627,11 +2863,12 @@ snapshots["open-scd layout looks like its snapshot"] = @@ -2642,7 +2879,7 @@ snapshots["open-scd layout looks like its snapshot"] = @@ -2673,11 +2911,12 @@ snapshots["open-scd layout looks like its snapshot"] = @@ -2688,7 +2927,7 @@ snapshots["open-scd layout looks like its snapshot"] = @@ -2735,7 +2975,7 @@ snapshots["open-scd layout looks like its snapshot"] = @@ -2766,11 +3007,12 @@ snapshots["open-scd layout looks like its snapshot"] = @@ -2804,7 +3046,7 @@ snapshots["open-scd layout looks like its snapshot"] =
  • @@ -2927,7 +3170,7 @@ snapshots["open-scd layout looks like its snapshot"] = @@ -2958,11 +3202,12 @@ snapshots["open-scd layout looks like its snapshot"] = @@ -2973,7 +3218,7 @@ snapshots["open-scd layout looks like its snapshot"] = @@ -3042,35 +3288,35 @@ snapshots["open-scd layout looks like its snapshot"] = - help + history_toggle_off - Help + Show SCL History - history_toggle_off + help - Show SCL History + Help { }); it('stores default plugins on load', () =>{ - expect(element.layout).property('editors').to.have.lengthOf(6) + expect(element.layout).property('editors').to.have.lengthOf(14) }); it('has Locale property', async () => { @@ -66,7 +66,7 @@ describe('OpenSCD-Plugin', () => { it('disables deselected plugins', async () => { firstEditorPlugin.click(); await element.updateComplete; - expect(element.layout).property('editors').to.have.lengthOf(5); + expect(element.layout).property('editors').to.have.lengthOf(13); }); it('enables selected plugins', async () => { @@ -74,7 +74,7 @@ describe('OpenSCD-Plugin', () => { await element.updateComplete; (element.layout.pluginList.firstElementChild).click(); await element.updateComplete; - expect(element.layout).property('editors').to.have.lengthOf(6); + expect(element.layout).property('editors').to.have.lengthOf(14); }); it('resets plugins to default on reset button click', async () => { @@ -173,7 +173,7 @@ describe('OpenSCD-Plugin', () => { await name.updateComplete; primaryAction.click(); await element.updateComplete; - expect(element.layout.editors).to.have.lengthOf(7); + expect(element.layout.editors).to.have.lengthOf(15); }); it('adds a new menu kind plugin on add button click', async () => { @@ -374,7 +374,7 @@ describe('OpenSCD-Plugin', () => { name: "plugin to remove, but wrong kind", kind: "editor", src: "https://example.com/plugin-to-remove.js", - installed: false, + installed: true, }], eventDetails: { name: "plugin to remove, but wrong kind", @@ -385,7 +385,7 @@ describe('OpenSCD-Plugin', () => { name: "plugin to remove, but wrong kind", kind: "editor", src: "https://example.com/plugin-to-remove.js", - installed: false, + installed: true, }] }, ] @@ -396,7 +396,7 @@ describe('OpenSCD-Plugin', () => { it(tc.desc, async () => { // ARRANGE - // @ts-ignore: we use the private to arrange the scenario + // @ts-ignore: we use the private function to arrange the scenario element.storePlugins(tc.currentPlugins) await element.updateComplete @@ -431,3 +431,4 @@ describe('OpenSCD-Plugin', () => { }) }); + From dc7832968512f66875360b81f065faddf6b4d1cd Mon Sep 17 00:00:00 2001 From: David Monichi Date: Wed, 5 Feb 2025 20:32:20 +0100 Subject: [PATCH 07/28] Add decisions made for externalizing OpenSCD plugins (#1599) * Added ADR-0003 - Externalize OpenSCD core plugins * Added ADR-0004 - Technical solution for releasing and deployments --- docs/decisions/0003-extract-plugins.md | 36 +++++++++++++++++++ ...openscd-release-and-deployment-strategy.md | 32 +++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 docs/decisions/0003-extract-plugins.md create mode 100644 docs/decisions/0004-openscd-release-and-deployment-strategy.md diff --git a/docs/decisions/0003-extract-plugins.md b/docs/decisions/0003-extract-plugins.md new file mode 100644 index 0000000000..230710cd8a --- /dev/null +++ b/docs/decisions/0003-extract-plugins.md @@ -0,0 +1,36 @@ +# ADR-0003 - Externalize OpenSCD core plugins + +Date: 2024-11-19 + +## Status + +Approved + +## Context + +For a better expandability we would like to extract all plugins in a new plugins repository. + +## Decision + +Following the architectural decision in [OpenSCD Theming](./../0001-ADR-Theming.md) we will extract all OpenSCD Core plugins to an external repository. +Doing so OpenSCD Core will be streamlined and a clean interface and structure for plugins will be provided for custom extensions. +Before extracting this plugins a shared UI-Components module will be extracted. This UI-Components provide reusable UI-Components based on [NX](https://nx.dev/) for faster development for OpenSCD Core and custom plugins. This new repository will be created as mono repository to facility the plugins development and simplify the release and deployment process. + +Plugins will be moved to repository [OpenSCD official Plugins](https://github.com/openscd/oscd-official-plugins) and the release strategy is defined [here](./0004-openscd-release-and-deploy-strategy.md). +As final task the current documentation will be added with a new section `How to add new and custom OpenSCD plugins` to support developers to follow the concept. + +## Consequences + +- Clean Code in OpenSCD Core +- Clear architectural structure of plugins + +- Building OpenSCD is more then building a simple repository +- Clear path must be defined how to extend OpenSCD with custom plugins (full software cycle till deployment) +- Release process for OpenSCD Core and OpenSCD official plugins + +## Agreed procedure + +- move the plugins without any components abstraction to the external plugins repository + - copy all required dependencies regardless of code duplication +- integrate the plugins as submodules within OpenSCD core in the pipeline +- later on we can extract step by step for each plugin UI-Components diff --git a/docs/decisions/0004-openscd-release-and-deployment-strategy.md b/docs/decisions/0004-openscd-release-and-deployment-strategy.md new file mode 100644 index 0000000000..1c186a4d5c --- /dev/null +++ b/docs/decisions/0004-openscd-release-and-deployment-strategy.md @@ -0,0 +1,32 @@ +# ADR-0004 - Technical solution for releasing and deployments + +Date: 2024-11-19 + +## Status + +Approved + +## Context + +Based on the [decision](./0003-extract-plugins.md) to externalize plugins in proper plugins repository a new release and deployment strategy needs to be defined. +This plugins repository is solved as mono repository. + +## Decision + +### Release process + +Since OpenSCD is based on [NX](https://nx.dev/) the release strategy needs to rely on NX as well and must allow single releases of sub modules within this mono repository. +Such feature is provided by [NX release](https://nx.dev/recipes/nx-release) specially when using the [NX independently release feature](https://nx.dev/recipes/nx-release/release-projects-independently). + +A possible release command would look like: +``` + nx release --projects=plugin-1,plugin-3 +``` + +## Consequences + +- Process needs to be documented so that all developers can easily follow it +- The building of complete OpenSCD Editor, OpenSCD Core + OpenSCD plugins, depends now on two repositories +- Custom OpenSCD eg. CoMPAS OpenSCD will be cleaner and more code can be reused +- Similar Look & Feel of plugins if shared UI-Components are used +- Faster plugin development and integration into OpenSCD Core From c39cd6cffde6fb37a68f2bf71b3cc74adeafd6b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Russ?= Date: Thu, 6 Feb 2025 14:48:31 +0100 Subject: [PATCH 08/28] fix: openscd forgets disabled plugins (#1618) --- package-lock.json | 117 ++++- packages/openscd/package.json | 2 +- packages/openscd/src/open-scd.ts | 14 +- packages/openscd/src/plugins.ts | 2 +- packages/openscd/test/mock-open-scd.ts | 18 +- packages/openscd/test/mock-plugins.ts | 256 +++++++++++ packages/openscd/test/unit/Plugging.test.ts | 433 +++++++++++++++--- packages/openscd/web-test-runner.config.mjs | 2 +- .../validators/ValidateSchema.test.ts | 290 +++++++++++- .../validators/ValidateTemplates.test.ts | 293 +++++++++++- packages/plugins/web-test-runner.config.mjs | 13 +- 11 files changed, 1359 insertions(+), 81 deletions(-) create mode 100644 packages/openscd/test/mock-plugins.ts diff --git a/package-lock.json b/package-lock.json index e4af216cc6..ff33c6e2ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2604,6 +2604,12 @@ "node": ">=6.9.0" } }, + "node_modules/@import-maps/resolve": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@import-maps/resolve/-/resolve-1.0.1.tgz", + "integrity": "sha512-tWZNBIS1CoekcwlMuyG2mr0a1Wo5lb5lEHwwWvZo+5GLgr3e9LLDTtmgtCWEwBpXMkxn9D+2W9j2FY6eZQq0tA==", + "dev": true + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -8725,6 +8731,114 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/@web/dev-server-import-maps": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@web/dev-server-import-maps/-/dev-server-import-maps-0.2.1.tgz", + "integrity": "sha512-iGM7s4qenmTDUWC2iV0HoQ1NR5lAyRxVHOpWzTsFH/TnUZzP+YuL6QIFtB2v2v7URfhGL2l2WPIibmliToITcg==", + "dev": true, + "dependencies": { + "@import-maps/resolve": "^1.0.1", + "@types/parse5": "^6.0.1", + "@web/dev-server-core": "^0.7.2", + "@web/parse5-utils": "^2.1.0", + "parse5": "^6.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@web/dev-server-import-maps/node_modules/@web/dev-server-core": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-0.7.5.tgz", + "integrity": "sha512-Da65zsiN6iZPMRuj4Oa6YPwvsmZmo5gtPWhW2lx3GTUf5CAEapjVpZVlUXnKPL7M7zRuk72jSsIl8lo+XpTCtw==", + "dev": true, + "dependencies": { + "@types/koa": "^2.11.6", + "@types/ws": "^7.4.0", + "@web/parse5-utils": "^2.1.0", + "chokidar": "^4.0.1", + "clone": "^2.1.2", + "es-module-lexer": "^1.0.0", + "get-stream": "^6.0.0", + "is-stream": "^2.0.0", + "isbinaryfile": "^5.0.0", + "koa": "^2.13.0", + "koa-etag": "^4.0.0", + "koa-send": "^5.0.1", + "koa-static": "^5.0.0", + "lru-cache": "^8.0.4", + "mime-types": "^2.1.27", + "parse5": "^6.0.1", + "picomatch": "^2.2.2", + "ws": "^7.5.10" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@web/dev-server-import-maps/node_modules/@web/parse5-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-2.1.0.tgz", + "integrity": "sha512-GzfK5disEJ6wEjoPwx8AVNwUe9gYIiwc+x//QYxYDAFKUp4Xb1OJAGLc2l2gVrSQmtPGLKrTRcW90Hv4pEq1qA==", + "dev": true, + "dependencies": { + "@types/parse5": "^6.0.1", + "parse5": "^6.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@web/dev-server-import-maps/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@web/dev-server-import-maps/node_modules/es-module-lexer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "dev": true + }, + "node_modules/@web/dev-server-import-maps/node_modules/lru-cache": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", + "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==", + "dev": true, + "engines": { + "node": ">=16.14" + } + }, + "node_modules/@web/dev-server-import-maps/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + }, + "node_modules/@web/dev-server-import-maps/node_modules/readdirp": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", + "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", + "dev": true, + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@web/dev-server-rollup": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@web/dev-server-rollup/-/dev-server-rollup-0.4.1.tgz", @@ -30515,7 +30629,7 @@ }, "packages/core": { "name": "@openscd/core", - "version": "0.1.3", + "version": "0.1.4", "license": "Apache-2.0", "dependencies": { "@lit/localize": "^0.11.4", @@ -31580,6 +31694,7 @@ "@typescript-eslint/eslint-plugin": "^4.29.2", "@typescript-eslint/parser": "^4.29.2", "@web/dev-server-esbuild": "^0.2.16", + "@web/dev-server-import-maps": "^0.2.1", "@web/test-runner": "^0.13.22", "@web/test-runner-playwright": "^0.11.0", "concurrently": "^6.2.1", diff --git a/packages/openscd/package.json b/packages/openscd/package.json index fc084cde7c..2c57aedc35 100644 --- a/packages/openscd/package.json +++ b/packages/openscd/package.json @@ -172,4 +172,4 @@ ], "commitUrlFormat": "https://github.com/openscd/open-scd/commits/{{hash}}" } -} \ No newline at end of file +} diff --git a/packages/openscd/src/open-scd.ts b/packages/openscd/src/open-scd.ts index 6b8eb582d3..d4edf5e74a 100644 --- a/packages/openscd/src/open-scd.ts +++ b/packages/openscd/src/open-scd.ts @@ -254,7 +254,7 @@ export class OpenSCD extends LitElement { private resetPlugins(): void { this.storePlugins( - (builtinPlugins as Plugin[]).concat(this.parsedPlugins).map(plugin => { + (this.getBuiltInPlugins() as Plugin[]).concat(this.parsedPlugins).map(plugin => { return { ...plugin, installed: plugin.default ?? false, @@ -316,7 +316,7 @@ export class OpenSCD extends LitElement { const isBuiltIn = !plugin?.official if (!isBuiltIn){ return plugin }; - const builtInPlugin = [...builtinPlugins, ...this.parsedPlugins] + const builtInPlugin = [...this.getBuiltInPlugins(), ...this.parsedPlugins] .find(p => p.src === plugin.src); return { @@ -367,14 +367,14 @@ export class OpenSCD extends LitElement { const localPluginConfigs = this.getPluginConfigsFromLocalStorage() const overwritesOfBultInPlugins = localPluginConfigs.filter((p) => { - return builtinPlugins.some(b => b.src === p.src) + return this.getBuiltInPlugins().some(b => b.src === p.src) }) const userInstalledPlugins = localPluginConfigs.filter((p) => { - return !builtinPlugins.some(b => b.src === p.src) + return !this.getBuiltInPlugins().some(b => b.src === p.src) }) - const mergedBuiltInPlugins = builtinPlugins.map((builtInPlugin) => { + const mergedBuiltInPlugins = this.getBuiltInPlugins().map((builtInPlugin) => { const noopOverwrite = {} const overwrite = overwritesOfBultInPlugins .find(p => p.src === builtInPlugin.src) @@ -404,6 +404,10 @@ export class OpenSCD extends LitElement { this.storePlugins(newPlugins); } + protected getBuiltInPlugins(): CorePlugin[] { + return builtinPlugins as CorePlugin[] + } + private addContent(plugin: Omit): Plugin { const tag = this.pluginTag(plugin.src); diff --git a/packages/openscd/src/plugins.ts b/packages/openscd/src/plugins.ts index 0c33dfa6c1..b1447c9e1d 100644 --- a/packages/openscd/src/plugins.ts +++ b/packages/openscd/src/plugins.ts @@ -1,4 +1,4 @@ -function generatePluginPath(plugin: string): string { +export function generatePluginPath(plugin: string): string { return location.origin+location.pathname+plugin; } diff --git a/packages/openscd/test/mock-open-scd.ts b/packages/openscd/test/mock-open-scd.ts index 597d549a3a..ff96c87722 100644 --- a/packages/openscd/test/mock-open-scd.ts +++ b/packages/openscd/test/mock-open-scd.ts @@ -4,6 +4,7 @@ import { html, queryAssignedNodes, query, + property } from 'lit-element'; import { OscdWizards } from '../src/addons/Wizards.js'; import { WizardFactory } from '../src/foundation.js'; @@ -11,9 +12,15 @@ import { OpenSCD } from '../src/open-scd.js'; import { WizardDialog } from '../src/wizard-dialog.js'; import { OscdHistory } from '../src/addons/History.js'; import { OscdLayout } from '../src/addons/Layout.js'; +// import type { Plugin } from '@openscd/core'; +import { Plugin } from '../src/plugin'; @customElement('mock-open-scd') export class MockOpenSCD extends OpenSCD { + + @property({ attribute: false }) + mockPlugins: Plugin[] = [] + @queryAssignedNodes() _plugins!: Array; @@ -32,10 +39,17 @@ export class MockOpenSCD extends OpenSCD { render(): TemplateResult { return html` - ${this.renderHosting()} - ${super.render()}`; + ${this.renderHosting()} + ${super.render()} + `; + } + + protected getBuiltInPlugins(): Plugin[]{ + return this.mockPlugins; } + + getPlugin(name: string): T | undefined { return this._plugins.find( p => p.tagName.toLowerCase() === name.toLowerCase() diff --git a/packages/openscd/test/mock-plugins.ts b/packages/openscd/test/mock-plugins.ts new file mode 100644 index 0000000000..254f0f071b --- /dev/null +++ b/packages/openscd/test/mock-plugins.ts @@ -0,0 +1,256 @@ + +function generatePluginPath(plugin: string): string { + return location.origin+location.pathname+plugin; +} + +export const officialPlugins = [ + { + name: 'IED', + src: generatePluginPath('plugins/src/editors/IED.js'), + icon: 'developer_board', + default: true, + kind: 'editor', + requireDoc: true, + }, + { + name: 'Substation', + src: generatePluginPath('plugins/src/editors/Substation.js'), + icon: 'margin', + default: true, + kind: 'editor', + requireDoc: true, + }, + { + name: 'Single Line Diagram', + src: generatePluginPath('plugins/src/editors/SingleLineDiagram.js'), + icon: 'edit', + default: false, + kind: 'editor', + requireDoc: true, + }, + { + name: 'Subscriber Message Binding (GOOSE)', + src: generatePluginPath('plugins/src/editors/GooseSubscriberMessageBinding.js'), + icon: 'link', + default: false, + kind: 'editor', + requireDoc: true, + }, + { + name: 'Subscriber Data Binding (GOOSE)', + src: generatePluginPath('plugins/src/editors/GooseSubscriberDataBinding.js'), + icon: 'link', + default: false, + kind: 'editor', + requireDoc: true, + }, + { + name: 'Subscriber Later Binding (GOOSE)', + src: generatePluginPath('plugins/src/editors/GooseSubscriberLaterBinding.js'), + icon: 'link', + default: true, + kind: 'editor', + requireDoc: true, + }, + { + name: 'Subscriber Message Binding (SMV)', + src: generatePluginPath('plugins/src/editors/SMVSubscriberMessageBinding.js'), + icon: 'link', + default: false, + kind: 'editor', + requireDoc: true, + }, + { + name: 'Subscriber Data Binding (SMV)', + src: generatePluginPath('plugins/src/editors/SMVSubscriberDataBinding.js'), + icon: 'link', + default: false, + kind: 'editor', + requireDoc: true, + }, + { + name: 'Subscriber Later Binding (SMV)', + src: generatePluginPath('plugins/src/editors/SMVSubscriberLaterBinding.js'), + icon: 'link', + default: true, + kind: 'editor', + requireDoc: true, + }, + { + name: 'Communication', + src: generatePluginPath('plugins/src/editors/Communication.js'), + icon: 'settings_ethernet', + default: true, + kind: 'editor', + requireDoc: true, + }, + { + name: '104', + src: generatePluginPath('plugins/src/editors/Protocol104.js'), + icon: 'settings_ethernet', + default: false, + kind: 'editor', + requireDoc: true, + }, + { + name: 'Templates', + src: generatePluginPath('plugins/src/editors/Templates.js'), + icon: 'copy_all', + default: true, + kind: 'editor', + requireDoc: true, + }, + { + name: 'Publisher', + src: generatePluginPath('plugins/src/editors/Publisher.js'), + icon: 'publish', + default: false, + kind: 'editor', + requireDoc: true, + }, + { + name: 'Cleanup', + src: generatePluginPath('plugins/src/editors/Cleanup.js'), + icon: 'cleaning_services', + default: false, + kind: 'editor', + requireDoc: true, + }, + { + name: 'Open project', + src: generatePluginPath('plugins/src/menu/OpenProject.js'), + icon: 'folder_open', + default: true, + kind: 'menu', + requireDoc: false, + position: 'top', + }, + { + name: 'New project', + src: generatePluginPath('plugins/src/menu/NewProject.js'), + icon: 'create_new_folder', + default: true, + kind: 'menu', + requireDoc: false, + position: 'top', + }, + { + name: 'Save project', + src: generatePluginPath('plugins/src/menu/SaveProject.js'), + icon: 'save', + default: true, + kind: 'menu', + requireDoc: true, + position: 'top', + }, + { + name: 'Validate Schema', + src: generatePluginPath('plugins/src/validators/ValidateSchema.js'), + icon: 'rule_folder', + default: true, + kind: 'validator', + }, + { + name: 'Validate Templates', + src: generatePluginPath('plugins/src/validators/ValidateTemplates.js'), + icon: 'rule_folder', + default: true, + kind: 'validator', + }, + { + name: 'Import IEDs', + src: generatePluginPath('plugins/src/menu/ImportIEDs.js'), + icon: 'snippet_folder', + default: true, + kind: 'menu', + requireDoc: true, + position: 'middle', + }, + { + name: 'Create Virtual IED', + src: generatePluginPath('plugins/src/menu/VirtualTemplateIED.js'), + icon: 'developer_board', + default: false, + kind: 'menu', + requireDoc: true, + position: 'middle', + }, + { + name: 'Subscriber Update', + src: generatePluginPath('plugins/src/menu/SubscriberInfo.js'), + default: true, + kind: 'menu', + requireDoc: true, + position: 'middle', + }, + { + name: 'Update desc (ABB)', + src: generatePluginPath('plugins/src/menu/UpdateDescriptionABB.js'), + default: false, + kind: 'menu', + requireDoc: true, + position: 'middle', + }, + { + name: 'Update desc (SEL)', + src: generatePluginPath('plugins/src/menu/UpdateDescriptionSEL.js'), + default: false, + kind: 'menu', + requireDoc: true, + position: 'middle', + }, + { + name: 'Merge Project', + src: generatePluginPath('plugins/src/menu/Merge.js'), + icon: 'merge_type', + default: true, + kind: 'menu', + requireDoc: true, + position: 'middle', + }, + { + name: 'Update Substation', + src: generatePluginPath('plugins/src/menu/UpdateSubstation.js'), + icon: 'merge_type', + default: true, + kind: 'menu', + requireDoc: true, + position: 'middle', + }, + { + name: 'Compare IED', + src: generatePluginPath('plugins/src/menu/CompareIED.js'), + icon: 'compare_arrows', + default: true, + kind: 'menu', + requireDoc: true, + position: 'middle', + }, + { + name: 'Show SCL History', + src: generatePluginPath('plugins/src/menu/SclHistory.js'), + icon: 'history_toggle_off', + default: true, + kind: 'menu', + requireDoc: true, + position: 'bottom', + }, + { + name: 'Help', + src: generatePluginPath('plugins/src/menu/Help.js'), + icon: 'help', + default: true, + kind: 'menu', + requireDoc: false, + position: 'bottom', + }, + { + name: 'Export Communication Section', + src: generatePluginPath('plugins/src/menu/ExportCommunication.js'), + icon: 'sim_card_download', + default: false, + kind: 'menu', + requireDoc: true, + position: 'middle', + }, +]; diff --git a/packages/openscd/test/unit/Plugging.test.ts b/packages/openscd/test/unit/Plugging.test.ts index 9e4976ebac..6f1d5e7a05 100644 --- a/packages/openscd/test/unit/Plugging.test.ts +++ b/packages/openscd/test/unit/Plugging.test.ts @@ -2,93 +2,112 @@ import { expect, fixture, html } from '@open-wc/testing'; import '../mock-open-scd.js'; import { MockOpenSCD } from '../mock-open-scd.js'; - import { TextField } from '@material/mwc-textfield'; import { Plugin } from '../../src/plugin'; -import { ConfigurePluginDetail, ConfigurePluginEvent, newConfigurePluginEvent } from '../../src/plugin.events'; +import { ConfigurePluginDetail, newConfigurePluginEvent } from '../../src/plugin.events'; +import { generatePluginPath } from "../../src/plugins" + + +async function renderMockOpenSCD( + doc: XMLDocument, + docName: string = "testDoc", + builtInPlugins: Plugin[] = builtinPlugins, +): Promise{ + const mockHTML = html`` + const openscd = (await fixture(mockHTML)) as MockOpenSCD; + await openscd.updateComplete; + return openscd +} describe('OpenSCD-Plugin', () => { - let element: MockOpenSCD; let doc: XMLDocument; const docName = 'testDoc'; afterEach(async () => { - await new Promise(resolve => setTimeout(resolve, 50)); // await animation localStorage.clear(); }); - beforeEach(async () => { + + before(async () => { doc = await fetch('/test/testfiles/valid2007B4.scd') .then(response => response.text()) .then(str => new DOMParser().parseFromString(str, 'application/xml')); - element = ( - await fixture( - html`` - ) - ); - await element.updateComplete; + }); - it('stores default plugins on load', () =>{ - expect(element.layout).property('editors').to.have.lengthOf(14) + it('stores default plugins on load', async () =>{ + const openscd = await renderMockOpenSCD(doc, docName, builtinPlugins.slice(0, 2)) + + openscd.requestUpdate() + await openscd.updateComplete + expect(openscd.layout).property('editors').to.have.lengthOf(2) }); it('has Locale property', async () => { - expect(element).to.have.property('locale'); + const openscd = await renderMockOpenSCD(doc); + expect(openscd).to.have.property('locale'); }); - it('has docs property', () => { - expect(element).to.have.property(`docs`).that.is.a('Object'); - expect(element.docs[docName]).to.equal(doc); + it('has docs property', async () => { + const openscd = await renderMockOpenSCD(doc) + expect(openscd).to.have.property(`docs`).that.is.a('Object'); + expect(openscd.docs[docName]).to.equal(doc); }); - describe('plugin manager dialog', () => { + describe('plugin manager dialog', async () => { let firstEditorPlugin: HTMLElement; let resetAction: HTMLElement; let primaryAction: HTMLElement; + const openscd = await renderMockOpenSCD(doc) + beforeEach(async () => { - element.layout.pluginUI.show(); - await element.layout.pluginUI.updateComplete; + openscd.layout.pluginUI.show(); + await openscd.layout.pluginUI.updateComplete; firstEditorPlugin = ( - element.layout.pluginList.querySelector( + openscd.layout.pluginList.querySelector( 'mwc-check-list-item:not([noninteractive])' ) ); resetAction = ( - element.layout.pluginUI.querySelector('mwc-button[slot="secondaryAction"]') + openscd.layout.pluginUI.querySelector('mwc-button[slot="secondaryAction"]') ); primaryAction = ( - element.layout.pluginUI.querySelector('mwc-button[slot="primaryAction"]') + openscd.layout.pluginUI.querySelector('mwc-button[slot="primaryAction"]') ); }); it('disables deselected plugins', async () => { firstEditorPlugin.click(); - await element.updateComplete; - expect(element.layout).property('editors').to.have.lengthOf(13); + await openscd.updateComplete; + expect(openscd.layout).property('editors').to.have.lengthOf(13); }); it('enables selected plugins', async () => { - (element.layout.pluginList.firstElementChild).click(); - await element.updateComplete; - (element.layout.pluginList.firstElementChild).click(); - await element.updateComplete; - expect(element.layout).property('editors').to.have.lengthOf(14); + + const openscd = await renderMockOpenSCD(doc); + + (openscd.layout.pluginList.firstElementChild).click(); + await openscd.updateComplete; + (openscd.layout.pluginList.firstElementChild).click(); + await openscd.updateComplete; + expect(openscd.layout).property('editors').to.have.lengthOf(14); }); it('resets plugins to default on reset button click', async () => { - (element.layout.pluginList.firstElementChild).click(); - await element.updateComplete; + const openscd = await renderMockOpenSCD(doc); + (openscd.layout.pluginList.firstElementChild).click(); + await openscd.updateComplete; resetAction.click(); - await element.updateComplete; - expect(element.layout).property('editors').to.have.lengthOf(6); + await openscd.updateComplete; + expect(openscd.layout).property('editors').to.have.lengthOf(6); }); it('opens the custom plugin dialog on add button click', async () => { + const openscd = await renderMockOpenSCD(doc); primaryAction.click(); - await element.updateComplete; - expect(element.layout) + await openscd.updateComplete; + expect(openscd.layout) .property('pluginDownloadUI') .to.have.property('open', true); }); @@ -100,29 +119,31 @@ describe('OpenSCD-Plugin', () => { let primaryAction: HTMLElement; let menuKindOption: HTMLElement; let validatorKindOption: HTMLElement; + let openscd: MockOpenSCD beforeEach(async () => { + openscd = await renderMockOpenSCD(doc); src = ( - element.layout.pluginDownloadUI.querySelector('#pluginSrcInput') + openscd.layout.pluginDownloadUI.querySelector('#pluginSrcInput') ); name = ( - element.layout.pluginDownloadUI.querySelector('#pluginNameInput') + openscd.layout.pluginDownloadUI.querySelector('#pluginNameInput') ); primaryAction = ( - element.layout.pluginDownloadUI.querySelector( + openscd.layout.pluginDownloadUI.querySelector( 'mwc-button[slot="primaryAction"]' ) ); - element.layout.pluginDownloadUI.show(); - await element.layout.pluginDownloadUI.updateComplete; - await element.updateComplete; + openscd.layout.pluginDownloadUI.show(); + await openscd.layout.pluginDownloadUI.updateComplete; + await openscd.updateComplete; menuKindOption = ( - element.layout.pluginDownloadUI.querySelector( + openscd.layout.pluginDownloadUI.querySelector( '#pluginKindList > mwc-radio-list-item[value="menu"]' ) ); validatorKindOption = ( - element.layout.pluginDownloadUI.querySelector( + openscd.layout.pluginDownloadUI.querySelector( '#pluginKindList > mwc-radio-list-item[id="validator"]' ) ); @@ -132,14 +153,14 @@ describe('OpenSCD-Plugin', () => { it('does not add without user interaction', async () => { primaryAction.click(); - expect(element.layout.pluginDownloadUI).to.have.property('open', true); + expect(openscd.layout.pluginDownloadUI).to.have.property('open', true); }) it('does not add without a name', async () => { src.value = 'http://example.com/plugin.js'; await src.updateComplete; primaryAction.click(); - expect(element.layout.pluginDownloadUI).to.have.property('open', true); + expect(openscd.layout.pluginDownloadUI).to.have.property('open', true); }) it('does not add plugin with incorrect url', async () => { @@ -148,7 +169,7 @@ describe('OpenSCD-Plugin', () => { await src.updateComplete; await name.updateComplete; primaryAction.click(); - expect(element.layout.pluginDownloadUI).to.have.property('open', true); + expect(openscd.layout.pluginDownloadUI).to.have.property('open', true); }); @@ -161,7 +182,7 @@ describe('OpenSCD-Plugin', () => { primaryAction.click(); - expect(element.layout.pluginDownloadUI).to.have.property('open', false); + expect(openscd.layout.pluginDownloadUI).to.have.property('open', false); }) }); @@ -172,20 +193,20 @@ describe('OpenSCD-Plugin', () => { await src.updateComplete; await name.updateComplete; primaryAction.click(); - await element.updateComplete; - expect(element.layout.editors).to.have.lengthOf(15); + await openscd.updateComplete; + expect(openscd.layout.editors).to.have.lengthOf(15); }); it('adds a new menu kind plugin on add button click', async () => { - const lengthMenuKindPlugins = element.layout.menuEntries.length; + const lengthMenuKindPlugins = openscd.layout.menuEntries.length; src.value = 'http://example.com/plugin.js'; name.value = 'testName'; menuKindOption.click(); await src.updateComplete; await name.updateComplete; primaryAction.click(); - await element.updateComplete; - expect(element.layout.menuEntries).to.have.lengthOf(lengthMenuKindPlugins + 1); + await openscd.updateComplete; + expect(openscd.layout.menuEntries).to.have.lengthOf(lengthMenuKindPlugins + 1); }); it('sets requireDoc and position for new menu kind plugin', async () => { @@ -195,25 +216,25 @@ describe('OpenSCD-Plugin', () => { await src.updateComplete; await name.updateComplete; primaryAction.click(); - await element.updateComplete; + await openscd.updateComplete; expect( - element.layout.menuEntries[element.layout.menuEntries.length - 1] + openscd.layout.menuEntries[openscd.layout.menuEntries.length - 1] ).to.have.property('requireDoc'); expect( - element.layout.menuEntries[element.layout.menuEntries.length - 1] + openscd.layout.menuEntries[openscd.layout.menuEntries.length - 1] ).to.have.property('position'); }); it('adds a new validator kind plugin on add button click', async () => { - expect(element.layout.validators).to.have.lengthOf(2); + expect(openscd.layout.validators).to.have.lengthOf(2); src.value = 'http://example.com/plugin.js'; name.value = 'testName'; validatorKindOption.click(); await src.updateComplete; await name.updateComplete; primaryAction.click(); - await element.updateComplete; - expect(element.layout.validators).to.have.lengthOf(3); + await openscd.updateComplete; + expect(openscd.layout.validators).to.have.lengthOf(3); }); }); @@ -395,15 +416,16 @@ describe('OpenSCD-Plugin', () => { function testFeature(tc: TestCase) { it(tc.desc, async () => { // ARRANGE + const openscd = await renderMockOpenSCD(doc); // @ts-ignore: we use the private function to arrange the scenario - element.storePlugins(tc.currentPlugins) - await element.updateComplete + openscd.storePlugins(tc.currentPlugins) + await openscd.updateComplete // ACT const event = newConfigurePluginEvent(tc.eventDetails.name, tc.eventDetails.kind, tc.eventDetails.config) - element.layout.dispatchEvent(event) - await element.updateComplete + openscd.layout.dispatchEvent(event) + await openscd.updateComplete // ASSERT @@ -413,7 +435,7 @@ describe('OpenSCD-Plugin', () => { // I've tried to use chai's deep.members and deep.include.members // and others but non of them worked. const keys = ["name", "kind", "src", "installed"] - const storedPlugins = element.layout.plugins.map((plugin) => { + const storedPlugins = openscd.layout.plugins.map((plugin) => { Object.keys(plugin).forEach((key) => { if(!keys.includes(key)) { delete plugin[key] @@ -423,7 +445,7 @@ describe('OpenSCD-Plugin', () => { return plugin }) - const msg = `expected: ${JSON.stringify(tc.expectedPlugins)} but got: ${JSON.stringify(element.layout.plugins)}` + const msg = `expected: ${JSON.stringify(tc.expectedPlugins)} but got: ${JSON.stringify(openscd.layout.plugins)}` expect(tc.expectedPlugins).to.have.deep.members(storedPlugins, msg) }) @@ -432,3 +454,284 @@ describe('OpenSCD-Plugin', () => { }) }); +const builtinPlugins: Plugin[] = [ + { + name: 'IED', + src: generatePluginPath('plugins/src/editors/IED.js'), + icon: 'developer_board', + default: true, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Substation', + src: generatePluginPath('plugins/src/editors/Substation.js'), + icon: 'margin', + default: true, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Single Line Diagram', + src: generatePluginPath('plugins/src/editors/SingleLineDiagram.js'), + icon: 'edit', + default: false, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Subscriber Message Binding (GOOSE)', + src: generatePluginPath('plugins/src/editors/GooseSubscriberMessageBinding.js'), + icon: 'link', + default: false, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Subscriber Data Binding (GOOSE)', + src: generatePluginPath('plugins/src/editors/GooseSubscriberDataBinding.js'), + icon: 'link', + default: false, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Subscriber Later Binding (GOOSE)', + src: generatePluginPath('plugins/src/editors/GooseSubscriberLaterBinding.js'), + icon: 'link', + default: true, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Subscriber Message Binding (SMV)', + src: generatePluginPath('plugins/src/editors/SMVSubscriberMessageBinding.js'), + icon: 'link', + default: false, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Subscriber Data Binding (SMV)', + src: generatePluginPath('plugins/src/editors/SMVSubscriberDataBinding.js'), + icon: 'link', + default: false, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Subscriber Later Binding (SMV)', + src: generatePluginPath('plugins/src/editors/SMVSubscriberLaterBinding.js'), + icon: 'link', + default: true, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Communication', + src: generatePluginPath('plugins/src/editors/Communication.js'), + icon: 'settings_ethernet', + default: true, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: '104', + src: generatePluginPath('plugins/src/editors/Protocol104.js'), + icon: 'settings_ethernet', + default: false, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Templates', + src: generatePluginPath('plugins/src/editors/Templates.js'), + icon: 'copy_all', + default: true, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Publisher', + src: generatePluginPath('plugins/src/editors/Publisher.js'), + icon: 'publish', + default: false, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Cleanup', + src: generatePluginPath('plugins/src/editors/Cleanup.js'), + icon: 'cleaning_services', + default: false, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Open project', + src: generatePluginPath('plugins/src/menu/OpenProject.js'), + icon: 'folder_open', + default: true, + kind: 'menu', + requireDoc: false, + installed: true, + position: 'top', + }, + { + name: 'New project', + src: generatePluginPath('plugins/src/menu/NewProject.js'), + icon: 'create_new_folder', + default: true, + kind: 'menu', + requireDoc: false, + installed: true, + position: 'top', + }, + { + name: 'Save project', + src: generatePluginPath('plugins/src/menu/SaveProject.js'), + icon: 'save', + default: true, + kind: 'menu', + requireDoc: true, + installed: true, + position: 'top', + }, + { + name: 'Validate Schema', + src: generatePluginPath('plugins/src/validators/ValidateSchema.js'), + icon: 'rule_folder', + default: true, + kind: 'validator', + installed: true, + }, + { + name: 'Validate Templates', + src: generatePluginPath('plugins/src/validators/ValidateTemplates.js'), + icon: 'rule_folder', + default: true, + kind: 'validator', + installed: true, + }, + { + name: 'Import IEDs', + src: generatePluginPath('plugins/src/menu/ImportIEDs.js'), + icon: 'snippet_folder', + default: true, + kind: 'menu', + requireDoc: true, + installed: true, + position: 'middle', + }, + { + name: 'Create Virtual IED', + src: generatePluginPath('plugins/src/menu/VirtualTemplateIED.js'), + icon: 'developer_board', + default: false, + kind: 'menu', + requireDoc: true, + installed: true, + position: 'middle', + }, + { + name: 'Subscriber Update', + src: generatePluginPath('plugins/src/menu/SubscriberInfo.js'), + default: true, + kind: 'menu', + requireDoc: true, + installed: true, + position: 'middle', + }, + { + name: 'Update desc (ABB)', + src: generatePluginPath('plugins/src/menu/UpdateDescriptionABB.js'), + default: false, + kind: 'menu', + requireDoc: true, + installed: true, + position: 'middle', + }, + { + name: 'Update desc (SEL)', + src: generatePluginPath('plugins/src/menu/UpdateDescriptionSEL.js'), + default: false, + kind: 'menu', + requireDoc: true, + installed: true, + position: 'middle', + }, + { + name: 'Merge Project', + src: generatePluginPath('plugins/src/menu/Merge.js'), + icon: 'merge_type', + default: true, + kind: 'menu', + requireDoc: true, + installed: true, + position: 'middle', + }, + { + name: 'Update Substation', + src: generatePluginPath('plugins/src/menu/UpdateSubstation.js'), + icon: 'merge_type', + default: true, + kind: 'menu', + requireDoc: true, + installed: true, + position: 'middle', + }, + { + name: 'Compare IED', + src: generatePluginPath('plugins/src/menu/CompareIED.js'), + icon: 'compare_arrows', + default: true, + kind: 'menu', + requireDoc: true, + installed: true, + position: 'middle', + }, + { + name: 'Show SCL History', + src: generatePluginPath('plugins/src/menu/SclHistory.js'), + icon: 'history_toggle_off', + default: true, + kind: 'menu', + requireDoc: true, + installed: true, + position: 'bottom', + }, + { + name: 'Help', + src: generatePluginPath('plugins/src/menu/Help.js'), + icon: 'help', + default: true, + kind: 'menu', + requireDoc: false, + installed: true, + position: 'bottom', + }, + { + name: 'Export Communication Section', + src: generatePluginPath('plugins/src/menu/ExportCommunication.js'), + icon: 'sim_card_download', + default: false, + kind: 'menu', + requireDoc: true, + installed: true, + position: 'middle', + }, +]; diff --git a/packages/openscd/web-test-runner.config.mjs b/packages/openscd/web-test-runner.config.mjs index 0bf46459bd..fea03e351e 100644 --- a/packages/openscd/web-test-runner.config.mjs +++ b/packages/openscd/web-test-runner.config.mjs @@ -18,7 +18,7 @@ export default /** @type {import("@web/test-runner").TestRunnerConfig} */ ({ product: 'chromium', launchOptions: { headless: true, - devtools: true, + devtools: false, } }), // playwrightLauncher({ product: 'firefox' }), diff --git a/packages/plugins/test/integration/validators/ValidateSchema.test.ts b/packages/plugins/test/integration/validators/ValidateSchema.test.ts index 67d90b4d53..b4a5f15e95 100644 --- a/packages/plugins/test/integration/validators/ValidateSchema.test.ts +++ b/packages/plugins/test/integration/validators/ValidateSchema.test.ts @@ -2,6 +2,7 @@ import { expect, fixture, html } from '@open-wc/testing'; import '@openscd/open-scd/test/mock-open-scd.js'; import { MockOpenSCD } from '@openscd/open-scd/test/mock-open-scd.js'; +import type { Plugin } from '@openscd/open-scd/src/plugin.js'; import ValidateSchema from '../../../src/validators/ValidateSchema.js'; import { IssueDetail, LogEntry } from '@openscd/core/foundation/deprecated/history.js'; @@ -18,7 +19,7 @@ describe('ValidateSchema plugin', () => { before(async () => { parent = await fixture(html` - + `); element = parent.getActivePlugin(); @@ -108,3 +109,290 @@ describe('ValidateSchema plugin', () => { }); }); + + +const builtinPlugins: Plugin[] = [ + { + name: 'IED', + src: generatePluginPath('plugins/src/editors/IED.js'), + icon: 'developer_board', + default: true, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Substation', + src: generatePluginPath('plugins/src/editors/Substation.js'), + icon: 'margin', + default: true, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Single Line Diagram', + src: generatePluginPath('plugins/src/editors/SingleLineDiagram.js'), + icon: 'edit', + default: false, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Subscriber Message Binding (GOOSE)', + src: generatePluginPath('plugins/src/editors/GooseSubscriberMessageBinding.js'), + icon: 'link', + default: false, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Subscriber Data Binding (GOOSE)', + src: generatePluginPath('plugins/src/editors/GooseSubscriberDataBinding.js'), + icon: 'link', + default: false, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Subscriber Later Binding (GOOSE)', + src: generatePluginPath('plugins/src/editors/GooseSubscriberLaterBinding.js'), + icon: 'link', + default: true, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Subscriber Message Binding (SMV)', + src: generatePluginPath('plugins/src/editors/SMVSubscriberMessageBinding.js'), + icon: 'link', + default: false, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Subscriber Data Binding (SMV)', + src: generatePluginPath('plugins/src/editors/SMVSubscriberDataBinding.js'), + icon: 'link', + default: false, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Subscriber Later Binding (SMV)', + src: generatePluginPath('plugins/src/editors/SMVSubscriberLaterBinding.js'), + icon: 'link', + default: true, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Communication', + src: generatePluginPath('plugins/src/editors/Communication.js'), + icon: 'settings_ethernet', + default: true, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: '104', + src: generatePluginPath('plugins/src/editors/Protocol104.js'), + icon: 'settings_ethernet', + default: false, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Templates', + src: generatePluginPath('plugins/src/editors/Templates.js'), + icon: 'copy_all', + default: true, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Publisher', + src: generatePluginPath('plugins/src/editors/Publisher.js'), + icon: 'publish', + default: false, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Cleanup', + src: generatePluginPath('plugins/src/editors/Cleanup.js'), + icon: 'cleaning_services', + default: false, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Open project', + src: generatePluginPath('plugins/src/menu/OpenProject.js'), + icon: 'folder_open', + default: true, + kind: 'menu', + requireDoc: false, + installed: true, + position: 'top', + }, + { + name: 'New project', + src: generatePluginPath('plugins/src/menu/NewProject.js'), + icon: 'create_new_folder', + default: true, + kind: 'menu', + requireDoc: false, + installed: true, + position: 'top', + }, + { + name: 'Save project', + src: generatePluginPath('plugins/src/menu/SaveProject.js'), + icon: 'save', + default: true, + kind: 'menu', + requireDoc: true, + installed: true, + position: 'top', + }, + { + name: 'Validate Schema', + src: generatePluginPath('plugins/src/validators/ValidateSchema.js'), + icon: 'rule_folder', + default: true, + kind: 'validator', + installed: true, + }, + { + name: 'Validate Templates', + src: generatePluginPath('plugins/src/validators/ValidateTemplates.js'), + icon: 'rule_folder', + default: true, + kind: 'validator', + installed: true, + }, + { + name: 'Import IEDs', + src: generatePluginPath('plugins/src/menu/ImportIEDs.js'), + icon: 'snippet_folder', + default: true, + kind: 'menu', + requireDoc: true, + installed: true, + position: 'middle', + }, + { + name: 'Create Virtual IED', + src: generatePluginPath('plugins/src/menu/VirtualTemplateIED.js'), + icon: 'developer_board', + default: false, + kind: 'menu', + requireDoc: true, + installed: true, + position: 'middle', + }, + { + name: 'Subscriber Update', + src: generatePluginPath('plugins/src/menu/SubscriberInfo.js'), + default: true, + kind: 'menu', + requireDoc: true, + installed: true, + position: 'middle', + }, + { + name: 'Update desc (ABB)', + src: generatePluginPath('plugins/src/menu/UpdateDescriptionABB.js'), + default: false, + kind: 'menu', + requireDoc: true, + installed: true, + position: 'middle', + }, + { + name: 'Update desc (SEL)', + src: generatePluginPath('plugins/src/menu/UpdateDescriptionSEL.js'), + default: false, + kind: 'menu', + requireDoc: true, + installed: true, + position: 'middle', + }, + { + name: 'Merge Project', + src: generatePluginPath('plugins/src/menu/Merge.js'), + icon: 'merge_type', + default: true, + kind: 'menu', + requireDoc: true, + installed: true, + position: 'middle', + }, + { + name: 'Update Substation', + src: generatePluginPath('plugins/src/menu/UpdateSubstation.js'), + icon: 'merge_type', + default: true, + kind: 'menu', + requireDoc: true, + installed: true, + position: 'middle', + }, + { + name: 'Compare IED', + src: generatePluginPath('plugins/src/menu/CompareIED.js'), + icon: 'compare_arrows', + default: true, + kind: 'menu', + requireDoc: true, + installed: true, + position: 'middle', + }, + { + name: 'Show SCL History', + src: generatePluginPath('plugins/src/menu/SclHistory.js'), + icon: 'history_toggle_off', + default: true, + kind: 'menu', + requireDoc: true, + installed: true, + position: 'bottom', + }, + { + name: 'Help', + src: generatePluginPath('plugins/src/menu/Help.js'), + icon: 'help', + default: true, + kind: 'menu', + requireDoc: false, + installed: true, + position: 'bottom', + }, + { + name: 'Export Communication Section', + src: generatePluginPath('plugins/src/menu/ExportCommunication.js'), + icon: 'sim_card_download', + default: false, + kind: 'menu', + requireDoc: true, + installed: true, + position: 'middle', + }, +]; + +export function generatePluginPath(plugin: string): string { + return location.origin+location.pathname+plugin; +} diff --git a/packages/plugins/test/integration/validators/ValidateTemplates.test.ts b/packages/plugins/test/integration/validators/ValidateTemplates.test.ts index 3e1dd8aa1d..d6997811b5 100644 --- a/packages/plugins/test/integration/validators/ValidateTemplates.test.ts +++ b/packages/plugins/test/integration/validators/ValidateTemplates.test.ts @@ -2,6 +2,7 @@ import { expect, fixture, html } from '@open-wc/testing'; import '@openscd/open-scd/test/mock-open-scd.js'; import { MockOpenSCD } from '@openscd/open-scd/test/mock-open-scd.js'; +import type { Plugin } from '@openscd/open-scd/src/plugin.js'; import ValidateTemplates from '../../../src/validators/ValidateTemplates.js'; @@ -21,7 +22,7 @@ describe('ValidateTemplates OpenSCD integration test ', () => { .then(str => new DOMParser().parseFromString(str, 'application/xml')); parent = await fixture(html` - `); @@ -46,7 +47,7 @@ describe('ValidateTemplates OpenSCD integration test ', () => { .then(str => new DOMParser().parseFromString(str, 'application/xml')); parent = await fixture(html` - `); @@ -74,7 +75,7 @@ describe('ValidateTemplates OpenSCD integration test ', () => { .then(str => new DOMParser().parseFromString(str, 'application/xml')); parent = await fixture(html` - `); @@ -96,3 +97,289 @@ describe('ValidateTemplates OpenSCD integration test ', () => { }); }); }); + +const builtinPlugins: Plugin[] = [ + { + name: 'IED', + src: generatePluginPath('plugins/src/editors/IED.js'), + icon: 'developer_board', + default: true, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Substation', + src: generatePluginPath('plugins/src/editors/Substation.js'), + icon: 'margin', + default: true, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Single Line Diagram', + src: generatePluginPath('plugins/src/editors/SingleLineDiagram.js'), + icon: 'edit', + default: false, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Subscriber Message Binding (GOOSE)', + src: generatePluginPath('plugins/src/editors/GooseSubscriberMessageBinding.js'), + icon: 'link', + default: false, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Subscriber Data Binding (GOOSE)', + src: generatePluginPath('plugins/src/editors/GooseSubscriberDataBinding.js'), + icon: 'link', + default: false, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Subscriber Later Binding (GOOSE)', + src: generatePluginPath('plugins/src/editors/GooseSubscriberLaterBinding.js'), + icon: 'link', + default: true, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Subscriber Message Binding (SMV)', + src: generatePluginPath('plugins/src/editors/SMVSubscriberMessageBinding.js'), + icon: 'link', + default: false, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Subscriber Data Binding (SMV)', + src: generatePluginPath('plugins/src/editors/SMVSubscriberDataBinding.js'), + icon: 'link', + default: false, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Subscriber Later Binding (SMV)', + src: generatePluginPath('plugins/src/editors/SMVSubscriberLaterBinding.js'), + icon: 'link', + default: true, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Communication', + src: generatePluginPath('plugins/src/editors/Communication.js'), + icon: 'settings_ethernet', + default: true, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: '104', + src: generatePluginPath('plugins/src/editors/Protocol104.js'), + icon: 'settings_ethernet', + default: false, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Templates', + src: generatePluginPath('plugins/src/editors/Templates.js'), + icon: 'copy_all', + default: true, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Publisher', + src: generatePluginPath('plugins/src/editors/Publisher.js'), + icon: 'publish', + default: false, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Cleanup', + src: generatePluginPath('plugins/src/editors/Cleanup.js'), + icon: 'cleaning_services', + default: false, + kind: 'editor', + requireDoc: true, + installed: true, + }, + { + name: 'Open project', + src: generatePluginPath('plugins/src/menu/OpenProject.js'), + icon: 'folder_open', + default: true, + kind: 'menu', + requireDoc: false, + installed: true, + position: 'top', + }, + { + name: 'New project', + src: generatePluginPath('plugins/src/menu/NewProject.js'), + icon: 'create_new_folder', + default: true, + kind: 'menu', + requireDoc: false, + installed: true, + position: 'top', + }, + { + name: 'Save project', + src: generatePluginPath('plugins/src/menu/SaveProject.js'), + icon: 'save', + default: true, + kind: 'menu', + requireDoc: true, + installed: true, + position: 'top', + }, + { + name: 'Validate Schema', + src: generatePluginPath('plugins/src/validators/ValidateSchema.js'), + icon: 'rule_folder', + default: true, + kind: 'validator', + installed: true, + }, + { + name: 'Validate Templates', + src: generatePluginPath('plugins/src/validators/ValidateTemplates.js'), + icon: 'rule_folder', + default: true, + kind: 'validator', + installed: true, + }, + { + name: 'Import IEDs', + src: generatePluginPath('plugins/src/menu/ImportIEDs.js'), + icon: 'snippet_folder', + default: true, + kind: 'menu', + requireDoc: true, + installed: true, + position: 'middle', + }, + { + name: 'Create Virtual IED', + src: generatePluginPath('plugins/src/menu/VirtualTemplateIED.js'), + icon: 'developer_board', + default: false, + kind: 'menu', + requireDoc: true, + installed: true, + position: 'middle', + }, + { + name: 'Subscriber Update', + src: generatePluginPath('plugins/src/menu/SubscriberInfo.js'), + default: true, + kind: 'menu', + requireDoc: true, + installed: true, + position: 'middle', + }, + { + name: 'Update desc (ABB)', + src: generatePluginPath('plugins/src/menu/UpdateDescriptionABB.js'), + default: false, + kind: 'menu', + requireDoc: true, + installed: true, + position: 'middle', + }, + { + name: 'Update desc (SEL)', + src: generatePluginPath('plugins/src/menu/UpdateDescriptionSEL.js'), + default: false, + kind: 'menu', + requireDoc: true, + installed: true, + position: 'middle', + }, + { + name: 'Merge Project', + src: generatePluginPath('plugins/src/menu/Merge.js'), + icon: 'merge_type', + default: true, + kind: 'menu', + requireDoc: true, + installed: true, + position: 'middle', + }, + { + name: 'Update Substation', + src: generatePluginPath('plugins/src/menu/UpdateSubstation.js'), + icon: 'merge_type', + default: true, + kind: 'menu', + requireDoc: true, + installed: true, + position: 'middle', + }, + { + name: 'Compare IED', + src: generatePluginPath('plugins/src/menu/CompareIED.js'), + icon: 'compare_arrows', + default: true, + kind: 'menu', + requireDoc: true, + installed: true, + position: 'middle', + }, + { + name: 'Show SCL History', + src: generatePluginPath('plugins/src/menu/SclHistory.js'), + icon: 'history_toggle_off', + default: true, + kind: 'menu', + requireDoc: true, + installed: true, + position: 'bottom', + }, + { + name: 'Help', + src: generatePluginPath('plugins/src/menu/Help.js'), + icon: 'help', + default: true, + kind: 'menu', + requireDoc: false, + installed: true, + position: 'bottom', + }, + { + name: 'Export Communication Section', + src: generatePluginPath('plugins/src/menu/ExportCommunication.js'), + icon: 'sim_card_download', + default: false, + kind: 'menu', + requireDoc: true, + installed: true, + position: 'middle', + }, +]; + +export function generatePluginPath(plugin: string): string { + return location.origin+location.pathname+plugin; +} diff --git a/packages/plugins/web-test-runner.config.mjs b/packages/plugins/web-test-runner.config.mjs index d717d5ef1f..4386b9bd4c 100644 --- a/packages/plugins/web-test-runner.config.mjs +++ b/packages/plugins/web-test-runner.config.mjs @@ -28,6 +28,10 @@ export default /** @type {import("@web/test-runner").TestRunnerConfig} */ ({ name: 'integration', files: 'test/integration/**/*.test.ts', }, + // { + // name: 'single', + // files: 'test/integration/validators/ValidateTemplates.test.ts', + // }, ], /** Compile JS for older browsers. Requires @web/dev-server-esbuild plugin */ @@ -41,7 +45,14 @@ export default /** @type {import("@web/test-runner").TestRunnerConfig} */ ({ /** Browsers to run tests on */ browsers: [ - playwrightLauncher({ product: 'chromium' }), + playwrightLauncher({ + // concurrency: 1, + product: 'chromium', + launchOptions: { + headless: true, + devtools: false, + }, + }), // playwrightLauncher({ product: 'firefox' }), // playwrightLauncher({ product: 'webkit' }), ], From 101aaaef2217d7bb9d1b1d6a99d3b7ea3d77c503 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Russ?= Date: Thu, 6 Feb 2025 15:40:43 +0100 Subject: [PATCH 09/28] Fix: Forgetting Plugin Settnigs (#1619) --- packages/openscd/src/open-scd.ts | 1 - .../__snapshots__/open-scd.test.snap.js | 1103 +---------------- 2 files changed, 13 insertions(+), 1091 deletions(-) diff --git a/packages/openscd/src/open-scd.ts b/packages/openscd/src/open-scd.ts index d4edf5e74a..6b0f458d3a 100644 --- a/packages/openscd/src/open-scd.ts +++ b/packages/openscd/src/open-scd.ts @@ -383,7 +383,6 @@ export class OpenSCD extends LitElement { return { ...builtInPlugin, ...overwrite, - installed: true, // TODO: is this correct? should we decide it based on something? } }) diff --git a/packages/openscd/test/integration/__snapshots__/open-scd.test.snap.js b/packages/openscd/test/integration/__snapshots__/open-scd.test.snap.js index 2029591739..fdf0469dcd 100644 --- a/packages/openscd/test/integration/__snapshots__/open-scd.test.snap.js +++ b/packages/openscd/test/integration/__snapshots__/open-scd.test.snap.js @@ -82,282 +82,12 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like role="separator" > - - - folder_open - - - Open project - - - - - - - create_new_folder - - - New project - - - - - - - save - - - Save project - - - - -
  • -
  • - - - rule_folder - - - Validate Schema - - - - - - - rule_folder - - - Validate Templates - - - -
  • - - - snippet_folder - - - Import IEDs - - - - - - - developer_board - - - Create Virtual IED - - - - - - - play_circle - - - Subscriber Update - - - - - - - play_circle - - - Update desc (ABB) - - - - - - - play_circle - - - Update desc (SEL) - - - - - - - merge_type - - - Merge Project - - - - - - - merge_type - - - Update Substation - - - - - - - compare_arrows - - - Compare IED - - - - - - - sim_card_download - - - Export Communication Section - - - -
  • settings @@ -380,49 +110,6 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like Settings - - - history_toggle_off - - - Show SCL History - - - - - - - help - - - Help - - - - -
  • -
  • - -
    - Open project -
    -
    - -
    - New project -
    -
    @@ -512,7 +182,6 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/Substation.js" > @@ -528,7 +197,6 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/SingleLineDiagram.js" > @@ -544,7 +212,6 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/GooseSubscriberMessageBinding.js" > @@ -560,7 +227,6 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/GooseSubscriberDataBinding.js" > @@ -576,7 +242,6 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/GooseSubscriberLaterBinding.js" > @@ -592,7 +257,6 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/SMVSubscriberMessageBinding.js" > @@ -608,7 +272,6 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/SMVSubscriberDataBinding.js" > @@ -624,7 +287,6 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/SMVSubscriberLaterBinding.js" > @@ -640,7 +302,6 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/Communication.js" > @@ -656,7 +317,6 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/Protocol104.js" > @@ -672,7 +332,6 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/Templates.js" > @@ -688,7 +347,6 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/Publisher.js" > @@ -704,7 +362,6 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/Cleanup.js" > @@ -743,7 +400,6 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/OpenProject.js" > @@ -759,7 +415,6 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/NewProject.js" > @@ -775,7 +430,6 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/SaveProject.js" > @@ -797,7 +451,6 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/validators/ValidateSchema.js" > @@ -813,7 +466,6 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/validators/ValidateTemplates.js" > @@ -835,7 +487,6 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/ImportIEDs.js" > @@ -851,7 +502,6 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/VirtualTemplateIED.js" > @@ -867,7 +517,6 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/SubscriberInfo.js" > @@ -883,7 +532,6 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/UpdateDescriptionABB.js" > @@ -899,7 +547,6 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/UpdateDescriptionSEL.js" > @@ -915,7 +562,6 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/Merge.js" > @@ -931,7 +577,6 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/UpdateSubstation.js" > @@ -947,7 +592,6 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/CompareIED.js" > @@ -963,7 +607,6 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/ExportCommunication.js" > @@ -985,7 +628,6 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/SclHistory.js" > @@ -1001,7 +643,6 @@ snapshots["open-scd renders menu plugins passed down as props and it looks like hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/Help.js" > @@ -1231,287 +872,17 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik
  • -
  • - - - folder_open - - - Open project - - - - - - - create_new_folder - - - New project - - - - - - - save - - - Save project - - - - -
  • -
  • - - - rule_folder - - - Validate Schema - - - - - - - rule_folder - - - Validate Templates - - - - -
  • -
  • - - - snippet_folder - - - Import IEDs - - - - - - - developer_board - - - Create Virtual IED - - - - - - - play_circle - - - Subscriber Update - - - - - - - play_circle - - - Update desc (ABB) - - - - - - - play_circle - - - Update desc (SEL) - - - - - - - merge_type - - - Merge Project - - - - - - - merge_type - - - Update Substation - - - - - - - compare_arrows - - - Compare IED - - - - - - - sim_card_download - - - Export Communication Section - - - - + +
  • +
  • settings @@ -1534,49 +905,6 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik Settings - - - history_toggle_off - - - Show SCL History - - - - - - - help - - - Help - - - - -
  • -
  • - -
    - Open project -
    -
    - -
    - New project -
    -
    @@ -1666,7 +977,6 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/Substation.js" > @@ -1682,7 +992,6 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/SingleLineDiagram.js" > @@ -1698,7 +1007,6 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/GooseSubscriberMessageBinding.js" > @@ -1714,7 +1022,6 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/GooseSubscriberDataBinding.js" > @@ -1730,7 +1037,6 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/GooseSubscriberLaterBinding.js" > @@ -1746,7 +1052,6 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/SMVSubscriberMessageBinding.js" > @@ -1762,7 +1067,6 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/SMVSubscriberDataBinding.js" > @@ -1778,7 +1082,6 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/SMVSubscriberLaterBinding.js" > @@ -1794,7 +1097,6 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/Communication.js" > @@ -1810,7 +1112,6 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/Protocol104.js" > @@ -1826,7 +1127,6 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/Templates.js" > @@ -1842,7 +1142,6 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/Publisher.js" > @@ -1858,7 +1157,6 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/Cleanup.js" > @@ -1897,7 +1195,6 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/OpenProject.js" > @@ -1913,7 +1210,6 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/NewProject.js" > @@ -1929,7 +1225,6 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/SaveProject.js" > @@ -1951,7 +1246,6 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/validators/ValidateSchema.js" > @@ -1967,7 +1261,6 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/validators/ValidateTemplates.js" > @@ -1989,7 +1282,6 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/ImportIEDs.js" > @@ -2005,7 +1297,6 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/VirtualTemplateIED.js" > @@ -2021,7 +1312,6 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/SubscriberInfo.js" > @@ -2037,7 +1327,6 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/UpdateDescriptionABB.js" > @@ -2053,7 +1342,6 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/UpdateDescriptionSEL.js" > @@ -2069,7 +1357,6 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/Merge.js" > @@ -2085,7 +1372,6 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/UpdateSubstation.js" > @@ -2101,7 +1387,6 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/CompareIED.js" > @@ -2117,7 +1402,6 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/ExportCommunication.js" > @@ -2139,7 +1423,6 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/SclHistory.js" > @@ -2155,7 +1438,6 @@ snapshots["open-scd renders editor plugins passed down as props and it looks lik hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/Help.js" > @@ -2390,282 +1672,12 @@ snapshots["open-scd layout looks like its snapshot"] = role="separator" > - - - folder_open - - - Open project - - - - - - - create_new_folder - - - New project - - - - - - - save - - - Save project - - - - -
  • -
  • - - - rule_folder - - - Validate Schema - - - - - - - rule_folder - - - Validate Templates - - - -
  • - - - snippet_folder - - - Import IEDs - - - - - - - developer_board - - - Create Virtual IED - - - - - - - play_circle - - - Subscriber Update - - - - - - - play_circle - - - Update desc (ABB) - - - - - - - play_circle - - - Update desc (SEL) - - - - - - - merge_type - - - Merge Project - - - - - - - merge_type - - - Update Substation - - - - - - - compare_arrows - - - Compare IED - - - - - - - sim_card_download - - - Export Communication Section - - - -
  • settings @@ -2688,49 +1700,6 @@ snapshots["open-scd layout looks like its snapshot"] = Settings - - - history_toggle_off - - - Show SCL History - - - - - - - help - - - Help - - - - -
  • -
  • - -
    - Open project -
    -
    - -
    - New project -
    -
    @@ -2820,7 +1772,6 @@ snapshots["open-scd layout looks like its snapshot"] = hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/Substation.js" > @@ -2836,7 +1787,6 @@ snapshots["open-scd layout looks like its snapshot"] = hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/SingleLineDiagram.js" > @@ -2852,7 +1802,6 @@ snapshots["open-scd layout looks like its snapshot"] = hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/GooseSubscriberMessageBinding.js" > @@ -2868,7 +1817,6 @@ snapshots["open-scd layout looks like its snapshot"] = hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/GooseSubscriberDataBinding.js" > @@ -2884,7 +1832,6 @@ snapshots["open-scd layout looks like its snapshot"] = hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/GooseSubscriberLaterBinding.js" > @@ -2900,7 +1847,6 @@ snapshots["open-scd layout looks like its snapshot"] = hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/SMVSubscriberMessageBinding.js" > @@ -2916,7 +1862,6 @@ snapshots["open-scd layout looks like its snapshot"] = hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/SMVSubscriberDataBinding.js" > @@ -2932,7 +1877,6 @@ snapshots["open-scd layout looks like its snapshot"] = hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/SMVSubscriberLaterBinding.js" > @@ -2948,7 +1892,6 @@ snapshots["open-scd layout looks like its snapshot"] = hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/Communication.js" > @@ -2964,7 +1907,6 @@ snapshots["open-scd layout looks like its snapshot"] = hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/Protocol104.js" > @@ -2980,7 +1922,6 @@ snapshots["open-scd layout looks like its snapshot"] = hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/Templates.js" > @@ -2996,7 +1937,6 @@ snapshots["open-scd layout looks like its snapshot"] = hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/Publisher.js" > @@ -3012,7 +1952,6 @@ snapshots["open-scd layout looks like its snapshot"] = hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/editors/Cleanup.js" > @@ -3051,7 +1990,6 @@ snapshots["open-scd layout looks like its snapshot"] = hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/OpenProject.js" > @@ -3067,7 +2005,6 @@ snapshots["open-scd layout looks like its snapshot"] = hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/NewProject.js" > @@ -3083,7 +2020,6 @@ snapshots["open-scd layout looks like its snapshot"] = hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/SaveProject.js" > @@ -3105,7 +2041,6 @@ snapshots["open-scd layout looks like its snapshot"] = hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/validators/ValidateSchema.js" > @@ -3121,7 +2056,6 @@ snapshots["open-scd layout looks like its snapshot"] = hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/validators/ValidateTemplates.js" > @@ -3143,7 +2077,6 @@ snapshots["open-scd layout looks like its snapshot"] = hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/ImportIEDs.js" > @@ -3159,7 +2092,6 @@ snapshots["open-scd layout looks like its snapshot"] = hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/VirtualTemplateIED.js" > @@ -3175,7 +2107,6 @@ snapshots["open-scd layout looks like its snapshot"] = hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/SubscriberInfo.js" > @@ -3191,7 +2122,6 @@ snapshots["open-scd layout looks like its snapshot"] = hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/UpdateDescriptionABB.js" > @@ -3207,7 +2137,6 @@ snapshots["open-scd layout looks like its snapshot"] = hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/UpdateDescriptionSEL.js" > @@ -3223,7 +2152,6 @@ snapshots["open-scd layout looks like its snapshot"] = hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/Merge.js" > @@ -3239,7 +2167,6 @@ snapshots["open-scd layout looks like its snapshot"] = hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/UpdateSubstation.js" > @@ -3255,7 +2182,6 @@ snapshots["open-scd layout looks like its snapshot"] = hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/CompareIED.js" > @@ -3271,7 +2197,6 @@ snapshots["open-scd layout looks like its snapshot"] = hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/ExportCommunication.js" > @@ -3293,7 +2218,6 @@ snapshots["open-scd layout looks like its snapshot"] = hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/SclHistory.js" > @@ -3309,7 +2233,6 @@ snapshots["open-scd layout looks like its snapshot"] = hasmeta="" left="" mwc-list-item="" - selected="" tabindex="-1" value="http://localhost:8000/plugins/src/menu/Help.js" > From 2273c2dab6799f3665b1897bc9abcbe0dab2c496 Mon Sep 17 00:00:00 2001 From: Sergio Alvarenga <71974638+salvar3nga@users.noreply.github.com> Date: Thu, 13 Feb 2025 10:08:20 +0100 Subject: [PATCH 10/28] feat: Attach files to release build (#1620) * create workflow for attaching files * feat: attach core and plugin docs to build --------- Co-authored-by: Sergio Alvarenga --- .github/workflows/attach-release-assets.yml | 38 +++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/attach-release-assets.yml diff --git a/.github/workflows/attach-release-assets.yml b/.github/workflows/attach-release-assets.yml new file mode 100644 index 0000000000..9d505e689c --- /dev/null +++ b/.github/workflows/attach-release-assets.yml @@ -0,0 +1,38 @@ +on: + release: + types: [published] # This triggers when a new release is published + +permissions: + contents: write + +name: attach-release-assets + +jobs: + attach-assets: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2.3.1 + - name: Use Node.js 18.x + uses: actions/setup-node@v1 + with: + node-version: "18.x" + - name: Install and Build OpenSCD + run: | + npm i @nx/nx-linux-x64-gnu + npm clean-install + npm run-script build + npm run-script doc + - name: Copy Core Docs to OpenSCD + run: cp -R packages/core/doc packages/distribution/build/core-doc + + - name: Copy Plugin Docs to OpenSCD + run: cp -R packages/plugins/doc packages/distribution/build/plugin-doc + + - name: Compress files + run: tar -czf open-scd.tar.gz -C packages/distribution/build + + - name: Upload release files + run: gh release upload ${{ steps.release.outputs.tag_name }} open-scd.tar.gz + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From cb008379a95e73dd79da2d8766dfd1e66dd9d96c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2025 11:27:50 +0100 Subject: [PATCH 11/28] chore(main): release 0.37.1 (#1617) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Sergio Alvarenga <71974638+salvar3nga@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index b8504b5303..0202474d1e 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,5 +1,5 @@ { "packages/openscd": "0.37.0", "packages/core": "0.1.4", - ".": "0.37.0" + ".": "0.37.1" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cae83f4ed..cb3c1aa280 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.37.1](https://github.com/openscd/open-scd/compare/v0.37.0...v0.37.1) (2025-02-04) + + +### Features + +* Programatic Plugin Activation ([#1611](https://github.com/openscd/open-scd/issues/1611)) ([d3b2a0a](https://github.com/openscd/open-scd/commit/d3b2a0a7b2d08d0ce5484567ebfe6c6d4e548c5e)) + ## [0.37.0](https://github.com/openscd/open-scd/compare/v0.36.0...v0.37.0) (2025-01-27) diff --git a/package-lock.json b/package-lock.json index ff33c6e2ce..af9e7b65be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "openscd-monorepo", - "version": "0.37.0", + "version": "0.37.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openscd-monorepo", - "version": "0.37.0", + "version": "0.37.1", "license": "Apache-2.0", "workspaces": [ "packages/*" diff --git a/package.json b/package.json index 6cefbfdfe6..c58cbb9c5d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openscd-monorepo", - "version": "0.37.0", + "version": "0.37.1", "description": "OpenSCD base distribution and plugins", "private": true, "workspaces": [ From 53f24cbc9b2be8407aa1420b5665d2a09e9051ea Mon Sep 17 00:00:00 2001 From: Sergio Alvarenga <71974638+salvar3nga@users.noreply.github.com> Date: Mon, 17 Feb 2025 16:09:48 +0100 Subject: [PATCH 12/28] feat: change release please Workflow trigger (#1624) * feat: change github actions workflow trigger * feat: trigger release-please only after release * chore: release 0.37.1 Release-As: 0.37.1 --------- Co-authored-by: Sergio Alvarenga --- .github/workflows/release-please.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 35dcc9ff7c..fec867a86e 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -2,8 +2,6 @@ on: push: branches: - main - paths: - - "packages/core/**" permissions: contents: write From 1e50fd935512172d97d923b62e25bd064d850ca0 Mon Sep 17 00:00:00 2001 From: Sergio Alvarenga <71974638+salvar3nga@users.noreply.github.com> Date: Tue, 18 Feb 2025 17:16:22 +0100 Subject: [PATCH 13/28] chore: update release please version to 0.37.2 (#1627) * chore: release 0.37.2 Release-As: 0.37.2 --------- Co-authored-by: Sergio Alvarenga From 00c4dc06f6d0cf1c39e4822a5b21d650d698785e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Russ?= Date: Wed, 19 Feb 2025 10:07:49 +0100 Subject: [PATCH 14/28] Fix: inconsistent plugin activation behaviour caused by refactoring (#1626) --- package-lock.json | 115 - packages/core/foundation/plugin.ts | 24 +- packages/openscd/package.json | 1 + packages/openscd/src/addons/Layout.ts | 352 +-- .../plugin-manager/custom-plugin-dialog.ts | 207 ++ .../addons/plugin-manager/plugin-manager.ts | 186 ++ packages/openscd/src/open-scd.ts | 105 +- packages/openscd/src/plugin.ts | 24 +- packages/openscd/src/plugins.ts | 73 +- .../__snapshots__/open-scd.test.snap.js | 2684 +++++------------ packages/openscd/test/unit/Plugging.test.ts | 617 ++-- .../editors/__snapshots__/IED.test.snap.js | 1 - 12 files changed, 1664 insertions(+), 2725 deletions(-) create mode 100644 packages/openscd/src/addons/plugin-manager/custom-plugin-dialog.ts create mode 100644 packages/openscd/src/addons/plugin-manager/plugin-manager.ts diff --git a/package-lock.json b/package-lock.json index af9e7b65be..abba14296e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2604,12 +2604,6 @@ "node": ">=6.9.0" } }, - "node_modules/@import-maps/resolve": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@import-maps/resolve/-/resolve-1.0.1.tgz", - "integrity": "sha512-tWZNBIS1CoekcwlMuyG2mr0a1Wo5lb5lEHwwWvZo+5GLgr3e9LLDTtmgtCWEwBpXMkxn9D+2W9j2FY6eZQq0tA==", - "dev": true - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -8731,114 +8725,6 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, - "node_modules/@web/dev-server-import-maps": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@web/dev-server-import-maps/-/dev-server-import-maps-0.2.1.tgz", - "integrity": "sha512-iGM7s4qenmTDUWC2iV0HoQ1NR5lAyRxVHOpWzTsFH/TnUZzP+YuL6QIFtB2v2v7URfhGL2l2WPIibmliToITcg==", - "dev": true, - "dependencies": { - "@import-maps/resolve": "^1.0.1", - "@types/parse5": "^6.0.1", - "@web/dev-server-core": "^0.7.2", - "@web/parse5-utils": "^2.1.0", - "parse5": "^6.0.1", - "picomatch": "^2.2.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@web/dev-server-import-maps/node_modules/@web/dev-server-core": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-0.7.5.tgz", - "integrity": "sha512-Da65zsiN6iZPMRuj4Oa6YPwvsmZmo5gtPWhW2lx3GTUf5CAEapjVpZVlUXnKPL7M7zRuk72jSsIl8lo+XpTCtw==", - "dev": true, - "dependencies": { - "@types/koa": "^2.11.6", - "@types/ws": "^7.4.0", - "@web/parse5-utils": "^2.1.0", - "chokidar": "^4.0.1", - "clone": "^2.1.2", - "es-module-lexer": "^1.0.0", - "get-stream": "^6.0.0", - "is-stream": "^2.0.0", - "isbinaryfile": "^5.0.0", - "koa": "^2.13.0", - "koa-etag": "^4.0.0", - "koa-send": "^5.0.1", - "koa-static": "^5.0.0", - "lru-cache": "^8.0.4", - "mime-types": "^2.1.27", - "parse5": "^6.0.1", - "picomatch": "^2.2.2", - "ws": "^7.5.10" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@web/dev-server-import-maps/node_modules/@web/parse5-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-2.1.0.tgz", - "integrity": "sha512-GzfK5disEJ6wEjoPwx8AVNwUe9gYIiwc+x//QYxYDAFKUp4Xb1OJAGLc2l2gVrSQmtPGLKrTRcW90Hv4pEq1qA==", - "dev": true, - "dependencies": { - "@types/parse5": "^6.0.1", - "parse5": "^6.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@web/dev-server-import-maps/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@web/dev-server-import-maps/node_modules/es-module-lexer": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", - "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", - "dev": true - }, - "node_modules/@web/dev-server-import-maps/node_modules/lru-cache": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", - "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==", - "dev": true, - "engines": { - "node": ">=16.14" - } - }, - "node_modules/@web/dev-server-import-maps/node_modules/parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "dev": true - }, - "node_modules/@web/dev-server-import-maps/node_modules/readdirp": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", - "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", - "dev": true, - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@web/dev-server-rollup": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@web/dev-server-rollup/-/dev-server-rollup-0.4.1.tgz", @@ -31694,7 +31580,6 @@ "@typescript-eslint/eslint-plugin": "^4.29.2", "@typescript-eslint/parser": "^4.29.2", "@web/dev-server-esbuild": "^0.2.16", - "@web/dev-server-import-maps": "^0.2.1", "@web/test-runner": "^0.13.22", "@web/test-runner-playwright": "^0.11.0", "concurrently": "^6.2.1", diff --git a/packages/core/foundation/plugin.ts b/packages/core/foundation/plugin.ts index 300bb19adc..712c666912 100644 --- a/packages/core/foundation/plugin.ts +++ b/packages/core/foundation/plugin.ts @@ -1,12 +1,28 @@ import { targetLocales } from '../locales.js'; export type Plugin = { + // name defines the name of the plugin name: string; - translations?: Record<(typeof targetLocales)[number], string>; + // src defines the path to the plugins source file src: string; - icon: string; - requireDoc?: boolean; + // kind defines the type of the plugin + kind: PluginKind; + // activeByDefault configures if the plugin should be active by default + // this is will be user when users resets the plugins + activeByDefault: boolean; + // icon stores the icon name of the Material Icon + icon?: string; + // active shows if the plugin currently is active active?: boolean; - position: ('top' | 'middle' | 'bottom') | number; + // requireDoc shows if the plugin requires a document to be loaded + requireDoc?: boolean; + // position defines the position of menu plugins + position?: MenuPosition + translations?: Record<(typeof targetLocales)[number], string>; }; + export type PluginSet = { menu: Plugin[]; editor: Plugin[] }; +export type PluginKind = 'editor' | 'menu' | 'validator'; +export const menuPosition = ['top', 'middle', 'bottom'] as const; +export type MenuPosition = (typeof menuPosition)[number]; + diff --git a/packages/openscd/package.json b/packages/openscd/package.json index 2c57aedc35..4b64689d4a 100644 --- a/packages/openscd/package.json +++ b/packages/openscd/package.json @@ -60,6 +60,7 @@ "test:manual": "web-test-runner --manual", "test:watch": "web-test-runner --watch", "test:unit": "web-test-runner --watch --group unit", + "test:unit:headless": "web-test-runner --watch --group unit --concurrency 1 --headless", "test:integration": "web-test-runner --watch --group integration", "doc:clean": "npx rimraf doc", "doc:typedoc": "typedoc --plugin none --out doc --entryPointStrategy expand ./src", diff --git a/packages/openscd/src/addons/Layout.ts b/packages/openscd/src/addons/Layout.ts index 68055f25f7..78c0cce4b3 100644 --- a/packages/openscd/src/addons/Layout.ts +++ b/packages/openscd/src/addons/Layout.ts @@ -54,10 +54,41 @@ import '@material/mwc-select'; import '@material/mwc-textfield'; import { nothing } from 'lit'; +import {OscdPluginManager} from "./plugin-manager/plugin-manager.js"; +import "./plugin-manager/plugin-manager.js"; +import {OscdCustomPluginDialog} from "./plugin-manager/custom-plugin-dialog.js"; +import "./plugin-manager/custom-plugin-dialog.js"; + @customElement('oscd-layout') export class OscdLayout extends LitElement { + /** The `XMLDocument` to be edited */ + @property({ attribute: false }) doc: XMLDocument | null = null; + /** The name of the current [[`doc`]] */ + @property({ type: String }) docName = ''; + /** Index of the last [[`EditorAction`]] applied. */ + @property({ type: Number }) editCount = -1; + /** The currently active editor tab. */ + @property({ type: Number }) activeTab = 0; + + /** The plugins to render the layout. */ + @property({ type: Array }) plugins: Plugin[] = []; + + /** The open-scd host element */ + @property({ type: Object }) host!: HTMLElement; + + @property({ type: Object }) historyState!: HistoryState; + + @state() validated: Promise = Promise.resolve(); + @state() shouldValidate = false; + + @query('#menu') menuUI!: Drawer; + @query('#pluginManager') pluginUI!: OscdPluginManager; + @query('#pluginList') pluginList!: List; + @query('#pluginAdd') pluginDownloadUI!: OscdCustomPluginDialog; + + render(): TemplateResult { return html`
    = Promise.resolve(); + private renderPlugging(): TemplateResult { + return html` ${this.renderPluginUI()} ${this.renderDownloadUI()} `; + } - @state() - shouldValidate = false; + /** Renders the "Add Custom Plug-in" UI*/ + protected renderDownloadUI(): TemplateResult { + return html` + + ` + } - @query('#menu') - menuUI!: Drawer; - @query('#pluginManager') - pluginUI!: Dialog; - @query('#pluginList') - pluginList!: List; - @query('#pluginAdd') - pluginDownloadUI!: Dialog; + /** + * Renders the plug-in management UI (turning plug-ins on/off) + */ + protected renderPluginUI(): TemplateResult { + return html` + + ` + } // Computed properties get validators(): Plugin[] { return this.plugins.filter( - plugin => plugin.installed && plugin.kind === 'validator' + plugin => plugin.active && plugin.kind === 'validator' ); } get menuEntries(): Plugin[] { return this.plugins.filter( - plugin => plugin.installed && plugin.kind === 'menu' + plugin => plugin.active && plugin.kind === 'menu' ); } get topMenu(): Plugin[] { @@ -226,7 +238,7 @@ export class OscdLayout extends LitElement { get editors(): Plugin[] { return this.plugins.filter( - plugin => plugin.installed && plugin.kind === 'editor' + plugin => plugin.active && plugin.kind === 'editor' ); } @@ -250,50 +262,6 @@ export class OscdLayout extends LitElement { fn(); } - private handleAddPlugin() { - const pluginSrcInput = ( - this.pluginDownloadUI.querySelector('#pluginSrcInput') - ); - const pluginNameInput = ( - this.pluginDownloadUI.querySelector('#pluginNameInput') - ); - const pluginKindList = ( - this.pluginDownloadUI.querySelector('#pluginKindList') - ); - const requireDoc = ( - this.pluginDownloadUI.querySelector('#requireDoc') - ); - const positionList = ( - this.pluginDownloadUI.querySelector('#menuPosition') - ); + // currently we only handley key shortcuts when users press ctrl + if(!e.ctrlKey){ return } + + const keyFunctionMap: {[key:string]: () => void} = { + 'm': () => this.menuUI.open = !this.menuUI.open, + 'o': () => this.menuUI.querySelector('mwc-list-item[iconid="folder_open"]')?.click(), + 'O': () => this.menuUI.querySelector('mwc-list-item[iconid="create_new_folder"]')?.click(), + 's': () => this.menuUI.querySelector('mwc-list-item[iconid="save"]')?.click(), + 'P': () => this.pluginUI.show(), + } - if ( - !( - pluginSrcInput.checkValidity() && - pluginNameInput.checkValidity() && - pluginKindList.selected && - requireDoc && - positionList.selected - ) - ) - return; - - this.dispatchEvent( - newAddExternalPluginEvent({ - src: pluginSrcInput.value, - name: pluginNameInput.value, - kind: (pluginKindList.selected).value, - requireDoc: requireDoc.checked, - position: positionList.value, - installed: true, - }) - ); + const fn = keyFunctionMap[e.key]; + if(!fn){ return; } - this.requestUpdate(); - this.pluginUI.requestUpdate(); - this.pluginDownloadUI.close(); + e.preventDefault(); + fn(); } connectedCallback(): void { super.connectedCallback(); - this.host.addEventListener('open-drawer', () => { - this.menuUI.open = true; - }) this.host.addEventListener('close-drawer', async () => { this.menuUI.open = false; }); @@ -383,7 +271,7 @@ export class CompasLayout extends LitElement { this.shouldValidate = true; await this.validated; - if (!this.shouldValidate) return; + if (!this.shouldValidate){ return; } this.shouldValidate = false; @@ -400,23 +288,63 @@ export class CompasLayout extends LitElement { this.handleKeyPress = this.handleKeyPress.bind(this); document.onkeydown = this.handleKeyPress; - this.host.addEventListener( - 'oscd-edit-completed', - (evt: EditCompletedEvent) => { - const initiator = evt.detail.initiator; + document.addEventListener("open-plugin-download", () => { + this.pluginDownloadUI.show(); + }); - if (initiator === 'undo') { - this.redoCount += 1; - } else if (initiator === 'redo') { - this.redoCount -= 1; - } + this.onUserInfo = this.onUserInfo.bind(this); + this.host.addEventListener('userinfo', this.onUserInfo); + } - this.requestUpdate(); + private generateMenu(plugins:Plugin[], kind: 'top' | 'middle' | 'bottom'): (MenuItem | 'divider')[]{ + return plugins.map(plugin => { + return { + icon: plugin.icon || pluginIcons['menu'], + name: plugin.name, + action: ae => { + this.dispatchEvent( + newPendingStateEvent( + (( + (( + (ae.target).items[ae.detail.index].nextElementSibling + )) + )).run() + ) + ); + }, + disabled: (): boolean => plugin.requireDoc! && this.doc === null, + content: () => { + if(plugin.content){ return plugin.content(); } + return html``; + }, + kind: kind, } - ); + }) + } - this.onUserInfo = this.onUserInfo.bind(this); - this.host.addEventListener('userinfo', this.onUserInfo); + private generateValidatorMenus(plugins: Plugin[]): (MenuItem | 'divider')[] { + return plugins.map(plugin =>{ + return { + icon: plugin.icon || pluginIcons['validator'], + name: plugin.name, + action: ae => { + this.dispatchEvent(newEmptyIssuesEvent(plugin.src)); + + this.dispatchEvent( + newPendingStateEvent( + (( + (( + (ae.target).items[ae.detail.index].nextElementSibling + )) + )).validate() + ) + ); + }, + disabled: (): boolean => this.doc === null, + content: plugin.content ?? (() => html``), + kind: 'validator', + } + }); } private onUserInfo(event: UserInfoEvent) { @@ -424,14 +352,17 @@ export class CompasLayout extends LitElement { } private renderMenuItem(me: MenuItem | 'divider'): TemplateResult { - if (me === 'divider') - return html`
  • `; - if (me.actionItem) return html``; + const isDivider = me === 'divider'; + const hasActionItem = me !== 'divider' && me.actionItem; + + if (isDivider) { return html`
  • `; } + if (hasActionItem){ return html``; } return html` ${me.icon} ${get(me.name)} @@ -439,24 +370,25 @@ export class CompasLayout extends LitElement { ? html`${me.hint}` : ''} - ${me.content ?? ''} + ${me.content ? me.content() : nothing} `; } private renderActionItem(me: MenuItem | 'divider'): TemplateResult { - if (me !== 'divider' && me.actionItem) - return html``; - else return html``; } private renderEditorTab({ name, icon }: Plugin): TemplateResult { - return html` `; + return html` `; } /** Renders top bar which features icon buttons for undo, redo, log, scl history and diagnostics*/ @@ -483,287 +415,155 @@ export class CompasLayout extends LitElement { `; } - /** Renders a drawer toolbar featuring the scl filename, enabled menu plugins, settings, help, scl history and plug-ins management */ + /** + * Renders a drawer toolbar featuring the scl filename, enabled menu plugins, + * settings, help, scl history and plug-ins management + */ protected renderAside(): TemplateResult { return html` ${get('menu.title')} - ${this.docName - ? html`${this.docName}` - : ''} + ${renderTitle(this.docName)} ) => { - //FIXME: dirty hack to be fixed in open-scd-core - // if clause not necessary when oscd... components in open-scd not list - if (ae.target instanceof List) - (( - this.menu.filter( - item => item !== 'divider' && !item.actionItem - )[ae.detail.index] - ))?.action?.(ae); - }} + @action=${makeListAction(this.menu)} > ${this.menu.map(this.renderMenuItem)} `; + + function renderTitle(docName?: string){ + if(!docName) return html``; + + return html`${docName}`; + } + + function makeListAction(menuItems : (MenuItem|'divider')[]){ + return function listAction(ae: CustomEvent){ + //FIXME: dirty hack to be fixed in open-scd-core + // if clause not necessary when oscd... components in open-scd not list + if (ae.target instanceof List) + (( + menuItems.filter( + item => item !== 'divider' && !item.actionItem + )[ae.detail.index] + ))?.action?.(ae); + } + } + } + + private calcActiveEditors(){ + const hasActiveDoc = Boolean(this.doc); + + return this.editors + .filter(editor => { + // this is necessary because `requireDoc` can be undefined + // and that is not the same as false + const doesNotRequireDoc = editor.requireDoc === false + return doesNotRequireDoc || hasActiveDoc + }) } /** Renders the enabled editor plugins and a tab bar to switch between them*/ protected renderContent(): TemplateResult { + const activeEditors = this.calcActiveEditors() + .map(this.renderEditorTab) + + const hasActiveEditors = activeEditors.length > 0; + if(!hasActiveEditors){ return html``; } + return html` - ${this.doc - ? html` - (this.activeTab = e.detail.index)} - > - ${this.editors.map(this.renderEditorTab)} - - ${this.editors[this.activeTab]?.content - ? this.editors[this.activeTab].content - : ``}` - : ``} + + ${activeEditors} + + ${renderEditorContent(this.editors, this.activeTab, this.doc)} `; - } - /** Renders the landing buttons (open project and new project)*/ - protected renderLanding(): TemplateResult { - return html` ${!this.doc - ? html`
    - ${(this.menu.filter(mi => mi !== 'divider')).map( - (mi: MenuItem, index) => - mi.kind === 'top' && !mi.disabled?.() - ? html` - -
    ${mi.name}
    -
    - ` - : html`` - )} -
    ` - : ``}`; + function renderEditorContent(editors: Plugin[], activeTab: number, doc: XMLDocument | null){ + const editor = editors[activeTab]; + const requireDoc = editor?.requireDoc + if(requireDoc && !doc) { return html`` } + + const content = editor?.content; + if(!content) { return html`` } + + return html`${content()}`; + } } - /** Renders the "Add Custom Plug-in" UI*/ - protected renderDownloadUI(): TemplateResult { - return html` - -
    -

    - ${get('plugins.add.warning')} -

    - - - ${get('plugins.editor')}${pluginIcons['editor']} - ${get('plugins.menu')}${pluginIcons['menu']} - - - ${get('plugins.validator')}${pluginIcons['validator']} - - -
    - - this.handleAddPlugin()} - > -
    - `; + private handleActivatedEditorTabByUser(e: CustomEvent): void { + const tabIndex = e.detail.index; + this.activateTab(tabIndex); } - private renderPluginKind( - type: PluginKind | MenuPosition, - plugins: Plugin[] - ): TemplateResult { - return html` - ${plugins.map( - plugin => - html` - ${plugin.icon || pluginIcons[plugin.kind]} - ${plugin.name} - ` - )} - `; + private handleActivateEditorByEvent(e: CustomEvent<{name: string, src: string}>): void { + const {name, src} = e.detail; + const editors = this.calcActiveEditors() + const wantedEditorIndex = editors.findIndex(editor => editor.name === name || editor.src === src) + if(wantedEditorIndex < 0){ return; } // TODO: log error + + this.activateTab(wantedEditorIndex); } - /** Renders the plug-in management UI (turning plug-ins on/off)*/ - protected renderPluginUI(): TemplateResult { - return html` - - - this.dispatchEvent(newSetPluginsEvent(e.detail.index))} - > - ${get(`plugins.editor`)}${pluginIcons['editor']} -
  • - ${this.renderPluginKind( - 'editor', - this.plugins.filter(p => p.kind === 'editor') - )} - ${get(`plugins.menu`)}${pluginIcons['menu']} -
  • - ${this.renderPluginKind( - 'top', - this.plugins.filter(p => p.kind === 'menu' && p.position === 'top') - )} -
  • - ${this.renderPluginKind( - 'validator', - this.plugins.filter(p => p.kind === 'validator') - )} -
  • - ${this.renderPluginKind( - 'middle', - this.plugins.filter( - p => p.kind === 'menu' && p.position === 'middle' - ) - )} -
  • - ${this.renderPluginKind( - 'bottom', - this.plugins.filter( - p => p.kind === 'menu' && p.position === 'bottom' - ) - )} -
    - { - this.dispatchEvent(newResetPluginsEvent()); - this.requestUpdate(); - }} - style="--mdc-theme-primary: var(--mdc-theme-error)" - > - - - this.pluginDownloadUI.show()} - > - -
    - `; + private activateTab(index: number){ + this.activeTab = index; } - private renderPlugging(): TemplateResult { - return html` ${this.renderPluginUI()} ${this.renderDownloadUI()} `; + private handleRunMenuByEvent(e: CustomEvent<{name: string}>): void { + + // TODO: this is a workaround, fix it + this.menuUI.open = true; + const menuEntry = this.menuUI.querySelector(`[data-name="${e.detail.name}"]`) as HTMLElement + const menuElement = menuEntry.nextElementSibling + if(!menuElement){ return; } // TODO: log error + + (menuElement as unknown as MenuPlugin).run() } - render(): TemplateResult { + /** + * Renders the landing buttons (open project and new project) + * it no document loaded we display the menu item that are in the position + * 'top' and are not disabled + * + * To enable replacement of this part we have to convert it to either an addon + * or a plugin + */ + protected renderLanding(): TemplateResult { + if(this.doc){ return html``; } + return html` - - ${this.renderHeader()} ${this.renderAside()} ${this.renderContent()} - ${this.renderLanding()} ${this.renderPlugging()} - `; +
    + ${renderMenuItems(this.menu, this.menuUI)} +
    ` + + function renderMenuItems(menuItemsAndDividers: (MenuItem | 'divider')[], menuUI: Drawer){ + + const menuItems = menuItemsAndDividers.filter(mi => mi !== 'divider') as MenuItem[]; + + return menuItems.map((mi: MenuItem, index) => { + if(mi.kind !== 'top' || mi.disabled?.()) { return html``; } + + return html` + +
    ${mi.name}
    +
    + ` + }) + + function clickListItem(index:number) { + const listItem = menuUI.querySelector('mwc-list')!.items[index]; + listItem.click(); + } + + } } static styles = css` From d6faece976115667ec9cd371d3a43cbbed5f495e Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Tue, 4 Mar 2025 15:30:25 +0100 Subject: [PATCH 19/28] chore: Fix build --- packages/compas-open-scd/public/js/plugins.js | 107 +-- .../src/addons/CompasLayout.ts | 7 +- packages/compas-open-scd/src/open-scd.ts | 618 +++++++++++++----- packages/openscd/src/addons/Layout.ts | 7 +- 4 files changed, 527 insertions(+), 212 deletions(-) diff --git a/packages/compas-open-scd/public/js/plugins.js b/packages/compas-open-scd/public/js/plugins.js index 49bf7595ec..35aa18af7e 100644 --- a/packages/compas-open-scd/public/js/plugins.js +++ b/packages/compas-open-scd/public/js/plugins.js @@ -3,119 +3,135 @@ export const officialPlugins = [ name: 'IED', src: '/plugins/src/editors/IED.js', icon: 'developer_board', - default: true, + activeByDefault: true, kind: 'editor', + requireDoc: true, }, { name: 'Substation', src: '/plugins/src/editors/Substation.js', icon: 'margin', - default: true, + activeByDefault: true, kind: 'editor', + requireDoc: true, }, { name: 'Single Line Diagram', src: '/plugins/src/editors/SingleLineDiagram.js', icon: 'edit', - default: true, + activeByDefault: true, kind: 'editor', + requireDoc: true, }, { name: 'Subscriber Message Binding (GOOSE)', src: '/plugins/src/editors/GooseSubscriberMessageBinding.js', icon: 'link', - default: false, + activeByDefault: false, kind: 'editor', + requireDoc: true, }, { name: 'Subscriber Data Binding (GOOSE)', src: '/plugins/src/editors/GooseSubscriberDataBinding.js', icon: 'link', - default: false, + activeByDefault: false, kind: 'editor', + requireDoc: true, }, { name: 'Subscriber Later Binding (GOOSE)', src: '/plugins/src/editors/GooseSubscriberLaterBinding.js', icon: 'link', - default: true, + activeByDefault: true, kind: 'editor', + requireDoc: true, }, { name: 'Subscriber Message Binding (SMV)', src: '/plugins/src/editors/SMVSubscriberMessageBinding.js', icon: 'link', - default: false, + activeByDefault: false, kind: 'editor', + requireDoc: true, }, { name: 'Subscriber Data Binding (SMV)', src: '/plugins/src/editors/SMVSubscriberDataBinding.js', icon: 'link', - default: false, + activeByDefault: false, kind: 'editor', + requireDoc: true, }, { name: 'Subscriber Later Binding (SMV)', src: '/plugins/src/editors/SMVSubscriberLaterBinding.js', icon: 'link', - default: true, + activeByDefault: true, kind: 'editor', + requireDoc: true, }, { name: 'Communication', src: '/plugins/src/editors/Communication.js', icon: 'settings_ethernet', - default: true, + activeByDefault: true, kind: 'editor', + requireDoc: true, }, { name: '104', src: '/plugins/src/editors/Protocol104.js', icon: 'settings_ethernet', - default: false, + activeByDefault: false, kind: 'editor', + requireDoc: true, }, { name: 'Templates', src: '/plugins/src/editors/Templates.js', icon: 'copy_all', - default: true, + activeByDefault: true, kind: 'editor', + requireDoc: true, }, { name: 'CoMPAS Versions', src: '/src/compas-editors/CompasVersions.js', icon: 'copy_all', - default: true, + activeByDefault: true, kind: 'editor', + requireDoc: true, }, { name: 'Publisher', src: '/external-plugins/oscd-publisher/oscd-publisher.js', icon: 'publish', - default: true, + activeByDefault: true, kind: 'editor', + requireDoc: true, }, { name: 'Communication Explorer', src: '/external-plugins/oscd-plugins/communication-explorer/0.0.31/index.js', icon: 'lan', - default: true, + activeByDefault: true, kind: 'editor', + requireDoc: true, }, { name: 'Cleanup', src: '/plugins/src/editors/Cleanup.js', icon: 'cleaning_services', - default: false, + activeByDefault: false, kind: 'editor', + requireDoc: true, }, { name: 'Open project', src: '/src/menu/CompasOpen.js', icon: 'folder_open', - default: true, + activeByDefault: true, kind: 'menu', requireDoc: false, position: 'top', @@ -124,7 +140,7 @@ export const officialPlugins = [ name: 'New project', src: '/plugins/src/menu/NewProject.js', icon: 'create_new_folder', - default: true, + activeByDefault: true, kind: 'menu', requireDoc: false, position: 'top', @@ -133,7 +149,7 @@ export const officialPlugins = [ name: 'Project from CIM', src: '/src/menu/CompasCimMapping.js', icon: 'input', - default: true, + activeByDefault: true, kind: 'menu', requireDoc: false, position: 'top', @@ -142,7 +158,7 @@ export const officialPlugins = [ name: 'Import from API', src: '/src/menu/CompasImportFromApi.js', icon: 'cloud_download', - default: false, + activeByDefault: false, kind: 'menu', requireDoc: false, position: 'top', @@ -151,7 +167,7 @@ export const officialPlugins = [ name: 'Save project', src: '/src/menu/CompasSave.js', icon: 'save', - default: true, + activeByDefault: true, kind: 'menu', requireDoc: true, position: 'top', @@ -160,7 +176,7 @@ export const officialPlugins = [ name: 'Save project as', src: '/src/menu/CompasSaveAs.js', icon: 'save', - default: true, + activeByDefault: true, kind: 'menu', requireDoc: true, position: 'top', @@ -169,7 +185,7 @@ export const officialPlugins = [ name: 'Save as version', src: '/src/menu/CompasSaveAsVersion.js', icon: 'save', - default: true, + activeByDefault: true, kind: 'menu', requireDoc: true, position: 'top', @@ -178,28 +194,28 @@ export const officialPlugins = [ name: '[WIP] Validate using OCL', src: '/src/validators/CompasValidateSchema.js', icon: 'rule_folder', - default: false, - kind: 'validator', + activeByDefault: false, + kind: 'validator' }, { name: 'Validate Schema', src: '/plugins/src/validators/ValidateSchema.js', icon: 'rule_folder', - default: true, + activeByDefault: true, kind: 'validator', }, { name: 'Validate Templates', src: '/plugins/src/validators/ValidateTemplates.js', icon: 'rule_folder', - default: true, + activeByDefault: true, kind: 'validator', }, { name: 'Import IEDs', src: '/src/menu/CompasImportIEDs.js', icon: 'snippet_folder', - default: true, + activeByDefault: true, kind: 'menu', requireDoc: true, position: 'middle', @@ -208,7 +224,7 @@ export const officialPlugins = [ name: 'Create Virtual IED', src: '/plugins/src/menu/VirtualTemplateIED.js', icon: 'developer_board', - default: false, + activeByDefault: false, kind: 'menu', requireDoc: true, position: 'middle', @@ -216,7 +232,7 @@ export const officialPlugins = [ { name: 'Subscriber Update', src: '/plugins/src/menu/SubscriberInfo.js', - default: true, + activeByDefault: true, kind: 'menu', requireDoc: true, position: 'middle', @@ -224,7 +240,7 @@ export const officialPlugins = [ { name: 'Update desc (ABB)', src: '/plugins/src/menu/UpdateDescriptionABB.js', - default: false, + activeByDefault: false, kind: 'menu', requireDoc: true, position: 'middle', @@ -232,7 +248,7 @@ export const officialPlugins = [ { name: 'Update desc (SEL)', src: '/plugins/src/menu/UpdateDescriptionSEL.js', - default: false, + activeByDefault: false, kind: 'menu', requireDoc: true, position: 'middle', @@ -241,7 +257,7 @@ export const officialPlugins = [ name: 'Merge Project', src: '/src/menu/CompasMerge.js', icon: 'merge_type', - default: true, + activeByDefault: true, kind: 'menu', requireDoc: true, position: 'middle', @@ -250,7 +266,7 @@ export const officialPlugins = [ name: 'Update Substation', src: '/src/menu/CompasUpdateSubstation.js', icon: 'merge_type', - default: true, + activeByDefault: true, kind: 'menu', requireDoc: true, position: 'middle', @@ -259,7 +275,7 @@ export const officialPlugins = [ name: 'Compare IED', src: '/src/menu/CompasCompareIED.js', icon: 'compare_arrows', - default: true, + activeByDefault: true, kind: 'menu', requireDoc: true, position: 'middle', @@ -268,7 +284,7 @@ export const officialPlugins = [ name: 'Auto Align SLD', src: '/src/menu/CompasAutoAlignment.js', icon: 'dashboard', - default: true, + activeByDefault: true, kind: 'menu', requireDoc: true, position: 'middle', @@ -277,7 +293,7 @@ export const officialPlugins = [ name: 'Export IED Params', src: '/src/menu/ExportIEDParams.js', icon: 'download', - default: false, + activeByDefault: false, kind: 'menu', requireDoc: true, position: 'middle', @@ -286,7 +302,7 @@ export const officialPlugins = [ name: 'Locamation VMU', src: '/src/menu/LocamationVMU.js', icon: 'edit_note', - default: false, + activeByDefault: false, kind: 'menu', requireDoc: true, position: 'middle', @@ -295,7 +311,7 @@ export const officialPlugins = [ name: 'Show SCL History', src: '/plugins/src/menu/SclHistory.js', icon: 'history_toggle_off', - default: true, + activeByDefault: true, kind: 'menu', requireDoc: true, position: 'bottom', @@ -304,7 +320,7 @@ export const officialPlugins = [ name: 'CoMPAS Settings', src: '/src/menu/CompasSettings.js', icon: 'settings', - default: true, + activeByDefault: true, kind: 'menu', requireDoc: false, position: 'bottom', @@ -313,7 +329,7 @@ export const officialPlugins = [ name: 'Help', src: '/plugins/src/menu/Help.js', icon: 'help', - default: true, + activeByDefault: true, kind: 'menu', requireDoc: false, position: 'bottom', @@ -322,7 +338,7 @@ export const officialPlugins = [ name: 'Export Communication Section', src: '/plugins/src/menu/ExportCommunication.js', icon: 'sim_card_download', - default: false, + activeByDefault: false, kind: 'menu', requireDoc: true, position: 'middle', @@ -331,14 +347,15 @@ export const officialPlugins = [ name: 'Sitipe', src: '/src/compas-editors/Sitipe.js', icon: 'precision_manufacturing', - default: true, + activeByDefault: true, kind: 'editor', + requireDoc: true }, { name: 'Autogen Substation', src: '/src/compas-editors/autogen-substation.js', icon: 'playlist_add_circle', - default: true, + activeByDefault: true, kind: 'menu', requireDoc: true, position: 'middle', @@ -347,7 +364,7 @@ export const officialPlugins = [ name: 'Export IEC 104 CSV', src: '/plugins/src/menu/Export104.js', icon: 'sim_card_download', - default: false, + activeByDefault: false, kind: 'menu', requireDoc: true, position: 'middle', diff --git a/packages/compas-open-scd/src/addons/CompasLayout.ts b/packages/compas-open-scd/src/addons/CompasLayout.ts index c8dc38e713..b55c65cc0b 100644 --- a/packages/compas-open-scd/src/addons/CompasLayout.ts +++ b/packages/compas-open-scd/src/addons/CompasLayout.ts @@ -352,10 +352,9 @@ export class CompasLayout extends LitElement { } private renderMenuItem(me: MenuItem | 'divider'): TemplateResult { - const isDivider = me === 'divider'; const hasActionItem = me !== 'divider' && me.actionItem; - if (isDivider) { return html`
  • `; } + if (isDivider(me)) { return html`
  • `; } if (hasActionItem){ return html``; } return html` - [a.kind, b.kind, a.position, b.position].includes(kind) - ); - return [a.kind, a.position].includes(earlier) ? -1 : 1; -} - -function compareNeedsDoc(a: Plugin, b: Plugin): -1 | 0 | 1 { - if (a.requireDoc === b.requireDoc) return 0; - return a.requireDoc ? 1 : -1; -} - -const loadedPlugins = new Set(); +import { newConfigurePluginEvent, ConfigurePluginEvent } from '@openscd/open-scd/src/plugin.events.js'; +import { newLogEvent } from '@openscd/core/foundation/deprecated/history.js'; /** The `` custom element is the main entry point of the * Open Substation Configuration Designer. */ @customElement('open-scd') export class OpenSCD extends LitElement { + render(): TemplateResult { + return html` + + + + + + this.setPlugins(e.detail.selectedPlugins) } + .host=${this} + .doc=${this.doc} + .docName=${this.docName} + .editCount=${this.historyState.editCount} + .historyState=${this.historyState} + .plugins=${this.storedPlugins} + > + + + + + + + `; + } + @property({ attribute: false }) doc: XMLDocument | null = null; /** The name of the current [[`doc`]] */ @@ -61,9 +83,12 @@ export class OpenSCD extends LitElement { /** The UUID of the current [[`doc`]] */ @property({ type: String }) docId = ''; - /** Index of the last [[`EditorAction`]] applied. */ @state() - editCount = -1; + historyState: HistoryState = { + editCount: -1, + canRedo: false, + canUndo: false, + } /** Object containing all *.nsdoc files and a function extracting element's label form them*/ @property({ attribute: false }) @@ -80,6 +105,8 @@ export class OpenSCD extends LitElement { this.dispatchEvent(newPendingStateEvent(this.loadDoc(value))); } + @state() private storedPlugins: Plugin[] = []; + /** Loads and parses an `XMLDocument` after [[`src`]] has changed. */ private async loadDoc(src: string): Promise { const response = await fetch(src); @@ -93,134 +120,203 @@ export class OpenSCD extends LitElement { if (src.startsWith('blob:')) URL.revokeObjectURL(src); } - connectedCallback(): void { - super.connectedCallback(); - this.addEventListener('reset-plugins', this.resetPlugins); - this.addEventListener( - 'add-external-plugin', - (e: AddExternalPluginEvent) => { - this.addExternalPlugin(e.detail.plugin); + /** + * + * @deprecated Use `handleConfigurationPluginEvent` instead + */ + public handleAddExternalPlugin(e: AddExternalPluginEvent){ + this.addExternalPlugin(e.detail.plugin); + const {name, kind} = e.detail.plugin + + const event = newConfigurePluginEvent(name,kind, e.detail.plugin) + + this.handleConfigurationPluginEvent(event) + } + + + public handleConfigurationPluginEvent(e: ConfigurePluginEvent){ + const { name, kind, config } = e.detail; + + const hasPlugin = this.hasPlugin(name, kind); + const hasConfig = config !== null; + const isChangeEvent = hasPlugin && hasConfig; + const isRemoveEvent = hasPlugin && !hasConfig; + const isAddEvent = !hasPlugin && hasConfig; + + // the `&& config`is only because typescript + // cannot infer that `isChangeEvent` and `isAddEvent` implies `config !== null` + if(isChangeEvent && config){ + this.changePlugin(config); + + }else if(isRemoveEvent){ + this.removePlugin(name, kind); + + }else if(isAddEvent && config){ + this.addPlugin(config); + + }else{ + const event = newLogEvent({ + kind: "error", + title: "Invalid plugin configuration event", + message: JSON.stringify({name, kind, config}), + }); + this.dispatchEvent(event); } - ); - this.addEventListener('set-plugins', (e: SetPluginsEvent) => { - this.setPlugins(e.detail.indices); - }); - - this.updatePlugins(); - this.requestUpdate(); - - this.addEventListener('oscd-edit-completed', (evt: EditCompletedEvent) => { - const initiator = evt.detail.initiator; + } - if (initiator === 'undo') { - this.editCount -= 1; - } else { - this.editCount += 1; - } + connectedCallback(): void { + super.connectedCallback(); + this.loadPlugins() + // TODO: let Lit handle the event listeners, move to render() + this.addEventListener('reset-plugins', this.resetPlugins); + this.addEventListener(historyStateEvent, (e: CustomEvent) => { + this.historyState = e.detail; this.requestUpdate(); }); } - render(): TemplateResult { - return html` - - - - - - - - - - - - - `; - } + /** + * + * @param name + * @param kind + * @returns the index of the plugin in the stored plugin list + */ + private findPluginIndex(name: string, kind: PluginKind): number { + return this.storedPlugins.findIndex(p => p.name === name && p.kind === kind); + } + + private hasPlugin(name: string, kind: PluginKind): boolean { + return this.findPluginIndex(name, kind) > -1; + } + + private removePlugin(name: string, kind: PluginKind) { + const newPlugins = this.storedPlugins.filter( + p => p.name !== name || p.kind !== kind + ); + this.updateStoredPlugins(newPlugins); + } + + private addPlugin(plugin: Plugin) { + const newPlugins = [...this.storedPlugins, plugin]; + this.updateStoredPlugins(newPlugins); + } + + /** + * + * @param plugin + * @throws if the plugin is not found + */ + private changePlugin(plugin: Plugin) { + const storedPlugins = this.storedPlugins; + const {name, kind} = plugin; + const pluginIndex = this.findPluginIndex(name, kind); + + if(pluginIndex < 0) { + const event = newLogEvent({ + kind: "error", + title: "Plugin not found, stopping change process", + message: JSON.stringify({name, kind}), + }) + this.dispatchEvent(event); + return; + } + + const pluginToChange = storedPlugins[pluginIndex] + const changedPlugin = {...pluginToChange, ...plugin} + const newPlugins = [...storedPlugins] + newPlugins.splice(pluginIndex, 1, changedPlugin) + + // this.storePlugins(newPlugins); + this.updateStoredPlugins(newPlugins); + } - private storePlugins(plugins: Array) { - localStorage.setItem( - 'plugins', - JSON.stringify(plugins.map(withoutContent)) - ); - this.requestUpdate(); - } private resetPlugins(): void { - this.storePlugins( - (officialPlugins as Plugin[]).concat(this.parsedPlugins).map(plugin => { - return { - src: plugin.src, - installed: plugin.default ?? false, - official: true, - }; - }) - ); + const builtInPlugins = this.getBuiltInPlugins() + const allPlugins = [...builtInPlugins, ...this.parsedPlugins] + + const newPluginConfigs = allPlugins.map(plugin => { + return { + ...plugin, + active: plugin.activeByDefault ?? false, + } + }) + + this.storePlugins(newPluginConfigs) } /** * @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, - })) - ); + @property({ type: Object }) plugins: PluginSet = { menu: [], editor: [] }; + +get parsedPlugins(): Plugin[] { + const menuPlugins: Plugin[] = this.plugins.menu.map((plugin) => { + let newPosition: MenuPosition | undefined = plugin.position as MenuPosition; + if(typeof plugin.position === 'number') { + newPosition = undefined + } + + return { + ...plugin, + position: newPosition, + kind: 'menu' as PluginKind, + active: plugin.active ?? false, + } + }) + + const editorPlugins: Plugin[] = this.plugins.editor.map((plugin) => { + const editorPlugin: Plugin = { + ...plugin, + position: undefined, + kind: 'editor' as PluginKind, + active: plugin.active ?? false, + } + return editorPlugin + }) + + const allPlugnis = [...menuPlugins, ...editorPlugins] + return allPlugnis } - private get sortedStoredPlugins(): Plugin[] { - const plugins = this.storedPlugins - .map(plugin => { - if (!plugin.official) return plugin; - const officialPlugin = (officialPlugins as Plugin[]) - .concat(this.parsedPlugins) - .find(needle => needle.src === plugin.src); + private updateStoredPlugins(newPlugins: Plugin[]) { + // + // Generate content of each plugin + // + const plugins = newPlugins.map(plugin => { + const isInstalled = plugin.src && plugin.active + if(!isInstalled) { return plugin } + + return this.addContent(plugin) + }) + + // + // Merge built-in plugins + // + const mergedPlugins = plugins.map(plugin => { + const isBuiltIn = !plugin?.official + if (!isBuiltIn){ return plugin } + + const builtInPlugin = [...this.getBuiltInPlugins(), ...this.parsedPlugins] + .find(p => p.src === plugin.src); + return { - ...officialPlugin, + ...builtInPlugin, ...plugin, }; - }) - .sort(compareNeedsDoc) - .sort(menuCompare); + }) + this.storePlugins(mergedPlugins); + } - return plugins; + private storePlugins(plugins: Plugin[]) { + this.storedPlugins = plugins + const pluginConfigs = JSON.stringify(plugins.map(withoutContent)) + localStorage.setItem('plugins', pluginConfigs); } - private get storedPlugins(): Plugin[] { - return ( - JSON.parse(localStorage.getItem('plugins') ?? '[]', (key, value) => - value.src && value.installed ? this.addContent(value) : value - ) - ); + private getPluginConfigsFromLocalStorage(): Plugin[] { + const pluginsConfigStr = localStorage.getItem('plugins') ?? '[]' + return JSON.parse(pluginsConfigStr) as Plugin[] } protected get locale(): string { @@ -237,39 +333,47 @@ export class OpenSCD extends LitElement { return docs; } - private setPlugins(indices: Set) { - const newPlugins = this.sortedStoredPlugins.map((plugin, index) => { - return { ...plugin, installed: indices.has(index) }; - }); - this.storePlugins(newPlugins); + private setPlugins(selectedPlugins: Plugin[]) { + + const newPlugins: Plugin[] = this.storedPlugins.map((storedPlugin) => { + const isSelected = selectedPlugins.some( (selectedPlugin) => { + return selectedPlugin.name === storedPlugin.name + && selectedPlugin.src === storedPlugin.src + }) + return { + ...storedPlugin, + active: isSelected + } + }) + + this.updateStoredPlugins(newPlugins); } - private updatePlugins() { - const stored: Plugin[] = this.storedPlugins; - const officialStored = stored.filter(p => p.official); - const newOfficial: Array = ( - officialPlugins as Plugin[] - ) - .concat(this.parsedPlugins) - .filter(p => !officialStored.find(o => o.src === p.src)) - .map(plugin => { - return { - src: plugin.src, - installed: plugin.default ?? false, - official: true as const, - }; - }); - const oldOfficial = officialStored.filter( - 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)); - this.storePlugins(newPlugins); + private loadPlugins(){ + const localPluginConfigs = this.getPluginConfigsFromLocalStorage() + + const overwritesOfBultInPlugins = localPluginConfigs.filter((p) => { + return this.getBuiltInPlugins().some(b => b.src === p.src) + }) + + const userInstalledPlugins = localPluginConfigs.filter((p) => { + return !this.getBuiltInPlugins().some(b => b.src === p.src) + }) + const mergedBuiltInPlugins = this.getBuiltInPlugins().map((builtInPlugin) => { + const overwrite = overwritesOfBultInPlugins.find(p => p.src === builtInPlugin.src) + + const mergedPlugin: Plugin = { + ...builtInPlugin, + ...overwrite, + active: overwrite?.active ?? builtInPlugin.activeByDefault, + } + + return mergedPlugin + }) + + const mergedPlugins = [...mergedBuiltInPlugins, ...userInstalledPlugins] + + this.updateStoredPlugins(mergedPlugins) } private async addExternalPlugin( @@ -282,18 +386,27 @@ export class OpenSCD extends LitElement { this.storePlugins(newPlugins); } + protected getBuiltInPlugins(): CorePlugin[] { + return builtinPlugins as CorePlugin[]; + } + private addContent(plugin: Omit): Plugin { - const tag = pluginTag(plugin.src); - if (!loadedPlugins.has(tag)) { - loadedPlugins.add(tag); - import(plugin.src).then(mod => customElements.define(tag, mod.default)); + const tag = this.pluginTag(plugin.src); + + if (!this.loadedPlugins.has(tag)) { + this.loadedPlugins.add(tag); + import(plugin.src).then((mod) => { + customElements.define(tag, mod.default) + }) } return { ...plugin, - content: staticTagHtml`<${tag} + content: () => { + return staticTagHtml`<${tag} .doc=${this.doc} .docName=${this.docName} - .editCount=${this.editCount} + .editCount=${this.historyState.editCount} + .plugins=${this.storedPlugins} .docId=${this.docId} .pluginId=${plugin.src} .nsdoc=${this.nsdoc} @@ -305,7 +418,186 @@ export class OpenSCD extends LitElement { validator: plugin.kind === 'validator', editor: plugin.kind === 'editor', })}" - >`, + >` + }, }; } + + @state() private loadedPlugins = new Set(); + + // PLUGGING INTERFACES + @state() private pluginTags = new Map(); + /** + * Hashes `uri` using cyrb64 analogous to + * https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js . + * @returns a valid customElement tagName containing the URI hash. + */ + private pluginTag(uri: string): string { + if (!this.pluginTags.has(uri)) { + let h1 = 0xdeadbeef, + h2 = 0x41c6ce57; + for (let i = 0, ch; i < uri.length; i++) { + ch = uri.charCodeAt(i); + h1 = Math.imul(h1 ^ ch, 2654435761); + h2 = Math.imul(h2 ^ ch, 1597334677); + } + h1 = + Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ + Math.imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = + Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ + Math.imul(h1 ^ (h1 >>> 13), 3266489909); + this.pluginTags.set( + uri, + 'oscd-plugin' + + ((h2 >>> 0).toString(16).padStart(8, '0') + + (h1 >>> 0).toString(16).padStart(8, '0')) + ); + } + return this.pluginTags.get(uri)!; + } +} + +declare global { + interface ElementEventMap { + 'reset-plugins': CustomEvent; + 'add-external-plugin': CustomEvent; + 'set-plugins': CustomEvent; + } +} + + +// HOSTING INTERFACES + +export interface MenuItem { + icon: string; + name: string; + hint?: string; + actionItem?: boolean; + action?: (event: CustomEvent) => void; + disabled?: () => boolean; + content: () => TemplateResult; + kind: string; +} + +export interface Validator { + validate: () => Promise; +} + +export interface MenuPlugin { + run: () => Promise; +} + +export function newResetPluginsEvent(): CustomEvent { + return new CustomEvent('reset-plugins', { bubbles: true, composed: true }); +} + +export interface AddExternalPluginDetail { + plugin: Omit; +} + +export type AddExternalPluginEvent = CustomEvent; + +export function newAddExternalPluginEvent( + plugin: Omit +): AddExternalPluginEvent { + return new CustomEvent('add-external-plugin', { + bubbles: true, + composed: true, + detail: { plugin }, + }); +} + +export interface SetPluginsDetail { + selectedPlugins: Plugin[]; +} + +export type SetPluginsEvent = CustomEvent; + +export function newSetPluginsEvent(selectedPlugins: Plugin[]): SetPluginsEvent { + return new CustomEvent('set-plugins', { + bubbles: true, + composed: true, + detail: { selectedPlugins }, + }); +} + + + + +/** + * This is a template literal tag function. See: + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates + * + * Passes its arguments to LitElement's `html` tag after combining the first and + * last expressions with the first two and last two static strings. + * Throws unless the first and last expressions are identical strings. + * + * We need this to get around the expression location limitations documented in + * https://lit.dev/docs/templates/expressions/#expression-locations + * + * After upgrading to Lit 2 we can use their static HTML functions instead: + * https://lit.dev/docs/api/static-html/ + */ +function staticTagHtml( + oldStrings: ReadonlyArray, + ...oldArgs: unknown[] +): TemplateResult { + const args = [...oldArgs]; + const firstArg = args.shift(); + const lastArg = args.pop(); + + if (firstArg !== lastArg) + throw new Error( + `Opening tag <${firstArg}> does not match closing tag .` + ); + + const strings = [...oldStrings] as string[] & { raw: string[] }; + const firstString = strings.shift(); + const secondString = strings.shift(); + + const lastString = strings.pop(); + const penultimateString = strings.pop(); + + strings.unshift(`${firstString}${firstArg}${secondString}`); + strings.push(`${penultimateString}${lastArg}${lastString}`); + + return html(strings, ...args); +} + + +function withoutContent

    ( + plugin: P +): P { + return { ...plugin, content: undefined }; +} + +export const pluginIcons: Record = { + editor: 'tab', + menu: 'play_circle', + validator: 'rule_folder', + top: 'play_circle', + middle: 'play_circle', + bottom: 'play_circle', +}; + +const menuOrder: (PluginKind | MenuPosition)[] = [ + 'editor', + 'top', + 'validator', + 'middle', + 'bottom', +]; + +function menuCompare(a: Plugin, b: Plugin): -1 | 0 | 1 { + if (a.kind === b.kind && a.position === b.position) return 0; + const earlier = menuOrder.find(kind => + [a.kind, b.kind, a.position, b.position].includes(kind) + ); + return [a.kind, a.position].includes(earlier) ? -1 : 1; +} + +function compareNeedsDoc(a: Plugin, b: Plugin): -1 | 0 | 1 { + if (a.requireDoc === b.requireDoc) return 0; + return a.requireDoc ? 1 : -1; } diff --git a/packages/openscd/src/addons/Layout.ts b/packages/openscd/src/addons/Layout.ts index 78c0cce4b3..d38a037092 100644 --- a/packages/openscd/src/addons/Layout.ts +++ b/packages/openscd/src/addons/Layout.ts @@ -346,10 +346,9 @@ export class OscdLayout extends LitElement { } private renderMenuItem(me: MenuItem | 'divider'): TemplateResult { - const isDivider = me === 'divider'; const hasActionItem = me !== 'divider' && me.actionItem; - if (isDivider) { return html`

  • `; } + if (isDivider(me)) { return html`
  • `; } if (hasActionItem){ return html``; } return html` Date: Wed, 5 Mar 2025 13:35:48 +0100 Subject: [PATCH 20/28] chore: Remove unnecessary workflow --- .github/workflows/attach-release-assets.yml | 38 --------------------- 1 file changed, 38 deletions(-) delete mode 100644 .github/workflows/attach-release-assets.yml diff --git a/.github/workflows/attach-release-assets.yml b/.github/workflows/attach-release-assets.yml deleted file mode 100644 index 9d505e689c..0000000000 --- a/.github/workflows/attach-release-assets.yml +++ /dev/null @@ -1,38 +0,0 @@ -on: - release: - types: [published] # This triggers when a new release is published - -permissions: - contents: write - -name: attach-release-assets - -jobs: - attach-assets: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2.3.1 - - name: Use Node.js 18.x - uses: actions/setup-node@v1 - with: - node-version: "18.x" - - name: Install and Build OpenSCD - run: | - npm i @nx/nx-linux-x64-gnu - npm clean-install - npm run-script build - npm run-script doc - - name: Copy Core Docs to OpenSCD - run: cp -R packages/core/doc packages/distribution/build/core-doc - - - name: Copy Plugin Docs to OpenSCD - run: cp -R packages/plugins/doc packages/distribution/build/plugin-doc - - - name: Compress files - run: tar -czf open-scd.tar.gz -C packages/distribution/build - - - name: Upload release files - run: gh release upload ${{ steps.release.outputs.tag_name }} open-scd.tar.gz - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 510329aa3bbdb2ceb6dc7b40c1f68ac9f6f1594a Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Wed, 5 Mar 2025 13:37:43 +0100 Subject: [PATCH 21/28] chore: Use ubuntu 22 build agent for workflow --- .github/workflows/build-project.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-project.yml b/.github/workflows/build-project.yml index 0b757a2427..0f42887b75 100644 --- a/.github/workflows/build-project.yml +++ b/.github/workflows/build-project.yml @@ -15,7 +15,7 @@ on: jobs: build: name: Build - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 timeout-minutes: 45 steps: From c3361f1b13a0226fe2f92c72a7de62e15554ece4 Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Wed, 5 Mar 2025 16:31:37 +0100 Subject: [PATCH 22/28] chore: Rename original open scd selector to avoid name collision --- packages/openscd/src/open-scd.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/openscd/src/open-scd.ts b/packages/openscd/src/open-scd.ts index 0c2db48a82..1e1aff301f 100644 --- a/packages/openscd/src/open-scd.ts +++ b/packages/openscd/src/open-scd.ts @@ -56,7 +56,8 @@ import { newLogEvent } from '@openscd/core/foundation/deprecated/history'; /** The `` custom element is the main entry point of the * Open Substation Configuration Designer. */ -@customElement('open-scd') +// Change selector to avoid conflicts with open-scd from compas +@customElement('original-open-scd') export class OpenSCD extends LitElement { render(): TemplateResult { From 6a2fef078128ac82784f6ae37c997fb037b207ec Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Wed, 5 Mar 2025 16:35:53 +0100 Subject: [PATCH 23/28] chore: Fix compas open --- packages/compas-open-scd/src/addons/CompasLayout.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/compas-open-scd/src/addons/CompasLayout.ts b/packages/compas-open-scd/src/addons/CompasLayout.ts index b55c65cc0b..79eddfcc43 100644 --- a/packages/compas-open-scd/src/addons/CompasLayout.ts +++ b/packages/compas-open-scd/src/addons/CompasLayout.ts @@ -264,6 +264,9 @@ export class CompasLayout extends LitElement { connectedCallback(): void { super.connectedCallback(); + this.host.addEventListener('open-drawer', () => { + this.menuUI.open = true; + }) this.host.addEventListener('close-drawer', async () => { this.menuUI.open = false; }); From 64ab17f9349520d9ba31a9a646871033906beaff Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Thu, 6 Mar 2025 13:02:04 +0100 Subject: [PATCH 24/28] chore: Fix after merge --- packages/compas-open-scd/public/js/plugins.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/compas-open-scd/public/js/plugins.js b/packages/compas-open-scd/public/js/plugins.js index 30b88aa1c0..da63bd3acf 100644 --- a/packages/compas-open-scd/public/js/plugins.js +++ b/packages/compas-open-scd/public/js/plugins.js @@ -131,8 +131,9 @@ export const officialPlugins = [ name: 'Subscribe (Later Binding)', src: '/external-plugins/oscd-subscriber-later-binding/oscd-subscriber-later-binding.js', icon: 'link', - default: false, + activeByDefault: false, kind: 'editor', + requireDoc: true }, { name: 'Open project', From e3be3188348eb2745d3d954df152672a7ac5d5e5 Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Thu, 6 Mar 2025 13:12:47 +0100 Subject: [PATCH 25/28] chore: Raise version to 0.38.0.1 --- packages/compas-open-scd/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compas-open-scd/package.json b/packages/compas-open-scd/package.json index 41d0b6d49f..e26429b02b 100644 --- a/packages/compas-open-scd/package.json +++ b/packages/compas-open-scd/package.json @@ -1,6 +1,6 @@ { "name": "compas-open-scd", - "version": "0.36.0-4", + "version": "0.38.0-1", "repository": "https://github.com/openscd/open-scd.git", "description": "OpenSCD CoMPAS Edition", "directory": "packages/compas-open-scd", From c4a337679a60c41cd1eaf888cd75024ff018c3ba Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Thu, 6 Mar 2025 13:43:10 +0100 Subject: [PATCH 26/28] fix: Fix local build by deleting snowpack cache for lit --- packages/compas-open-scd/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compas-open-scd/package.json b/packages/compas-open-scd/package.json index e26429b02b..26241bcf47 100644 --- a/packages/compas-open-scd/package.json +++ b/packages/compas-open-scd/package.json @@ -71,7 +71,7 @@ "build:test": "npm run test && npm run build && cp .nojekyll build/", "build": "npm run doc && npm run build:only && cp .nojekyll build/", "build:only": "snowpack build && workbox generateSW workbox-config.cjs", - "start": "snowpack dev" + "start": "npx rimraf node_modules/.cache/snowpack/build/lit@2.8.0 && snowpack dev" }, "devDependencies": { "@commitlint/cli": "^13.1.0", From 56328f66113d7de3497d7e8865a896a9a412e0c5 Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Thu, 6 Mar 2025 13:57:08 +0100 Subject: [PATCH 27/28] chore: Comment start script --- packages/compas-open-scd/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/compas-open-scd/package.json b/packages/compas-open-scd/package.json index 26241bcf47..3e7683c641 100644 --- a/packages/compas-open-scd/package.json +++ b/packages/compas-open-scd/package.json @@ -71,6 +71,7 @@ "build:test": "npm run test && npm run build && cp .nojekyll build/", "build": "npm run doc && npm run build:only && cp .nojekyll build/", "build:only": "snowpack build && workbox generateSW workbox-config.cjs", + "__comment:start": "snowpack dev fails if the lit package is cached. I don't know why, but we have to delete it before starting", "start": "npx rimraf node_modules/.cache/snowpack/build/lit@2.8.0 && snowpack dev" }, "devDependencies": { From ae8bd71adf65fdeeae14fbccd89c882e58a67592 Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Fri, 7 Mar 2025 13:06:32 +0100 Subject: [PATCH 28/28] chore: Remove unused imports --- packages/compas-open-scd/src/addons/CompasLayout.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/compas-open-scd/src/addons/CompasLayout.ts b/packages/compas-open-scd/src/addons/CompasLayout.ts index 79eddfcc43..7db59a7689 100644 --- a/packages/compas-open-scd/src/addons/CompasLayout.ts +++ b/packages/compas-open-scd/src/addons/CompasLayout.ts @@ -16,15 +16,9 @@ import { Validator, MenuPlugin, pluginIcons, - newResetPluginsEvent, - newAddExternalPluginEvent, - newSetPluginsEvent, } from '@openscd/open-scd/src/open-scd.js'; import { Plugin, - PluginKind, - MenuPosition, - menuPosition } from '@openscd/open-scd/src/plugin.js' import { HistoryUIKind, @@ -37,11 +31,6 @@ import type { Drawer } from '@material/mwc-drawer'; import type { ActionDetail } from '@material/mwc-list'; import { List } from '@material/mwc-list'; import type { ListItem } from '@material/mwc-list/mwc-list-item'; -import type { Dialog } from '@material/mwc-dialog'; -import type { MultiSelectedEvent } from '@material/mwc-list/mwc-list-foundation.js'; -import type { Select } from '@material/mwc-select'; -import type { Switch } from '@material/mwc-switch'; -import type { TextField } from '@material/mwc-textfield'; import '@material/mwc-drawer'; import '@material/mwc-list';