From 8c620eb97fb34a0a01e2ac21cefbb87950e5a6b4 Mon Sep 17 00:00:00 2001 From: danyill Date: Mon, 26 Jun 2023 20:57:54 +1200 Subject: [PATCH 01/15] fix(editors/subscription): Increase timeout for failing subscriber/fcda-binding-list test, closes #1257 (#1274) --- test/unit/editors/subscription/fcda-binding-list.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/editors/subscription/fcda-binding-list.test.ts b/test/unit/editors/subscription/fcda-binding-list.test.ts index 33ecc45e3..594821b6f 100644 --- a/test/unit/editors/subscription/fcda-binding-list.test.ts +++ b/test/unit/editors/subscription/fcda-binding-list.test.ts @@ -174,7 +174,7 @@ describe('fcda-binding-list', () => { (( element.actionsMenu!.querySelector('.filter-subscribed') ))!.click(); - await new Promise(resolve => setTimeout(resolve, 200)); // await animation + await new Promise(resolve => setTimeout(resolve, 300)); // await animation await element.updateComplete; element.actionsMenuIcon.click(); @@ -182,7 +182,7 @@ describe('fcda-binding-list', () => { (( element.actionsMenu!.querySelector('.filter-not-subscribed') ))!.click(); - await new Promise(resolve => setTimeout(resolve, 200)); // await animation + await new Promise(resolve => setTimeout(resolve, 300)); // await animation await element.updateComplete; const fcdaList = element.shadowRoot?.querySelector('filtered-list'); From baa9bdcd73bb6db9ab2956dfd58344bc8859262d Mon Sep 17 00:00:00 2001 From: danyill Date: Mon, 26 Jun 2023 22:47:15 +1200 Subject: [PATCH 02/15] fix(wizards/doTypes): Adjust regular expressions for v flag in template editor (#1273) fix(wizards/foundation): Escape limit regexes, closes #1271 --- src/wizards/foundation/limits.ts | 4 +- .../dotype-wizarding.test.snap.js | 10 -- .../templates/dotype-wizarding.test.ts | 137 +++++++++++++++++- 3 files changed, 135 insertions(+), 16 deletions(-) diff --git a/src/wizards/foundation/limits.ts b/src/wizards/foundation/limits.ts index 8096b637a..b7e66518c 100644 --- a/src/wizards/foundation/limits.ts +++ b/src/wizards/foundation/limits.ts @@ -3,7 +3,7 @@ const nameStartChar = '|[\u037F-\u1FFF]|[\u200C-\u200D]|[\u2070-\u218F]|[\u2C00-\u2FEF]' + '|[\u3001-\uD7FF]|[\uF900-\uFDCF]|[\uFDF0-\uFFFD]'; const nameChar = - nameStartChar + '|[.0-9-]|\u00B7|[\u0300-\u036F]|[\u203F-\u2040]'; + nameStartChar + '|[.0-9\\-]|\u00B7|[\u0300-\u036F]|[\u203F-\u2040]'; const name = nameStartChar + '(' + nameChar + ')*'; const nmToken = '(' + nameChar + ')+'; @@ -17,7 +17,7 @@ export const patterns = { nmToken, names: name + '( ' + name + ')*', nmTokens: nmToken + '( ' + nmToken + ')*', - decimal: '[+-]?[0-9]+(([.][0-9]*)?|([.][0-9]+))', + decimal: '[+\\-]?[0-9]+(([.][0-9]*)?|([.][0-9]+))', unsigned: '[+]?[0-9]+(([.][0-9]*)?|([.][0-9]+))', alphanumericFirstUpperCase: '[A-Z][0-9,A-Z,a-z]*', asciName: '[A-Za-z][0-9,A-Z,a-z_]*', diff --git a/test/integration/editors/templates/__snapshots__/dotype-wizarding.test.snap.js b/test/integration/editors/templates/__snapshots__/dotype-wizarding.test.snap.js index c87158ebc..d05a9d404 100644 --- a/test/integration/editors/templates/__snapshots__/dotype-wizarding.test.snap.js +++ b/test/integration/editors/templates/__snapshots__/dotype-wizarding.test.snap.js @@ -679,7 +679,6 @@ snapshots["DOType wizards defines a createDOTypeWizard looks like the latest sna label="id" maxlength="127" minlength="1" - pattern="([:_A-Za-z]|[À-Ö]|[Ø-ö]|[ø-˿]|[Ͱ-ͽ]|[Ϳ-῿]|[‌-‍]|[⁰-↏]|[Ⰰ-⿯]|[、-퟿]|[豈-﷏]|[ﷰ-�]|[.0-9-]|·|[̀-ͯ]|[‿-⁀])+" required="" > @@ -688,13 +687,11 @@ snapshots["DOType wizards defines a createDOTypeWizard looks like the latest sna helper="[scl.desc]" label="desc" nullable="" - pattern="([ -~]|[…]|[ -퟿]|[-�])*" > @@ -783,7 +780,6 @@ snapshots["DOType wizards defines a dOTypeWizard looks like the latest snapshot" label="id" maxlength="127" minlength="1" - pattern="([:_A-Za-z]|[À-Ö]|[Ø-ö]|[ø-˿]|[Ͱ-ͽ]|[Ϳ-῿]|[‌-‍]|[⁰-↏]|[Ⰰ-⿯]|[、-퟿]|[豈-﷏]|[ﷰ-�]|[.0-9-]|·|[̀-ͯ]|[‿-⁀])+" required="" > @@ -792,13 +788,11 @@ snapshots["DOType wizards defines a dOTypeWizard looks like the latest snapshot" helper="[scl.desc]" label="desc" nullable="" - pattern="([ -~]|[…]|[ -퟿]|[-�])*" > @@ -1068,7 +1062,6 @@ snapshots["DOType wizards defines a sDOWizard to edit an existing SDO looks like dialoginitialfocus="" helper="[scl.name]" label="name" - pattern="[a-z][0-9A-Za-z]*" required="" > > @@ -1078,7 +1071,6 @@ snapshots["DOType wizards defines a sDOWizard to edit an existing SDO looks like helper="[scl.desc]" label="desc" nullable="" - pattern="([ -~]|[…]|[ -퟿]|[-�])*" > > @@ -1266,7 +1257,6 @@ snapshots["DOType wizards defines a sDOWizard to create a new SDO element looks helper="[scl.desc]" label="desc" nullable="" - pattern="([ -~]|[…]|[ -퟿]|[-�])*" > { if (customElements.get('templates-editor') === undefined) customElements.define('templates-editor', TemplatesPlugin); @@ -69,7 +71,41 @@ describe('DOType wizards', () => { }); it('looks like the latest snapshot', async () => { - await expect(parent.wizardUI.dialog).to.equalSnapshot(); + // prettier does not support escaping in regexes of the /v flag + await expect(parent.wizardUI.dialog).dom.to.equalSnapshot({ + ignoreAttributes: [ + { + tags: ['wizard-textfield'], + attributes: ['pattern'], + }, + ], + }); + }); + + // work around, because the escapes get removed in snapshot by prettier + it('should have correct pattern', async () => { + expect( + parent.wizardUI.dialog!.querySelectorAll('wizard-textfield[pattern]')! + .length + ).to.equal(3); + + expect( + parent.wizardUI + .dialog!.querySelectorAll('wizard-textfield[pattern]')[0] + .getAttribute('pattern') + ).to.equal(patterns.nmToken); + + expect( + parent.wizardUI + .dialog!.querySelectorAll('wizard-textfield[pattern]')[1] + .getAttribute('pattern') + ).to.equal(patterns.normalizedString); + + expect( + parent.wizardUI + .dialog!.querySelectorAll('wizard-textfield[pattern]')[2] + .getAttribute('pattern') + ).to.equal(patterns.cdc); }); it('allows to add empty DOTypes to the project', async () => { @@ -171,7 +207,41 @@ describe('DOType wizards', () => { }); it('looks like the latest snapshot', async () => { - await expect(parent.wizardUI.dialog).to.equalSnapshot(); + // prettier does not support escaping in regexes of the /v flag + await expect(parent.wizardUI.dialog).dom.to.equalSnapshot({ + ignoreAttributes: [ + { + tags: ['wizard-textfield'], + attributes: ['pattern'], + }, + ], + }); + }); + + // work around, because the escapes get removed in snapshot by prettier + it('should have correct pattern', async () => { + expect( + parent.wizardUI.dialog!.querySelectorAll('wizard-textfield[pattern]')! + .length + ).to.equal(3); + + expect( + parent.wizardUI + .dialog!.querySelectorAll('wizard-textfield[pattern]')[0] + .getAttribute('pattern') + ).to.equal(patterns.nmToken); + + expect( + parent.wizardUI + .dialog!.querySelectorAll('wizard-textfield[pattern]')[1] + .getAttribute('pattern') + ).to.equal(patterns.normalizedString); + + expect( + parent.wizardUI + .dialog!.querySelectorAll('wizard-textfield[pattern]')[2] + .getAttribute('pattern') + ).to.equal(patterns.normalizedString); }); it('edits DOType attributes id', async () => { @@ -242,8 +312,37 @@ describe('DOType wizards', () => { }); it('looks like the latest snapshot', async () => { - await expect(parent.wizardUI.dialog).to.equalSnapshot(); + // prettier does not support escaping in regexes of the /v flag + await expect(parent.wizardUI.dialog).dom.to.equalSnapshot({ + ignoreAttributes: [ + { + tags: ['wizard-textfield'], + attributes: ['pattern'], + }, + ], + }); + }); + + // work around, because the escapes get removed in snapshot by prettier + it('should have correct pattern', async () => { + expect( + parent.wizardUI.dialog!.querySelectorAll('wizard-textfield[pattern]')! + .length + ).to.equal(2); + + expect( + parent.wizardUI + .dialog!.querySelectorAll('wizard-textfield[pattern]')[0] + .getAttribute('pattern') + ).to.equal(patterns.tRestrName1stL); + + expect( + parent.wizardUI + .dialog!.querySelectorAll('wizard-textfield[pattern]')[1] + .getAttribute('pattern') + ).to.equal(patterns.normalizedString); }); + it('edits SDO attributes name', async () => { expect(doc.querySelector('DOType[id="Dummy.WYE"] > SDO[name="phsA"]')).to .exist; @@ -330,8 +429,37 @@ describe('DOType wizards', () => { }); it('looks like the latest snapshot', async () => { - await expect(parent.wizardUI.dialog).to.equalSnapshot(); + // prettier does not support escaping in regexes of the /v flag + await expect(parent.wizardUI.dialog).dom.to.equalSnapshot({ + ignoreAttributes: [ + { + tags: ['wizard-textfield'], + attributes: ['pattern'], + }, + ], + }); }); + + // work around, because the escapes get removed in snapshot by prettier + it('should have correct pattern', async () => { + expect( + parent.wizardUI.dialog!.querySelectorAll('wizard-textfield[pattern]')! + .length + ).to.equal(2); + + expect( + parent.wizardUI + .dialog!.querySelectorAll('wizard-textfield[pattern]')[0] + .getAttribute('pattern') + ).to.equal(patterns.tRestrName1stL); + + expect( + parent.wizardUI + .dialog!.querySelectorAll('wizard-textfield[pattern]')[1] + .getAttribute('pattern') + ).to.equal(patterns.normalizedString); + }); + it('creates a new SDO element', async () => { expect( doc.querySelector( @@ -349,6 +477,7 @@ describe('DOType wizards', () => { ) ).to.exist; }); + it('creates yet another new SDO element', async () => { const name = 'newSDOElement2'; const desc = 'newSDOdesc'; From ebde77000373780dccfa6f345126732667be1c43 Mon Sep 17 00:00:00 2001 From: danyill Date: Thu, 29 Jun 2023 07:28:45 +1200 Subject: [PATCH 03/15] fix(open-scd): Make linear progress bar Github stylez, closes #1269 (#1276) fix(open-scd): Make linear progress bar Github stylez, closes #1269 --- src/open-scd.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/open-scd.ts b/src/open-scd.ts index 6c2f7900b..3afa2c861 100644 --- a/src/open-scd.ts +++ b/src/open-scd.ts @@ -121,17 +121,13 @@ export class OpenSCD extends Waiting( mwc-linear-progress { position: fixed; - --mdc-theme-primary: var(--mdc-theme-secondary); + --mdc-linear-progress-buffer-color: var(--primary); + --mdc-theme-primary: var(--secondary); left: 0px; - top: 112px; + top: 0px; width: 100%; pointer-events: none; - } - - @media (max-width: 599px) { - mwc-linear-progress { - top: 104px; - } + z-index: 1000; } tt { From 1df6842002891223cf7a58821494731c01be73a5 Mon Sep 17 00:00:00 2001 From: danyill Date: Sun, 2 Jul 2023 06:46:51 +1200 Subject: [PATCH 04/15] fix(editors/IED): Allow IEDs to be updated after edit count change (closes #1272) (#1275) Co-authored-by: cad --- src/editors/IED.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/editors/IED.ts b/src/editors/IED.ts index 2f239e9b9..196587751 100644 --- a/src/editors/IED.ts +++ b/src/editors/IED.ts @@ -96,7 +96,11 @@ export default class IedPlugin extends LitElement { super.updated(_changedProperties); // When the document is updated, we reset the selected IED. - if (_changedProperties.has('doc') || _changedProperties.has('nsdoc')) { + if ( + _changedProperties.has('doc') || + _changedProperties.has('editCount') || + _changedProperties.has('nsdoc') + ) { this.selectedIEDs = []; this.selectedLNClasses = []; From 8bdd990a1d0c77b50743281d71b61489709e433a Mon Sep 17 00:00:00 2001 From: danyill Date: Fri, 14 Jul 2023 08:55:50 +1200 Subject: [PATCH 05/15] fix(menu/importIEDs): Allow importing multiple IEDs from multiple SCD files (#1222) * Initial refactoring to functions * Allow consistent import of IEDs (closes #1106) * Tidy up * Tidying * More tidying * Remove nullish coalescing where not required --- src/menu/ImportIEDs.ts | 91 +++++++++++-------- .../triggered/ImportIedsPlugin.test.ts | 83 +++++------------ .../{dublicate.iid => duplicate.iid} | 0 3 files changed, 75 insertions(+), 99 deletions(-) rename test/testfiles/importieds/{dublicate.iid => duplicate.iid} (100%) diff --git a/src/menu/ImportIEDs.ts b/src/menu/ImportIEDs.ts index 439cdd89c..bb7ef95a1 100644 --- a/src/menu/ImportIEDs.ts +++ b/src/menu/ImportIEDs.ts @@ -23,7 +23,6 @@ import { isPublic, newActionEvent, newLogEvent, - newPendingStateEvent, selector, SimpleAction, } from '../foundation.js'; @@ -368,12 +367,6 @@ function isIedNameUnique(ied: Element, doc: Document): boolean { return true; } -function resetSelection(dialog: Dialog): void { - ( - (dialog.querySelector('filtered-list') as List).selected as ListItemBase[] - ).forEach(item => (item.selected = false)); -} - export default class ImportingIedPlugin extends LitElement { @property({ attribute: false }) doc!: XMLDocument; @@ -381,12 +374,13 @@ export default class ImportingIedPlugin extends LitElement { editCount = -1; @state() - importDoc?: XMLDocument; + iedSelection: TemplateResult[] = []; @query('#importied-plugin-input') pluginFileUI!: HTMLInputElement; @query('mwc-dialog') dialog!: Dialog; async run(): Promise { + this.iedSelection = []; this.pluginFileUI.click(); } @@ -423,6 +417,7 @@ export default class ImportingIedPlugin extends LitElement { // This doesn't provide redo/undo capability as it is not using the Editing // action API. To use it would require us to cache the full SCL file in // OpenSCD as it is now which could use significant memory. + // TODO: In open-scd core update this to allow including in undo/redo. updateNamespaces( this.doc.documentElement, @@ -447,19 +442,25 @@ export default class ImportingIedPlugin extends LitElement { ); } - private async importIEDs(): Promise { + private async importIEDs( + importDoc: XMLDocument, + fileName: string + ): Promise { + const documentDialog: Dialog = this.shadowRoot!.querySelector( + `mwc-dialog[data-file="${fileName}"]` + )!; + const selectedItems = ( - (this.dialog.querySelector('filtered-list')).selected + (documentDialog.querySelector('filtered-list')).selected ); const ieds = selectedItems .map(item => { - return this.importDoc!.querySelector(selector('IED', item.value)); + return importDoc!.querySelector(selector('IED', item.value)); }) .filter(ied => ied) as Element[]; - resetSelection(this.dialog); - this.dialog.close(); + documentDialog.close(); for (const ied of ieds) { this.importIED(ied); @@ -467,8 +468,8 @@ export default class ImportingIedPlugin extends LitElement { } } - public prepareImport(): void { - if (!this.importDoc) { + async prepareImport(importDoc: XMLDocument, fileName: string): Promise { + if (!importDoc) { this.dispatchEvent( newLogEvent({ kind: 'error', @@ -478,7 +479,7 @@ export default class ImportingIedPlugin extends LitElement { return; } - if (this.importDoc.querySelector('parsererror')) { + if (importDoc.querySelector('parsererror')) { this.dispatchEvent( newLogEvent({ kind: 'error', @@ -488,7 +489,7 @@ export default class ImportingIedPlugin extends LitElement { return; } - const ieds = Array.from(this.importDoc.querySelectorAll(':root > IED')); + const ieds = Array.from(importDoc.querySelectorAll(':root > IED')); if (ieds.length === 0) { this.dispatchEvent( newLogEvent({ @@ -501,10 +502,23 @@ export default class ImportingIedPlugin extends LitElement { if (ieds.length === 1) { this.importIED(ieds[0]); - return; + return await this.docUpdate(); } - this.dialog.show(); + this.buildIedSelection(importDoc, fileName); + await this.requestUpdate(); + const dialog = ( + this.shadowRoot!.querySelector(`mwc-dialog[data-file="${fileName}"]`) + ); + dialog.show(); + + // await closing of dialog + await new Promise(resolve => { + dialog.addEventListener('closed', function onClosed(evt) { + evt.target?.removeEventListener('closed', onClosed); + resolve(); + }); + }); } /** Loads the file `event.target.files[0]` into [[`src`]] as a `blob:...`. */ @@ -513,23 +527,20 @@ export default class ImportingIedPlugin extends LitElement { (event.target)?.files ?? [] ); - const promises = files.map(async file => { - this.importDoc = new DOMParser().parseFromString( - await file.text(), - 'application/xml' - ); - - return this.prepareImport(); + const promises = files.map(file => { + return { + text: file + .text() + .then(text => + new DOMParser().parseFromString(text, 'application/xml') + ), + name: file.name, + }; }); - const mergedPromise = new Promise((resolve, reject) => - Promise.allSettled(promises).then( - () => resolve(), - () => reject() - ) - ); - - this.dispatchEvent(newPendingStateEvent(mergedPromise)); + for await (const file of promises) { + await this.prepareImport(await file.text, file.name); + } } protected renderInput(): TemplateResult { @@ -539,10 +550,10 @@ export default class ImportingIedPlugin extends LitElement { }} id="importied-plugin-input" accept=".sed,.scd,.ssd,.iid,.cid,.icd" type="file">`; } - protected renderIedSelection(): TemplateResult { - return html` + protected buildIedSelection(importDoc: XMLDocument, fileName: string): void { + this.iedSelection.push(html` - ${Array.from(this.importDoc?.querySelectorAll(':root > IED') ?? []).map( + ${Array.from(importDoc?.querySelectorAll(':root > IED') ?? []).map( ied => html`${ied.getAttribute('name')} this.importIEDs(importDoc, fileName)} > - `; + `); } render(): TemplateResult { - return html`${this.renderIedSelection()}${this.renderInput()}`; + return html`${this.iedSelection}${this.renderInput()}`; } static styles = css` diff --git a/test/integration/editors/triggered/ImportIedsPlugin.test.ts b/test/integration/editors/triggered/ImportIedsPlugin.test.ts index f3219099d..7143a722d 100644 --- a/test/integration/editors/triggered/ImportIedsPlugin.test.ts +++ b/test/integration/editors/triggered/ImportIedsPlugin.test.ts @@ -41,7 +41,6 @@ describe('ImportIedsPlugin', () => { importDoc = await fetch('/test/testfiles/importieds/valid.iid') .then(response => response.text()) .then(str => new DOMParser().parseFromString(str, 'application/xml')); - element.importDoc = importDoc; await element.updateComplete; }); @@ -49,7 +48,7 @@ describe('ImportIedsPlugin', () => { expect(element.doc?.querySelector(':root > IED[name="TestImportIED"]')).to .not.exist; - element.prepareImport(); + element.prepareImport(importDoc, 'valid.iid'); await parent.updateComplete; expect(element.doc?.querySelector(':root > IED[name="TestImportIED"]')).to @@ -57,7 +56,7 @@ describe('ImportIedsPlugin', () => { }); it('adds the connectedap of the imported ied', async () => { - element.prepareImport(); + element.prepareImport(importDoc, 'valid.iid'); await parent.updateComplete; expect( @@ -71,7 +70,7 @@ describe('ImportIedsPlugin', () => { expect(element.doc.querySelector('SubNetwork[name="NewSubNetwork"]')).to .not.exist; - element.prepareImport(); + element.prepareImport(importDoc, 'valid.iid'); await parent.updateComplete; expect(element.doc.querySelector('SubNetwork[name="NewSubNetwork"]')).to @@ -87,10 +86,9 @@ describe('ImportIedsPlugin', () => { ied.setAttribute('manufacturer', 'Fancy-Vendy'); ied.setAttribute('type', 'Z#Mega$Y'); - element.importDoc = importDoc; await element.updateComplete; - element.prepareImport(); + element.prepareImport(importDoc, 'template.icd'); await parent.updateComplete; console.log( @@ -108,11 +106,7 @@ describe('ImportIedsPlugin', () => { ) .then(response => response.text()) .then(str => new DOMParser().parseFromString(str, 'application/xml')); - - element.importDoc = templateIED1; - await element.updateComplete; - - element.prepareImport(); + element.prepareImport(templateIED1, 'template.icd'); const templateIED2 = await fetch( '/test/testfiles/importieds/template.icd' @@ -120,10 +114,9 @@ describe('ImportIedsPlugin', () => { .then(response => response.text()) .then(str => new DOMParser().parseFromString(str, 'application/xml')); - element.importDoc = templateIED2; await element.updateComplete; - element.prepareImport(); + element.prepareImport(templateIED2, 'template.icd'); await parent.updateComplete; expect(element.doc.querySelector('IED[name="FancyVendy_ZMegaY_001"]')).to @@ -138,10 +131,7 @@ describe('ImportIedsPlugin', () => { ) .then(response => response.text()) .then(str => new DOMParser().parseFromString(str, 'application/xml')); - element.importDoc = templateIED1; - await element.updateComplete; - - element.prepareImport(); + element.prepareImport(templateIED1, 'template.icd'); await parent.updateComplete; expect( @@ -157,7 +147,7 @@ describe('ImportIedsPlugin', () => { .length ).to.equal(0); - element.prepareImport(); + element.prepareImport(importDoc, 'template.icd'); await parent.updateComplete; expect( @@ -189,7 +179,6 @@ describe('ImportIedsPlugin', () => { .then(response => response.text()) .then(str => new DOMParser().parseFromString(str, 'application/xml')); - element.importDoc = importDoc; await element.updateComplete; }); @@ -197,7 +186,7 @@ describe('ImportIedsPlugin', () => { expect(element.doc?.querySelector(':root > IED[name="TestImportIED"]')).to .not.exist; - element.prepareImport(); + element.prepareImport(importDoc, 'valid.iid'); await parent.updateComplete; expect(element.doc?.querySelector(':root > IED[name="TestImportIED"]')).to @@ -210,7 +199,7 @@ describe('ImportIedsPlugin', () => { .length ).to.equal(11); - element.prepareImport(); + element.prepareImport(importDoc, 'valid.iid'); await parent.updateComplete; expect( @@ -225,7 +214,7 @@ describe('ImportIedsPlugin', () => { .length ).to.equal(16); - element.prepareImport(); + element.prepareImport(importDoc, 'valid.iid'); await parent.updateComplete; expect( @@ -240,7 +229,7 @@ describe('ImportIedsPlugin', () => { .length ).to.equal(7); - element.prepareImport(); + element.prepareImport(importDoc, 'valid.iid'); await parent.updateComplete; expect( @@ -255,7 +244,7 @@ describe('ImportIedsPlugin', () => { .length ).to.equal(4); - element.prepareImport(); + element.prepareImport(importDoc, 'valid.iid'); await parent.updateComplete; expect( @@ -268,7 +257,7 @@ describe('ImportIedsPlugin', () => { expect(element.doc.querySelector('ConnectedAP[iedName="TestImportIED"]')) .to.not.exist; - element.prepareImport(); + element.prepareImport(importDoc, 'valid.iid'); await parent.updateComplete; expect(element.doc.querySelector('ConnectedAP[iedName="TestImportIED"]')) @@ -283,7 +272,7 @@ describe('ImportIedsPlugin', () => { expect(element.doc.querySelector('SubNetwork[name="NewSubNetwork"]')).to .not.exist; - element.prepareImport(); + element.prepareImport(importDoc, 'valid.iid'); await parent.updateComplete; expect(element.doc.querySelector('SubNetwork[name="NewSubNetwork"]')).to @@ -291,7 +280,7 @@ describe('ImportIedsPlugin', () => { }); it('correctly transfers document element namespaces', async () => { - element.prepareImport(); + element.prepareImport(importDoc, 'valid.iid'); await parent.updateComplete; expect( @@ -328,22 +317,14 @@ describe('ImportIedsPlugin', () => { ) .then(response => response.text()) .then(str => new DOMParser().parseFromString(str, 'application/xml')); - - element.importDoc = templateIED1; - await element.updateComplete; - - element.prepareImport(); + element.prepareImport(templateIED1, 'template.icd'); const templateIED2 = await fetch( '/test/testfiles/importieds/template.icd' ) .then(response => response.text()) .then(str => new DOMParser().parseFromString(str, 'application/xml')); - - element.importDoc = templateIED2; - await element.updateComplete; - - element.prepareImport(); + element.prepareImport(templateIED2, 'template.icd'); await parent.updateComplete; expect(element.doc.querySelector('IED[name="FancyVendy_ZMegaY_001"]')).to @@ -359,10 +340,7 @@ describe('ImportIedsPlugin', () => { .then(response => response.text()) .then(str => new DOMParser().parseFromString(str, 'application/xml')); - element.importDoc = multipleIedDoc; - await element.updateComplete; - - element.prepareImport(); + element.prepareImport(multipleIedDoc, 'multipleied.scd'); await element.updateComplete; expect(element.dialog).to.exist; @@ -379,10 +357,7 @@ describe('ImportIedsPlugin', () => { .then(response => response.text()) .then(str => new DOMParser().parseFromString(str, 'application/xml')); - element.importDoc = multipleIedDoc; - await element.updateComplete; - - element.prepareImport(); + element.prepareImport(multipleIedDoc, 'multipleied.scd'); await element.updateComplete; (( @@ -436,25 +411,17 @@ describe('ImportIedsPlugin', () => { importDoc = await fetch('/test/testfiles/importieds/invalid.iid') .then(response => response.text()) .then(str => new DOMParser().parseFromString(str, 'application/xml')); - - element.importDoc = importDoc; - await element.updateComplete; - - element.prepareImport(); + element.prepareImport(importDoc, 'invalid.iid'); expect(parent.history[0].kind).to.equal('error'); expect(parent.history[0].title).to.equal('[import.log.missingied]'); }); it('throws duplicate ied name error', async () => { - importDoc = await fetch('/test/testfiles/importieds/dublicate.iid') + importDoc = await fetch('/test/testfiles/importieds/duplicate.iid') .then(response => response.text()) .then(str => new DOMParser().parseFromString(str, 'application/xml')); - - element.importDoc = importDoc; - await element.updateComplete; - - element.prepareImport(); + element.prepareImport(importDoc, 'duplicate.iid'); expect(parent.history[0].kind).to.equal('error'); expect(parent.history[0].title).to.equal('[import.log.nouniqueied]'); @@ -464,11 +431,9 @@ describe('ImportIedsPlugin', () => { importDoc = await fetch('/test/testfiles/importieds/parsererror.iid') .then(response => response.text()) .then(str => new DOMParser().parseFromString(str, 'application/xml')); - - element.importDoc = importDoc; await element.updateComplete; - element.prepareImport(); + element.prepareImport(importDoc, 'parsererror.iid'); expect(parent.history[0].kind).to.equal('error'); expect(parent.history[0].title).to.equal('[import.log.parsererror]'); diff --git a/test/testfiles/importieds/dublicate.iid b/test/testfiles/importieds/duplicate.iid similarity index 100% rename from test/testfiles/importieds/dublicate.iid rename to test/testfiles/importieds/duplicate.iid From 2706f82283cc7a58a8da5ca4cb775bfec7c6b986 Mon Sep 17 00:00:00 2001 From: danyill Date: Tue, 25 Jul 2023 05:38:27 +1200 Subject: [PATCH 06/15] fix(editors/communication,wizards): Fix P-type names and display of BitRate (#1277) * fix(editors/communication,wizards): Fix P-type names and display of BitRate, closes #1264 * Add test --- .../communication/subnetwork-editor.ts | 4 +- src/wizards/connectedap.ts | 8 +- src/wizards/subnetwork.ts | 17 +- ...ubnetwork-editor-wizarding-editing.test.ts | 180 +++++++++++++++++- 4 files changed, 192 insertions(+), 17 deletions(-) diff --git a/src/editors/communication/subnetwork-editor.ts b/src/editors/communication/subnetwork-editor.ts index 57f35ae4c..398557ec8 100644 --- a/src/editors/communication/subnetwork-editor.ts +++ b/src/editors/communication/subnetwork-editor.ts @@ -142,9 +142,7 @@ export class SubNetworkEditor extends LitElement { private subNetworkSpecs(): string { if (!this.type && !this.bitrate) return ''; - return `(${this.type}${ - this.type && this.bitrate ? ` — ${this.bitrate}` : `` - })`; + return `(${[this.type, this.bitrate].filter(text => !!text).join(' — ')})`; } private header(): string { diff --git a/src/wizards/connectedap.ts b/src/wizards/connectedap.ts index 9b3252efe..6b3516ba8 100644 --- a/src/wizards/connectedap.ts +++ b/src/wizards/connectedap.ts @@ -104,13 +104,13 @@ function initSMVElements( actions.push({ new: { parent: address, element: pAppId } }); const pVlanId = createElement(connectedAp.ownerDocument, 'P', { - type: 'VLANID', + type: 'VLAN-ID', }); pVlanId.textContent = '000'; actions.push({ new: { parent: address, element: pVlanId } }); const pVlanPrio = createElement(connectedAp.ownerDocument, 'P', { - type: 'VLAN-Priority', + type: 'VLAN-PRIORITY', }); pVlanPrio.textContent = '4'; actions.push({ new: { parent: address, element: pVlanPrio } }); @@ -172,13 +172,13 @@ function initGSEElements( actions.push({ new: { parent: address, element: pAppId } }); const pVlanId = createElement(connectedAp.ownerDocument, 'P', { - type: 'VLANID', + type: 'VLAN-ID', }); pVlanId.textContent = '000'; actions.push({ new: { parent: address, element: pVlanId } }); const pVlanPrio = createElement(connectedAp.ownerDocument, 'P', { - type: 'VLAN-Priority', + type: 'VLAN-PRIORITY', }); pVlanPrio.textContent = '4'; actions.push({ new: { parent: address, element: pVlanPrio } }); diff --git a/src/wizards/subnetwork.ts b/src/wizards/subnetwork.ts index 411b7361d..a2bba6d7f 100644 --- a/src/wizards/subnetwork.ts +++ b/src/wizards/subnetwork.ts @@ -127,19 +127,22 @@ function getBitRateAction( multiplier: string | null, SubNetwork: Element ): EditorAction { - if (oldBitRate === null) + if (oldBitRate === null) { + const bitRateElement = createElement(SubNetwork.ownerDocument, 'BitRate', { + unit: 'b/s', + }); + + if (multiplier) bitRateElement.setAttribute('multiplier', multiplier); + if (BitRate) bitRateElement.textContent = BitRate; + return { new: { parent: SubNetwork, - element: new DOMParser().parseFromString( - `${BitRate === null ? '' : BitRate}`, - 'application/xml' - ).documentElement, + element: bitRateElement, reference: SubNetwork.firstElementChild, }, }; + } if (BitRate === null) return { diff --git a/test/integration/editors/communication/subnetwork-editor-wizarding-editing.test.ts b/test/integration/editors/communication/subnetwork-editor-wizarding-editing.test.ts index afcdc8a1e..cbe111534 100644 --- a/test/integration/editors/communication/subnetwork-editor-wizarding-editing.test.ts +++ b/test/integration/editors/communication/subnetwork-editor-wizarding-editing.test.ts @@ -59,6 +59,7 @@ describe('subnetwork-editor wizarding editing integration', () => { ) ); }); + it('closes on secondary action', async () => { await (( parent.wizardUI.dialog?.querySelector( @@ -153,6 +154,7 @@ describe('subnetwork-editor wizarding editing integration', () => { .null; }); }); + describe('remove action', () => { let doc: XMLDocument; let parent: MockWizardEditor; @@ -211,13 +213,16 @@ describe('subnetwork-editor wizarding editing integration', () => { ) ); element = parent.querySelector('subnetwork-editor'); + }); + it('adds ConnectedAP on primary action', async () => { (( element?.shadowRoot?.querySelector( 'mwc-icon-button[icon="playlist_add"]' ) )).click(); await parent.updateComplete; + await element?.updateComplete; newConnectedAPItem = ( parent.wizardUI.dialog!.querySelector( @@ -230,23 +235,192 @@ describe('subnetwork-editor wizarding editing integration', () => { 'mwc-button[slot="primaryAction"]' ) ); - }); - it('add ConnectedAP on primary action', async () => { expect( doc.querySelector( ':root > Communication > SubNetwork[name="StationBus"] > ConnectedAP[iedName="IED3"][apName="P2"]' ) ).to.not.exist; + newConnectedAPItem.click(); - await parent.updateComplete; primaryAction.click(); await parent.updateComplete; + expect( doc.querySelector( ':root > Communication > SubNetwork[name="StationBus"] > ConnectedAP[iedName="IED3"][apName="P2"]' ) ).to.exist; }); + + it('add ConnectedAP with GSE and generates correct addresses', async () => { + doc = await fetch('/test/testfiles/editors/MessageBindingGOOSE2007B4.scd') + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); + parent = ( + await fixture( + html`` + ) + ); + element = parent.querySelector('subnetwork-editor'); + await parent.updateComplete; + await element?.updateComplete; + + (( + element?.shadowRoot?.querySelector( + 'mwc-icon-button[icon="playlist_add"]' + ) + )).click(); + await parent.updateComplete; + await element?.updateComplete; + + expect( + doc.querySelector( + ':root > Communication > SubNetwork[name="StationBus"] > ConnectedAP[iedName="IED4"][apName="P1"]' + ) + ).to.not.exist; + + newConnectedAPItem = ( + parent.wizardUI.dialog!.querySelector( + 'mwc-check-list-item:nth-child(2)' + ) + ); + + primaryAction = ( + parent.wizardUI.dialog?.querySelector( + 'mwc-button[slot="primaryAction"]' + ) + ); + + newConnectedAPItem.click(); + primaryAction.click(); + await parent.updateComplete; + + const connectedAp = doc.querySelector( + ':root > Communication > SubNetwork[name="StationBus"] > ConnectedAP[iedName="IED4"][apName="P1"]' + ); + + expect(connectedAp).to.exist; + const gse1 = connectedAp!.querySelector('GSE[cbName="GCB2"]'); + const gse2 = connectedAp!.querySelector('GSE[cbName="GCB2"]'); + expect(gse1).to.exist; + expect(gse2).to.exist; + + expect(gse1?.getAttribute('ldInst')).to.equal('CircuitBreaker_CB1'); + + const address1 = gse1?.querySelector('Address'); + expect(address1).to.exist; + + const vlanPriority = address1?.querySelector('P[type="VLAN-PRIORITY"]'); + expect(vlanPriority).to.exist; + expect(vlanPriority?.textContent).to.equal('4'); + + const vlanId = address1?.querySelector('P[type="VLAN-ID"]'); + expect(vlanId).to.exist; + expect(vlanId?.textContent).to.equal('000'); + + const appId = address1?.querySelector('P[type="APPID"]'); + expect(appId).to.exist; + expect(appId?.textContent).to.equal('0001'); + + const mac = address1?.querySelector('P[type="MAC-Address"]'); + expect(mac).to.exist; + expect(mac?.textContent).to.equal('01-0C-CD-01-00-01'); + + const minTime = gse1?.querySelector('MinTime'); + expect(minTime).to.exist; + expect(minTime?.getAttribute('unit')).to.equal('s'); + expect(minTime?.getAttribute('multiplier')).to.equal('m'); + expect(minTime?.textContent).to.equal('10'); + + const maxTime = gse1?.querySelector('MaxTime'); + expect(maxTime).to.exist; + expect(maxTime?.getAttribute('unit')).to.equal('s'); + expect(maxTime?.getAttribute('multiplier')).to.equal('m'); + expect(maxTime?.textContent).to.equal('10000'); + }); + + it('add ConnectedAP with SMV and generates correct addresses', async () => { + doc = await fetch('/test/testfiles/editors/MessageBindingSMV2007B4.scd') + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); + parent = ( + await fixture( + html`` + ) + ); + element = parent.querySelector('subnetwork-editor'); + await parent.updateComplete; + await element?.updateComplete; + + (( + element?.shadowRoot?.querySelector( + 'mwc-icon-button[icon="playlist_add"]' + ) + )).click(); + await parent.updateComplete; + await element?.updateComplete; + + expect( + doc.querySelector( + ':root > Communication > SubNetwork[name="StationBus"] > ConnectedAP[iedName="IED4"][apName="P1"]' + ) + ).to.not.exist; + + newConnectedAPItem = ( + parent.wizardUI.dialog!.querySelector( + 'mwc-check-list-item:nth-child(2)' + ) + ); + + primaryAction = ( + parent.wizardUI.dialog?.querySelector( + 'mwc-button[slot="primaryAction"]' + ) + ); + + newConnectedAPItem.click(); + primaryAction.click(); + await parent.updateComplete; + + const connectedAp = doc.querySelector( + ':root > Communication > SubNetwork[name="StationBus"] > ConnectedAP[iedName="IED4"][apName="P1"]' + ); + + expect(connectedAp).to.exist; + const smv1 = connectedAp!.querySelector('SMV[cbName="MSVCB02"]'); + expect(smv1).to.exist; + + expect(smv1?.getAttribute('ldInst')).to.equal('CircuitBreaker_CB1'); + + const address1 = smv1?.querySelector('Address'); + expect(address1).to.exist; + + const vlanPriority = address1?.querySelector('P[type="VLAN-PRIORITY"]'); + expect(vlanPriority).to.exist; + expect(vlanPriority?.textContent).to.equal('4'); + + const vlanId = address1?.querySelector('P[type="VLAN-ID"]'); + expect(vlanId).to.exist; + expect(vlanId?.textContent).to.equal('000'); + + const appId = address1?.querySelector('P[type="APPID"]'); + expect(appId).to.exist; + expect(appId?.textContent).to.equal('4000'); + + const mac = address1?.querySelector('P[type="MAC-Address"]'); + expect(mac).to.exist; + expect(mac?.textContent).to.equal('01-0C-CD-04-00-00'); + }); }); }); From ec82a892f09f1d44232c739b251af534129dacf6 Mon Sep 17 00:00:00 2001 From: cad Date: Fri, 18 Aug 2023 15:51:46 +0200 Subject: [PATCH 07/15] refactor(foundation): substitute find() for selector() (#1285) --- src/editors/protocol104/wizards/selectDo.ts | 4 ++-- src/editors/publisher/data-set-editor.ts | 4 ++-- src/editors/publisher/foundation.ts | 4 ++-- src/editors/publisher/gse-control-editor.ts | 4 ++-- .../publisher/report-control-editor.ts | 4 ++-- .../publisher/sampled-value-control-editor.ts | 8 +++----- src/editors/templates/datype-wizards.ts | 6 +++--- src/editors/templates/dotype-wizards.ts | 13 ++++-------- src/editors/templates/enumtype-wizard.ts | 15 +++++++------- src/editors/templates/lnodetype-wizard.ts | 17 +++++----------- src/foundation.ts | 20 ++++++++++++++++++- src/foundation/ied.ts | 4 ++-- src/menu/CompareIED.ts | 4 ++-- src/menu/ImportIEDs.ts | 4 ++-- src/menu/UpdateDescriptionABB.ts | 4 ++-- src/menu/UpdateDescriptionSEL.ts | 4 ++-- src/menu/UpdateSubstation.ts | 12 ++++------- src/menu/VirtualTemplateIED.ts | 7 ++----- src/wizards/clientln.ts | 10 +++++----- src/wizards/commmap-wizards.ts | 6 ++---- src/wizards/dataset.ts | 6 ++---- src/wizards/fcda.ts | 8 ++++---- src/wizards/foundation/finder.ts | 4 ++-- src/wizards/gsecontrol.ts | 8 +++----- src/wizards/lnode.ts | 14 +++++-------- src/wizards/reportcontrol.ts | 10 ++++------ src/wizards/sampledvaluecontrol.ts | 10 ++++++---- test/unit/foundation.test.ts | 18 ++++++++--------- 28 files changed, 109 insertions(+), 123 deletions(-) diff --git a/src/editors/protocol104/wizards/selectDo.ts b/src/editors/protocol104/wizards/selectDo.ts index c7b9394fe..37d861dca 100644 --- a/src/editors/protocol104/wizards/selectDo.ts +++ b/src/editors/protocol104/wizards/selectDo.ts @@ -13,7 +13,7 @@ import { getNameAttribute, identity, newSubWizardEvent, - selector, + find, Wizard, WizardActor, WizardInputElement, @@ -158,7 +158,7 @@ function checkAndGetLastElementFromPath( const [tagName, id] = path.pop()!.split(': '); if (!expectedTag.includes(tagName)) return null; - return doc.querySelector(selector(tagName, id)); + return find(doc, tagName, id); } /** diff --git a/src/editors/publisher/data-set-editor.ts b/src/editors/publisher/data-set-editor.ts index 3f7f48f30..27ffa2c70 100644 --- a/src/editors/publisher/data-set-editor.ts +++ b/src/editors/publisher/data-set-editor.ts @@ -19,7 +19,7 @@ import './data-set-element-editor.js'; import '../../filtered-list.js'; import { FilteredList } from '../../filtered-list.js'; -import { compareNames, identity, selector } from '../../foundation.js'; +import { compareNames, identity, find } from '../../foundation.js'; import { styles, updateElementReference } from './foundation.js'; @customElement('data-set-editor') @@ -50,7 +50,7 @@ export class DataSetEditor extends LitElement { private selectDataSet(evt: Event): void { const id = ((evt.target as FilteredList).selected as ListItem).value; - const dataSet = this.doc.querySelector(selector('DataSet', id)); + const dataSet = find(this.doc, 'DataSet', id); if (dataSet) { this.selectedDataSet = dataSet; diff --git a/src/editors/publisher/foundation.ts b/src/editors/publisher/foundation.ts index 71cdaaf2d..be524f0be 100644 --- a/src/editors/publisher/foundation.ts +++ b/src/editors/publisher/foundation.ts @@ -1,6 +1,6 @@ import { css } from 'lit-element'; -import { identity, selector } from '../../foundation.js'; +import { identity, find } from '../../foundation.js'; export function updateElementReference( newDoc: XMLDocument, @@ -9,7 +9,7 @@ export function updateElementReference( if (!oldElement || !oldElement.closest('SCL')) return null; const id = identity(oldElement); - const newElement = newDoc.querySelector(selector(oldElement.tagName, id)); + const newElement = find(newDoc, oldElement.tagName, id); return newElement; } diff --git a/src/editors/publisher/gse-control-editor.ts b/src/editors/publisher/gse-control-editor.ts index 19f71a450..39cdd388a 100644 --- a/src/editors/publisher/gse-control-editor.ts +++ b/src/editors/publisher/gse-control-editor.ts @@ -21,7 +21,7 @@ import '../../filtered-list.js'; import { FilteredList } from '../../filtered-list.js'; import { gooseIcon } from '../../icons/icons.js'; -import { compareNames, identity, selector } from '../../foundation.js'; +import { compareNames, identity, find } from '../../foundation.js'; import { styles, updateElementReference } from './foundation.js'; @customElement('gse-control-editor') @@ -62,7 +62,7 @@ export class GseControlEditor extends LitElement { private selectGSEControl(evt: Event): void { const id = ((evt.target as FilteredList).selected as ListItem).value; - const gseControl = this.doc.querySelector(selector('GSEControl', id)); + const gseControl = find(this.doc, 'GSEControl', id); if (!gseControl) return; this.selectedGseControl = gseControl; diff --git a/src/editors/publisher/report-control-editor.ts b/src/editors/publisher/report-control-editor.ts index 3aa580a75..2768c83e0 100644 --- a/src/editors/publisher/report-control-editor.ts +++ b/src/editors/publisher/report-control-editor.ts @@ -20,7 +20,7 @@ import './report-control-element-editor.js'; import '../../filtered-list.js'; import { FilteredList } from '../../filtered-list.js'; -import { compareNames, identity, selector } from '../../foundation.js'; +import { compareNames, identity, find } from '../../foundation.js'; import { reportIcon } from '../../icons/icons.js'; import { styles, updateElementReference } from './foundation.js'; @@ -66,7 +66,7 @@ export class ReportControlEditor extends LitElement { private selectReportControl(evt: Event): void { const id = ((evt.target as FilteredList).selected as ListItem).value; - const reportControl = this.doc.querySelector(selector('ReportControl', id)); + const reportControl = find(this.doc, 'ReportControl', id); if (!reportControl) return; this.selectedReportControl = reportControl; diff --git a/src/editors/publisher/sampled-value-control-editor.ts b/src/editors/publisher/sampled-value-control-editor.ts index ae68c94d9..c0f5f0f84 100644 --- a/src/editors/publisher/sampled-value-control-editor.ts +++ b/src/editors/publisher/sampled-value-control-editor.ts @@ -20,7 +20,7 @@ import '../../filtered-list.js'; import './sampled-value-control-element-editor.js'; import { FilteredList } from '../../filtered-list.js'; -import { compareNames, identity, selector } from '../../foundation.js'; +import { compareNames, identity, find } from '../../foundation.js'; import { smvIcon } from '../../icons/icons.js'; import { styles, updateElementReference } from './foundation.js'; @@ -29,7 +29,7 @@ export class SampledValueControlEditor extends LitElement { /** The document being edited as provided to plugins by [[`OpenSCD`]]. */ @property({ attribute: false }) doc!: XMLDocument; - @property({type: Number}) + @property({ type: Number }) editCount = -1; @state() @@ -66,9 +66,7 @@ export class SampledValueControlEditor extends LitElement { private selectSMVControl(evt: Event): void { const id = ((evt.target as FilteredList).selected as ListItem).value; - const smvControl = this.doc.querySelector( - selector('SampledValueControl', id) - ); + const smvControl = find(this.doc, 'SampledValueControl', id); if (!smvControl) return; this.selectedSampledValueControl = smvControl; diff --git a/src/editors/templates/datype-wizards.ts b/src/editors/templates/datype-wizards.ts index c19696f31..689728419 100644 --- a/src/editors/templates/datype-wizards.ts +++ b/src/editors/templates/datype-wizards.ts @@ -16,6 +16,7 @@ import { Create, createElement, EditorAction, + find, getValue, identity, newActionEvent, @@ -23,7 +24,6 @@ import { newWizardEvent, patterns, Replace, - selector, Wizard, WizardActor, WizardInputElement, @@ -89,7 +89,7 @@ export function editDaTypeWizard( dATypeIdentity: string, doc: XMLDocument ): Wizard | undefined { - const datype = doc.querySelector(selector('DAType', dATypeIdentity)); + const datype = find(doc, 'DAType', dATypeIdentity); if (!datype) return undefined; const id = datype.getAttribute('id'); @@ -138,7 +138,7 @@ export function editDaTypeWizard( style="margin-top: 0px;" @selected=${(e: SingleSelectedEvent) => { const bdaIdentity = ((e.target).selected).value; - const bda = doc.querySelector(selector('BDA', bdaIdentity)); + const bda = find(doc, 'BDA', bdaIdentity); if (bda) e.target!.dispatchEvent(newSubWizardEvent(editBDAWizard(bda))); diff --git a/src/editors/templates/dotype-wizards.ts b/src/editors/templates/dotype-wizards.ts index 5562f2c27..ab8af6977 100644 --- a/src/editors/templates/dotype-wizards.ts +++ b/src/editors/templates/dotype-wizards.ts @@ -16,6 +16,7 @@ import { Create, createElement, EditorAction, + find, getValue, identity, isPublic, @@ -23,7 +24,6 @@ import { newSubWizardEvent, newWizardEvent, Replace, - selector, Wizard, WizardActor, WizardInputElement, @@ -99,12 +99,7 @@ function sDOWizard(options: WizardOptions): Wizard | undefined { const doc = (options).doc ? (options).doc : (options).parent.ownerDocument; - const sdo = - Array.from( - doc.querySelectorAll( - selector('SDO', (options).identity ?? NaN) - ) - ).find(isPublic) ?? null; + const sdo = find(doc, 'SDO', (options).identity ?? NaN); const [title, action, type, menuActions, name, desc] = sdo ? [ @@ -351,7 +346,7 @@ export function dOTypeWizard( dOTypeIdentity: string, doc: XMLDocument ): Wizard | undefined { - const dotype = doc.querySelector(selector('DOType', dOTypeIdentity)); + const dotype = find(doc, 'DOType', dOTypeIdentity); if (!dotype) return undefined; return [ @@ -411,7 +406,7 @@ export function dOTypeWizard( const item = (e.target).selected; const daIdentity = ((e.target).selected).value; - const da = doc.querySelector(selector('DA', daIdentity)); + const da = find(doc, 'DA', daIdentity); const wizard = item.classList.contains('DA') ? da diff --git a/src/editors/templates/enumtype-wizard.ts b/src/editors/templates/enumtype-wizard.ts index 282debb33..c33d39b48 100644 --- a/src/editors/templates/enumtype-wizard.ts +++ b/src/editors/templates/enumtype-wizard.ts @@ -15,6 +15,7 @@ import { cloneElement, createElement, EditorAction, + find, getValue, identity, isPublic, @@ -23,7 +24,6 @@ import { newWizardEvent, patterns, Replace, - selector, Wizard, WizardActor, WizardInputElement, @@ -101,12 +101,11 @@ function eNumValWizard(options: WizardOptions): Wizard { const doc = (options).doc ? (options).doc : (options).parent.ownerDocument; - const enumval = - Array.from( - doc.querySelectorAll( - selector('EnumVal', (options).identity ?? NaN) - ) - ).find(isPublic) ?? null; + const enumval = find( + doc, + 'EnumVal', + (options).identity ?? NaN + ); const [title, action, ord, desc, value, menuActions] = enumval ? [ @@ -295,7 +294,7 @@ export function eNumTypeEditWizard( eNumTypeIdentity: string, doc: XMLDocument ): Wizard | undefined { - const enumtype = doc.querySelector(selector('EnumType', eNumTypeIdentity)); + const enumtype = find(doc, 'EnumType', eNumTypeIdentity); if (!enumtype) return undefined; return [ diff --git a/src/editors/templates/lnodetype-wizard.ts b/src/editors/templates/lnodetype-wizard.ts index 51fa41ee9..25990e054 100644 --- a/src/editors/templates/lnodetype-wizard.ts +++ b/src/editors/templates/lnodetype-wizard.ts @@ -18,6 +18,7 @@ import { Create, createElement, EditorAction, + find, getChildElementsByTagName, getValue, identity, @@ -27,7 +28,6 @@ import { newWizardEvent, patterns, Replace, - selector, Wizard, WizardActor, WizardInputElement, @@ -130,12 +130,7 @@ function dOWizard(options: WizardOptions): Wizard | undefined { const doc = (options).doc ? (options).doc : (options).parent.ownerDocument; - const DO = - Array.from( - doc.querySelectorAll( - selector('DO', (options).identity ?? NaN) - ) - ).find(isPublic) ?? null; + const DO = find(doc, 'DO', (options).identity ?? NaN); const [ title, @@ -412,7 +407,7 @@ function startLNodeTypeCreate( const value = (e.target).selected?.value; - const lnodetype = identity - ? templates.querySelector(selector('LNodeType', identity)) - : null; + const lnodetype = identity ? find(templates, 'LNodeType', identity) : null; const primaryAction = (e.target) @@ -606,7 +599,7 @@ export function lNodeTypeWizard( lNodeTypeIdentity: string, doc: XMLDocument ): Wizard | undefined { - const lnodetype = doc.querySelector(selector('LNodeType', lNodeTypeIdentity)); + const lnodetype = find(doc, 'LNodeType', lNodeTypeIdentity); if (!lnodetype) return undefined; return [ diff --git a/src/foundation.ts b/src/foundation.ts index 2bfc59c3e..a956ca87f 100644 --- a/src/foundation.ts +++ b/src/foundation.ts @@ -2505,7 +2505,7 @@ export function getReference(parent: Element, tag: SCLTag): Element | null { return nextSibling ?? null; } -export function selector(tagName: string, identity: string | number): string { +function selector(tagName: string, identity: string | number): string { if (typeof identity !== 'string') return voidSelector; if (isSCLTag(tagName)) return tags[tagName].selector(tagName, identity); @@ -2513,6 +2513,24 @@ export function selector(tagName: string, identity: string | number): string { return tagName; } +export function find( + root: XMLDocument | Element | DocumentFragment, + tagName: string, + identity: string | number +): Element | null { + if (typeof identity !== 'string' || !isSCLTag(tagName)) return null; + + const element = root.querySelector(tags[tagName].selector(tagName, identity)); + + if (element === null || isPublic(element)) return element; + + return ( + Array.from( + root.querySelectorAll(tags[tagName].selector(tagName, identity)) + ).find(isPublic) ?? null + ); +} + /** @returns a string uniquely identifying `e` in its document, or NaN if `e` * is unidentifiable. */ export function identity(e: Element | null): string | number { diff --git a/src/foundation/ied.ts b/src/foundation/ied.ts index 980cc1288..ba735e411 100644 --- a/src/foundation/ied.ts +++ b/src/foundation/ied.ts @@ -1,4 +1,4 @@ -import { Delete, identity, selector } from '../foundation.js'; +import { Delete, find, identity } from '../foundation.js'; /** * All available FCDA references that are used to link ExtRefs. @@ -94,7 +94,7 @@ export function emptyInputsDeleteActions( Object.entries(inputsMap).forEach(([key, value]) => { if (value.children.length! == 0) { const doc = extRefDeleteActions[0].old.parent.ownerDocument!; - const inputs = doc.querySelector(selector('Inputs', key)); + const inputs = find(doc, 'Inputs', key); if (inputs && inputs.parentElement) { inputDeleteActions.push({ diff --git a/src/menu/CompareIED.ts b/src/menu/CompareIED.ts index c932534d0..c4043a94d 100644 --- a/src/menu/CompareIED.ts +++ b/src/menu/CompareIED.ts @@ -22,11 +22,11 @@ import '../plain-compare-list.js'; import { compareNames, + find, getNameAttribute, identity, isPublic, newPendingStateEvent, - selector, } from '../foundation.js'; import { DiffFilter } from '../foundation/compare.js'; @@ -137,7 +137,7 @@ export default class CompareIEDPlugin extends LitElement { ); const identity = selectListItem?.value; if (identity) { - return doc.querySelector(selector('IED', identity)) ?? undefined; + return find(doc, 'IED', identity) ?? undefined; } return undefined; } diff --git a/src/menu/ImportIEDs.ts b/src/menu/ImportIEDs.ts index bb7ef95a1..5c511af3b 100644 --- a/src/menu/ImportIEDs.ts +++ b/src/menu/ImportIEDs.ts @@ -19,11 +19,11 @@ import { ListItemBase } from '@material/mwc-list/mwc-list-item-base'; import '../filtered-list.js'; import { createElement, + find, identity, isPublic, newActionEvent, newLogEvent, - selector, SimpleAction, } from '../foundation.js'; @@ -456,7 +456,7 @@ export default class ImportingIedPlugin extends LitElement { const ieds = selectedItems .map(item => { - return importDoc!.querySelector(selector('IED', item.value)); + return find(importDoc, 'IED', item.value); }) .filter(ied => ied) as Element[]; diff --git a/src/menu/UpdateDescriptionABB.ts b/src/menu/UpdateDescriptionABB.ts index 57b70b65b..3a2db629b 100644 --- a/src/menu/UpdateDescriptionABB.ts +++ b/src/menu/UpdateDescriptionABB.ts @@ -8,11 +8,11 @@ import { ListItemBase } from '@material/mwc-list/mwc-list-item-base'; import '../filtered-list.js'; import { cloneElement, + find, identity, isPublic, newWizardEvent, SCLTag, - selector, Wizard, WizardAction, WizardActor, @@ -37,7 +37,7 @@ function addDescriptionAction(doc: XMLDocument): WizardActor { const desc = (item.querySelector('span')).textContent; const [tag, identity] = item.value.split(' | '); - const oldElement = doc.querySelector(selector(tag, identity))!; + const oldElement = find(doc, tag, identity)!; const newElement = cloneElement(oldElement, { desc }); return { old: { element: oldElement }, new: { element: newElement } }; }); diff --git a/src/menu/UpdateDescriptionSEL.ts b/src/menu/UpdateDescriptionSEL.ts index 9848665af..1918a5337 100644 --- a/src/menu/UpdateDescriptionSEL.ts +++ b/src/menu/UpdateDescriptionSEL.ts @@ -8,11 +8,11 @@ import { ListItemBase } from '@material/mwc-list/mwc-list-item-base'; import '../filtered-list.js'; import { cloneElement, + find, identity, isPublic, newWizardEvent, SCLTag, - selector, Wizard, WizardAction, WizardActor, @@ -71,7 +71,7 @@ function addDescriptionAction(doc: XMLDocument): WizardActor { const desc = (item.querySelector('span')).textContent; const [tag, identity] = item.value.split(' | '); - const oldElement = doc.querySelector(selector(tag, identity))!; + const oldElement = find(doc, tag, identity)!; const newElement = cloneElement(oldElement, { desc }); return { old: { element: oldElement }, new: { element: newElement } }; }); diff --git a/src/menu/UpdateSubstation.ts b/src/menu/UpdateSubstation.ts index 0ebd3d37b..71d00e475 100644 --- a/src/menu/UpdateSubstation.ts +++ b/src/menu/UpdateSubstation.ts @@ -3,10 +3,10 @@ import { get } from 'lit-translate'; import { crossProduct, + find, identity, newWizardEvent, SCLTag, - selector, tags, } from '../foundation.js'; import { Diff, mergeWizard } from '../wizards.js'; @@ -101,10 +101,8 @@ export default class UpdateSubstationPlugin extends LitElement { selected: (diff: Diff): boolean => diff.theirs instanceof Element ? diff.theirs.tagName === 'LNode' - ? this.doc.querySelector( - selector('LNode', identity(diff.theirs)) - ) === null && - isValidReference(doc, identity(diff.theirs)) + ? find(this.doc, 'LNode', identity(diff.theirs)) === + null && isValidReference(doc, identity(diff.theirs)) : diff.theirs.tagName === 'Substation' || !tags['SCL'].children.includes( diff.theirs.tagName @@ -113,9 +111,7 @@ export default class UpdateSubstationPlugin extends LitElement { disabled: (diff: Diff): boolean => diff.theirs instanceof Element && diff.theirs.tagName === 'LNode' && - (this.doc.querySelector( - selector('LNode', identity(diff.theirs)) - ) !== null || + (find(this.doc, 'LNode', identity(diff.theirs)) !== null || !isValidReference(doc, identity(diff.theirs))), auto: (): boolean => true, } diff --git a/src/menu/VirtualTemplateIED.ts b/src/menu/VirtualTemplateIED.ts index a45a11491..dd2501924 100644 --- a/src/menu/VirtualTemplateIED.ts +++ b/src/menu/VirtualTemplateIED.ts @@ -21,10 +21,10 @@ import { Select } from '@material/mwc-select'; import '../filtered-list.js'; import { + find, getChildElementsByTagName, identity, newActionEvent, - selector, } from '../foundation.js'; import { WizardTextField } from '../wizard-textfield.js'; import { @@ -164,10 +164,7 @@ export default class VirtualTemplateIED extends LitElement { this.dialog.querySelectorAll( 'mwc-check-list-item[selected]:not([disabled])' ) ?? [] - ).map( - selectedItem => - this.doc.querySelector(selector('LNode', selectedItem.value))! - ); + ).map(selectedItem => find(this.doc, 'LNode', selectedItem.value)!); if (!selectedLNode.length) return; const selectedLLN0s = Array.from( diff --git a/src/wizards/clientln.ts b/src/wizards/clientln.ts index 8b7c8c482..4da19dbcd 100644 --- a/src/wizards/clientln.ts +++ b/src/wizards/clientln.ts @@ -9,9 +9,9 @@ import { ListItemBase } from '@material/mwc-list/mwc-list-item-base'; import '../filtered-list.js'; import { createElement, + find, identity, pathParts, - selector, Wizard, WizardAction, WizardActor, @@ -34,16 +34,16 @@ function getElement(identity: string | number): string { function getLogicalNode(doc: XMLDocument, identity: string): Element | null { if (identity.split('>').length === 4) { - return doc.querySelector(selector('LN', identity)); + return find(doc, 'LN', identity); } if (identity.split('>').length === 3) { if (getElement(identity).split(' ').length > 1) { - return doc.querySelector(selector('LN', identity)); + return find(doc, 'LN', identity); } if (getElement(identity).split(' ').length === 1) { - return doc.querySelector(selector('LN0', identity)); + return find(doc, 'LN0', identity); } } @@ -128,7 +128,7 @@ function addClientLnAction(doc: XMLDocument): WizardActor { const reportCbs = cbSelected .map(cb => cb.value) - .map(cbValue => doc.querySelector(selector('ReportControl', cbValue))) + .map(cbValue => find(doc, 'ReportControl', cbValue)) .filter(cb => cb !== null); reportCbs.forEach(cb => { diff --git a/src/wizards/commmap-wizards.ts b/src/wizards/commmap-wizards.ts index ca9a96589..36437ee88 100644 --- a/src/wizards/commmap-wizards.ts +++ b/src/wizards/commmap-wizards.ts @@ -7,11 +7,11 @@ import { SingleSelectedEvent } from '@material/mwc-list/mwc-list-foundation'; import '../filtered-list.js'; import { + find, findControlBlocks, identity, isPublic, newWizardEvent, - selector, Wizard, WizardActor, } from '../foundation.js'; @@ -90,9 +90,7 @@ export function communicationMappingWizard( >${Array.from(connections.keys()).map(key => { const elements = connections.get(key)!; const [cbId, cbTag, sinkIED] = key.split(' | '); - const cbElement = ownerDocument.querySelector( - selector(cbTag, cbId) - ); + const cbElement = find(ownerDocument, cbTag, cbId); const [_, sourceIED, controlBlock] = cbId.match(/^(.+)>>(.*)$/)!; return html` mwc-check-list-item:not([selected])' ) ) - .map(listItem => - element.querySelector(selector('FCDA', (listItem).value)) - ) + .map(listItem => find(element, 'FCDA', (listItem).value)) .filter(fcda => fcda) .map(fcda => { return { diff --git a/src/wizards/fcda.ts b/src/wizards/fcda.ts index ee9343789..939a7feb0 100644 --- a/src/wizards/fcda.ts +++ b/src/wizards/fcda.ts @@ -3,7 +3,7 @@ import { get } from 'lit-translate'; import { createElement, - selector, + find, Wizard, WizardAction, WizardActor, @@ -17,7 +17,7 @@ import { export function newFCDA(parent: Element, path: string[]): Element | undefined { const [leafTag, leafId] = path[path.length - 1].split(': '); - const leaf = parent.ownerDocument.querySelector(selector(leafTag, leafId)); + const leaf = find(parent.ownerDocument, leafTag, leafId); if (!leaf || getDataModelChildren(leaf).length > 0) return; const lnSegment = path.find(segment => segment.startsWith('LN')); @@ -25,7 +25,7 @@ export function newFCDA(parent: Element, path: string[]): Element | undefined { const [lnTag, lnId] = lnSegment.split(': '); - const ln = parent.ownerDocument.querySelector(selector(lnTag, lnId)); + const ln = find(parent.ownerDocument, lnTag, lnId); if (!ln) return; const ldInst = ln.closest('LDevice')?.getAttribute('inst'); @@ -44,7 +44,7 @@ export function newFCDA(parent: Element, path: string[]): Element | undefined { const [tagName, id] = segment.split(': '); if (!['DO', 'DA', 'SDO', 'BDA'].includes(tagName)) continue; - const element = parent.ownerDocument.querySelector(selector(tagName, id)); + const element = find(parent.ownerDocument, tagName, id); if (!element) return; diff --git a/src/wizards/foundation/finder.ts b/src/wizards/foundation/finder.ts index 67832295a..62553e29c 100644 --- a/src/wizards/foundation/finder.ts +++ b/src/wizards/foundation/finder.ts @@ -3,7 +3,7 @@ import { translate } from 'lit-translate'; import '../../finder-list.js'; import { Directory } from '../../finder-list.js'; -import { identity, isPublic, selector } from '../../foundation.js'; +import { find, identity, isPublic } from '../../foundation.js'; export function getDisplayString(entry: string): string { if (entry.startsWith('IED:')) return entry.replace(/^.*:/, '').trim(); @@ -17,7 +17,7 @@ export function getReader( ): (path: string[]) => Promise { return async (path: string[]) => { const [tagName, id] = path[path.length - 1]?.split(': ', 2); - const element = doc.querySelector(selector(tagName, id)); + const element = find(doc, tagName, id); if (!element) return { path, header: html`

${translate('error')}

`, entries: [] }; diff --git a/src/wizards/gsecontrol.ts b/src/wizards/gsecontrol.ts index ab7a91115..7f02cb626 100644 --- a/src/wizards/gsecontrol.ts +++ b/src/wizards/gsecontrol.ts @@ -18,6 +18,7 @@ import { createElement, Delete, EditorAction, + find, getUniqueElementName, getValue, identity, @@ -26,7 +27,6 @@ import { newActionEvent, newSubWizardEvent, newWizardEvent, - selector, SimpleAction, Wizard, WizardActor, @@ -335,7 +335,7 @@ function openGseControlCreateWizard(doc: XMLDocument): WizardActor { const [tagName, id] = path.pop()!.split(': '); if (tagName !== 'IED') return []; - const ied = doc.querySelector(selector(tagName, id)); + const ied = find(doc, tagName, id); if (!ied) return []; const ln0 = ied.querySelector('LN0'); @@ -562,9 +562,7 @@ export function selectGseControlWizard(element: Element): Wizard { @selected=${(e: SingleSelectedEvent) => { const gseControlIndentity = ((e.target).selected) .value; - const gseControl = element.querySelector( - selector('GSEControl', gseControlIndentity) - ); + const gseControl = find(element, 'GSEControl', gseControlIndentity); if (gseControl) { e.target!.dispatchEvent( newSubWizardEvent(() => editGseControlWizard(gseControl)) diff --git a/src/wizards/lnode.ts b/src/wizards/lnode.ts index 109519799..d4978e378 100644 --- a/src/wizards/lnode.ts +++ b/src/wizards/lnode.ts @@ -15,6 +15,7 @@ import { cloneElement, createElement, EditorAction, + find, getChildElementsByTagName, getValue, identity, @@ -22,7 +23,6 @@ import { newLogEvent, newWizardEvent, referencePath, - selector, Wizard, WizardActor, WizardInputElement, @@ -40,11 +40,7 @@ function createLNodeAction(parent: Element): WizardActor { const selectedLNodeTypes = list!.items .filter(item => item.selected) .map(item => item.value) - .map(identity => { - return parent.ownerDocument.querySelector( - selector('LNodeType', identity) - ); - }) + .map(identity => find(parent.ownerDocument, 'LNodeType', identity)) .filter(item => item !== null); const lnInstGenerator = newLnInstGenerator(parent); @@ -279,9 +275,9 @@ export function lNodeWizardAction(parent: Element): WizardActor { .filter(item => item.selected) .map(item => item.value) .map(identity => { - return parent.ownerDocument.querySelector(selector('LN0', identity)) - ? parent.ownerDocument.querySelector(selector('LN0', identity)) - : parent.ownerDocument.querySelector(selector('LN', identity)); + const ln0 = find(parent.ownerDocument, 'LN0', identity); + if (ln0) return ln0; + return find(parent.ownerDocument, 'LN', identity); }) .filter(item => item !== null); diff --git a/src/wizards/reportcontrol.ts b/src/wizards/reportcontrol.ts index 0e8d7d2a3..4577259d9 100644 --- a/src/wizards/reportcontrol.ts +++ b/src/wizards/reportcontrol.ts @@ -16,12 +16,12 @@ import { cloneElement, createElement, EditorAction, + find, getReference, getValue, identity, isPublic, newSubWizardEvent, - selector, SimpleAction, Wizard, WizardActor, @@ -307,7 +307,7 @@ function openReportControlCreateWizard(doc: XMLDocument): WizardActor { const [tagName, id] = path.pop()!.split(': '); if (tagName !== 'IED') return []; - const ied = doc.querySelector(selector(tagName, id)); + const ied = find(doc, tagName, id); if (!ied) return []; const ln0 = ied.querySelector('LN0'); @@ -421,7 +421,7 @@ function copyReportControlActions(element: Element): WizardActor { const complexActions: ComplexAction[] = []; iedItems.forEach(iedItem => { - const ied = doc.querySelector(selector('IED', iedItem.value)); + const ied = find(doc, 'IED', iedItem.value); if (!ied) return; const sinkLn0 = ied.querySelector('LN0'); @@ -738,9 +738,7 @@ export function selectReportControlWizard(element: Element): Wizard { html` { const identity = ((e.target).selected).value; - const reportControl = element.querySelector( - selector('ReportControl', identity) - ); + const reportControl = find(element, 'ReportControl', identity); if (!reportControl) return; e.target?.dispatchEvent( diff --git a/src/wizards/sampledvaluecontrol.ts b/src/wizards/sampledvaluecontrol.ts index 396dd3491..c3dc41aea 100644 --- a/src/wizards/sampledvaluecontrol.ts +++ b/src/wizards/sampledvaluecontrol.ts @@ -17,6 +17,7 @@ import { createElement, Delete, EditorAction, + find, getUniqueElementName, getValue, identity, @@ -25,7 +26,6 @@ import { newActionEvent, newSubWizardEvent, newWizardEvent, - selector, Wizard, WizardActor, WizardInputElement, @@ -455,7 +455,7 @@ function openSampledValueControlCreateWizard(doc: XMLDocument): WizardActor { const [tagName, id] = path.pop()!.split(': '); if (tagName !== 'IED') return []; - const ied = doc.querySelector(selector(tagName, id)); + const ied = find(doc, tagName, id); if (!ied) return []; const ln0 = ied.querySelector('LN0'); @@ -652,8 +652,10 @@ export function selectSampledValueControlWizard(element: Element): Wizard { html` { const identity = ((e.target).selected).value; - const sampledValueControl = element.querySelector( - selector('SampledValueControl', identity) + const sampledValueControl = find( + element, + 'SampledValueControl', + identity ); if (!sampledValueControl) return; diff --git a/test/unit/foundation.test.ts b/test/unit/foundation.test.ts index 60b8b5da6..5178f00bc 100644 --- a/test/unit/foundation.test.ts +++ b/test/unit/foundation.test.ts @@ -5,6 +5,7 @@ import { ComplexAction, depth, EditorAction, + find, findControlBlocks, findFCDAs, getChildElementsByTagName, @@ -26,7 +27,6 @@ import { newPendingStateEvent, newWizardEvent, SCLTag, - selector, tags, minAvailableLogicalNodeInstance, } from '../../src/foundation.js'; @@ -320,23 +320,23 @@ describe('foundation', () => { }); }); - describe('selector', () => { - it('returns negation pseudo-class for identity of type NaN', () => { + describe('find', () => { + it('returns null for the identity NaN', () => { const element = scl1.querySelector('Assotiation'); const ident = identity(element!); - expect(selector('Assotiation', ident)).to.equal(':not(*)'); + expect(find(scl1, 'Assotiation', ident)).to.equal(null); }); - it('returns correct selector for all tags except IEDName and ProtNs', () => { + it('returns correct element for all tags except IEDName and ProtNs', () => { Object.keys(tags).forEach(tag => { const element = Array.from(scl1.querySelectorAll(tag)).filter( item => !item.closest('Private') )[0]; if (element && tag !== 'IEDName' && tag !== 'ProtNs') - expect(element).to.satisfy((element: Element) => - element.isEqualNode( - scl1.querySelector(selector(tag, identity(element))) + expect(element) + .to.satisfy((element: Element) => + element.isEqualNode(find(scl1, tag, identity(element))) ) - ); + .and.to.equal(find(scl1, tag, identity(element))); }); }); }); From e6d175fe4c1397bd844ae7f334145827729e9737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Russ?= Date: Tue, 22 Aug 2023 11:25:30 +0200 Subject: [PATCH 08/15] doc: initalize ADRs --- .../0001-record-architecture-decisions.md | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 docs/decisions/0001-record-architecture-decisions.md diff --git a/docs/decisions/0001-record-architecture-decisions.md b/docs/decisions/0001-record-architecture-decisions.md new file mode 100644 index 000000000..4cdab76ce --- /dev/null +++ b/docs/decisions/0001-record-architecture-decisions.md @@ -0,0 +1,28 @@ +# 1. Record architecture decisions + +Date: 2023-07 + +## Status + +Accepted + +## Context + +We need to record the architectural decisions made on this project. + +## Decision + +We will follow the decisions recorded in the central organizational +repository ([.github](https://github.com/openscd/.github)), +and record new repo-specific decisions in this repository. + + +We write ADRs in the `docs/decisions` folder instead of a standard `doc/adr`: +- `docs` instead of `doc` because `doc` is used for the generated documentation. +- `decisions` instead of `adrs` because it is more explicit and a followed practice: + [↗ Markdown Any Decision Records - Applying MADR to your project ](https://adr.github.io/madr/#applying-madr-to-your-project) + +## Consequences + +- It will be harder to track which decisions have to be taken into consideration +- Local decisions will be easier to find From a1fc60b301d1330b428637961db8bcd1e1656538 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Russ?= Date: Wed, 23 Aug 2023 22:16:58 +0200 Subject: [PATCH 09/15] infra: mark stale issues automatically (#1308) --- .github/workflows/stale-issues-cron.yml | 11 +++++++ .github/workflows/stale-issues-manual.yml | 11 +++++++ .github/workflows/stale-issues.yml | 40 +++++++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 .github/workflows/stale-issues-cron.yml create mode 100644 .github/workflows/stale-issues-manual.yml create mode 100644 .github/workflows/stale-issues.yml diff --git a/.github/workflows/stale-issues-cron.yml b/.github/workflows/stale-issues-cron.yml new file mode 100644 index 000000000..d1b74a132 --- /dev/null +++ b/.github/workflows/stale-issues-cron.yml @@ -0,0 +1,11 @@ +name: Mark stale issues (cronjob) + +on: + schedule: + - cron: '0 19 * * *' + +jobs: + run-stale: + uses: "./.github/workflows/stale-issues.yml" + secrets: + TOKEN_FOR_STALE_ISSUES: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/stale-issues-manual.yml b/.github/workflows/stale-issues-manual.yml new file mode 100644 index 000000000..8f8d7c80b --- /dev/null +++ b/.github/workflows/stale-issues-manual.yml @@ -0,0 +1,11 @@ + +name: Mark stale issues (manual) + +on: + workflow_dispatch + +jobs: + run-stale: + uses: "./.github/workflows/stale-issues.yml" + secrets: + TOKEN_FOR_STALE_ISSUES: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml new file mode 100644 index 000000000..e9b3ace7e --- /dev/null +++ b/.github/workflows/stale-issues.yml @@ -0,0 +1,40 @@ +# This workflow labels stale issues. +# +# For more information, see: +# https://github.com/actions/stale +name: Mark stale issues + +on: + workflow_call: + secrets: + TOKEN_FOR_STALE_ISSUES: + required: true + +jobs: + stale: + + runs-on: ubuntu-latest + permissions: + issues: write + + steps: + - uses: actions/stale@v5 + with: + debug-only: true # temporary + repo-token: ${{ secrets.TOKEN_FOR_STALE_ISSUES }} + days-before-stale: 60 + stale-issue-label: 'stale' + days-before-close: -1 # `-1` disables closing + stale-issue-message: | + Hello there, + + Thank you for opening this issue! We appreciate your interest in our project. + However, it seems that this issue hasn't had any activity for a while. To ensure that our issue tracker remains organized and efficient, we occasionally review and address stale issues. + + If you believe this issue is still relevant and requires attention, please provide any additional context, updates, or details that might help us understand the problem better. + Feel free to continue the conversation here. + + If the issue is no longer relevant, you can simply close it. If you're uncertain, you can always reopen it later. + + Remember, our project thrives on community contributions, and your input matters. We're here to collaborate and improve. + Thank you for being part of this journey! From 8d5de2d93714cef7f5a725704a6adf38602b581c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Russ?= Date: Thu, 24 Aug 2023 21:15:50 +0200 Subject: [PATCH 10/15] fix(ci): remove unneeded permission check --- .github/workflows/stale-issues.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml index e9b3ace7e..27fd86083 100644 --- a/.github/workflows/stale-issues.yml +++ b/.github/workflows/stale-issues.yml @@ -12,10 +12,7 @@ on: jobs: stale: - runs-on: ubuntu-latest - permissions: - issues: write steps: - uses: actions/stale@v5 From 29d4f7f2ab9c348ff0c855b2b691a5a2628a8e37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Russ?= Date: Fri, 25 Aug 2023 10:46:14 +0200 Subject: [PATCH 11/15] infra: enable stale action --- .github/workflows/stale-issues.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml index 27fd86083..c8e735adf 100644 --- a/.github/workflows/stale-issues.yml +++ b/.github/workflows/stale-issues.yml @@ -17,7 +17,7 @@ jobs: steps: - uses: actions/stale@v5 with: - debug-only: true # temporary + debug-only: false repo-token: ${{ secrets.TOKEN_FOR_STALE_ISSUES }} days-before-stale: 60 stale-issue-label: 'stale' From a170e8b4e06f6871b1895d64dd710ccdcf76bc1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Russ?= Date: Wed, 30 Aug 2023 12:57:26 +0200 Subject: [PATCH 12/15] fix: stale issue action --- .github/workflows/stale-issues-cron.yml | 11 ----------- .github/workflows/stale-issues-manual.yml | 11 ----------- .github/workflows/stale-issues.yml | 21 ++++++++++++++------- 3 files changed, 14 insertions(+), 29 deletions(-) delete mode 100644 .github/workflows/stale-issues-cron.yml delete mode 100644 .github/workflows/stale-issues-manual.yml diff --git a/.github/workflows/stale-issues-cron.yml b/.github/workflows/stale-issues-cron.yml deleted file mode 100644 index d1b74a132..000000000 --- a/.github/workflows/stale-issues-cron.yml +++ /dev/null @@ -1,11 +0,0 @@ -name: Mark stale issues (cronjob) - -on: - schedule: - - cron: '0 19 * * *' - -jobs: - run-stale: - uses: "./.github/workflows/stale-issues.yml" - secrets: - TOKEN_FOR_STALE_ISSUES: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/stale-issues-manual.yml b/.github/workflows/stale-issues-manual.yml deleted file mode 100644 index 8f8d7c80b..000000000 --- a/.github/workflows/stale-issues-manual.yml +++ /dev/null @@ -1,11 +0,0 @@ - -name: Mark stale issues (manual) - -on: - workflow_dispatch - -jobs: - run-stale: - uses: "./.github/workflows/stale-issues.yml" - secrets: - TOKEN_FOR_STALE_ISSUES: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml index c8e735adf..ef5f44a17 100644 --- a/.github/workflows/stale-issues.yml +++ b/.github/workflows/stale-issues.yml @@ -5,23 +5,27 @@ name: Mark stale issues on: - workflow_call: - secrets: - TOKEN_FOR_STALE_ISSUES: - required: true + workflow_dispatch: + schedule: + - cron: '0 19 * * *' jobs: stale: runs-on: ubuntu-latest + permissions: + issues: write steps: - uses: actions/stale@v5 with: - debug-only: false - repo-token: ${{ secrets.TOKEN_FOR_STALE_ISSUES }} + repo-token: ${{ secrets.GITHUB_TOKEN }} + days-before-stale: 60 + days-before-close: -1 + days-before-pr-stale: -1 + days-before-pr-close: -1 + stale-issue-label: 'stale' - days-before-close: -1 # `-1` disables closing stale-issue-message: | Hello there, @@ -35,3 +39,6 @@ jobs: Remember, our project thrives on community contributions, and your input matters. We're here to collaborate and improve. Thank you for being part of this journey! + + + From 8991c58e9fac1e6e494bfd675c3b92ff21e9d5d8 Mon Sep 17 00:00:00 2001 From: Pascal Wilbrink Date: Mon, 11 Sep 2023 15:54:25 +0200 Subject: [PATCH 13/15] Feat: Updated version in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9238aa93e..0b16a5abb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-scd", - "version": "0.33.0", + "version": "0.34.0", "repository": "https://github.com/openscd/open-scd.git", "description": "A bottom-up substation configuration designer for projects described using SCL `IEC 61850-6` Edition 2 or greater.", "keywords": [ From d745c1b8bb6c6187e61e3b005ce1e857ef5117eb Mon Sep 17 00:00:00 2001 From: Pascal Wilbrink Date: Mon, 11 Sep 2023 16:05:11 +0200 Subject: [PATCH 14/15] Reverted importIED to Compas-openscd/main --- src/menu/ImportIEDs.ts | 93 +++++++++++++++++++----------------------- 1 file changed, 41 insertions(+), 52 deletions(-) diff --git a/src/menu/ImportIEDs.ts b/src/menu/ImportIEDs.ts index a0163951e..a1bb1edcc 100644 --- a/src/menu/ImportIEDs.ts +++ b/src/menu/ImportIEDs.ts @@ -19,11 +19,12 @@ import { ListItemBase } from '@material/mwc-list/mwc-list-item-base'; import '../filtered-list.js'; import { createElement, - find, identity, isPublic, newActionEvent, newLogEvent, + newPendingStateEvent, + selector, SimpleAction, } from '../foundation.js'; @@ -367,6 +368,12 @@ function isIedNameUnique(ied: Element, doc: Document): boolean { return true; } +function resetSelection(dialog: Dialog): void { + ( + (dialog.querySelector('filtered-list') as List).selected as ListItemBase[] + ).forEach(item => (item.selected = false)); +} + export default class ImportingIedPlugin extends LitElement { @property({ attribute: false }) doc!: XMLDocument; @@ -374,13 +381,12 @@ export default class ImportingIedPlugin extends LitElement { editCount = -1; @state() - iedSelection: TemplateResult[] = []; + importDoc?: XMLDocument; @query('#importied-plugin-input') pluginFileUI!: HTMLInputElement; @query('mwc-dialog') dialog!: Dialog; async run(): Promise { - this.iedSelection = []; this.pluginFileUI.click(); } @@ -417,7 +423,6 @@ export default class ImportingIedPlugin extends LitElement { // This doesn't provide redo/undo capability as it is not using the Editing // action API. To use it would require us to cache the full SCL file in // OpenSCD as it is now which could use significant memory. - // TODO: In open-scd core update this to allow including in undo/redo. updateNamespaces( this.doc.documentElement, @@ -442,25 +447,19 @@ export default class ImportingIedPlugin extends LitElement { ); } - private async importIEDs( - importDoc: XMLDocument, - fileName: string - ): Promise { - const documentDialog: Dialog = this.shadowRoot!.querySelector( - `mwc-dialog[data-file="${fileName}"]` - )!; - + private async importIEDs(): Promise { const selectedItems = ( - (documentDialog.querySelector('filtered-list')).selected + (this.dialog.querySelector('filtered-list')).selected ); const ieds = selectedItems .map(item => { - return find(importDoc, 'IED', item.value); + return this.importDoc!.querySelector(selector('IED', item.value)); }) .filter(ied => ied) as Element[]; - documentDialog.close(); + resetSelection(this.dialog); + this.dialog.close(); for (const ied of ieds) { this.importIED(ied); @@ -468,8 +467,8 @@ export default class ImportingIedPlugin extends LitElement { } } - async prepareImport(importDoc: XMLDocument, fileName: string): Promise { - if (!importDoc) { + public prepareImport(): void { + if (!this.importDoc) { this.dispatchEvent( newLogEvent({ kind: 'error', @@ -479,7 +478,7 @@ export default class ImportingIedPlugin extends LitElement { return; } - if (importDoc.querySelector('parsererror')) { + if (this.importDoc.querySelector('parsererror')) { this.dispatchEvent( newLogEvent({ kind: 'error', @@ -489,7 +488,7 @@ export default class ImportingIedPlugin extends LitElement { return; } - const ieds = Array.from(importDoc.querySelectorAll(':root > IED')); + const ieds = Array.from(this.importDoc.querySelectorAll(':root > IED')); if (ieds.length === 0) { this.dispatchEvent( newLogEvent({ @@ -502,23 +501,10 @@ export default class ImportingIedPlugin extends LitElement { if (ieds.length === 1) { this.importIED(ieds[0]); - return await this.docUpdate(); + return; } - this.buildIedSelection(importDoc, fileName); - await this.requestUpdate(); - const dialog = ( - this.shadowRoot!.querySelector(`mwc-dialog[data-file="${fileName}"]`) - ); - dialog.show(); - - // await closing of dialog - await new Promise(resolve => { - dialog.addEventListener('closed', function onClosed(evt) { - evt.target?.removeEventListener('closed', onClosed); - resolve(); - }); - }); + this.dialog.show(); } /** Loads the file `event.target.files[0]` into [[`src`]] as a `blob:...`. */ @@ -527,20 +513,23 @@ export default class ImportingIedPlugin extends LitElement { (event.target)?.files ?? [] ); - const promises = files.map(file => { - return { - text: file - .text() - .then(text => - new DOMParser().parseFromString(text, 'application/xml') - ), - name: file.name, - }; + const promises = files.map(async file => { + this.importDoc = new DOMParser().parseFromString( + await file.text(), + 'application/xml' + ); + + return this.prepareImport(); }); - for await (const file of promises) { - await this.prepareImport(await file.text, file.name); - } + const mergedPromise = new Promise((resolve, reject) => + Promise.allSettled(promises).then( + () => resolve(), + () => reject() + ) + ); + + this.dispatchEvent(newPendingStateEvent(mergedPromise)); } protected renderInput(): TemplateResult { @@ -550,10 +539,10 @@ export default class ImportingIedPlugin extends LitElement { }} id="importied-plugin-input" accept=".sed,.scd,.ssd,.isd,.iid,.cid,.icd" type="file">`; } - protected buildIedSelection(importDoc: XMLDocument, fileName: string): void { - this.iedSelection.push(html` + protected renderIedSelection(): TemplateResult { + return html` - ${Array.from(importDoc?.querySelectorAll(':root > IED') ?? []).map( + ${Array.from(this.importDoc?.querySelectorAll(':root > IED') ?? []).map( ied => html`${ied.getAttribute('name')} this.importIEDs(importDoc, fileName)} + @click=${this.importIEDs} > - `); + `; } render(): TemplateResult { - return html`${this.iedSelection}${this.renderInput()}`; + return html`${this.renderIedSelection()}${this.renderInput()}`; } static styles = css` From ac23853e1dd0de0e6de5f2834fc5659226ca9586 Mon Sep 17 00:00:00 2001 From: Pascal Wilbrink Date: Mon, 11 Sep 2023 16:15:23 +0200 Subject: [PATCH 15/15] Fixed test --- src/menu/ImportIEDs.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/menu/ImportIEDs.ts b/src/menu/ImportIEDs.ts index a1bb1edcc..e488245eb 100644 --- a/src/menu/ImportIEDs.ts +++ b/src/menu/ImportIEDs.ts @@ -24,7 +24,7 @@ import { newActionEvent, newLogEvent, newPendingStateEvent, - selector, + find, SimpleAction, } from '../foundation.js'; @@ -454,7 +454,7 @@ export default class ImportingIedPlugin extends LitElement { const ieds = selectedItems .map(item => { - return this.importDoc!.querySelector(selector('IED', item.value)); + return find(this.importDoc!, 'IED', item.value); }) .filter(ied => ied) as Element[];