From 7f4f929553a8e27ec62296e13ef500d297ebb06c Mon Sep 17 00:00:00 2001 From: Jakob Vogelsang Date: Thu, 17 Feb 2022 17:08:54 +0100 Subject: [PATCH 1/5] feat(translations): add smvOpts related --- src/translations/de.ts | 9 ++++++++- src/translations/en.ts | 8 +++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/translations/de.ts b/src/translations/de.ts index c2d17f67cf..eec38bdee4 100644 --- a/src/translations/de.ts +++ b/src/translations/de.ts @@ -40,6 +40,12 @@ export const de: Translations = { smpMod: 'Abtast-Art', smpRate: 'Abtastrate', nofASDU: 'Abtastpunkte pro Datenpacket', + SmvOpts: 'Optionale Informationen', + refreshTime: 'Zeitstempel des Abtastwertes zu Telegram hinzufügen', + sampleRate: 'Abtastrate zu Telegram hinzufügen', + dataSet: 'Datensatznamen zu Telegram hinzufügen', + security: 'Potentiel in Zukunft für z.B. digitale Signature', + synchSourceId: 'Identität der Zeitquelle zu Telegram hinzufügen', }, settings: { title: 'Einstellungen', @@ -50,7 +56,8 @@ export const de: Translations = { showieds: 'Zeige IEDs im Substation-Editor', selectFileButton: 'Datei auswählen', loadNsdTranslations: 'NSDoc-Dateien hochladen', - invalidFileNoIdFound: 'Ungültiges NSDoc; kein \'id\'-Attribut in der Datei gefunden' + invalidFileNoIdFound: + "Ungültiges NSDoc; kein 'id'-Attribut in der Datei gefunden", }, menu: { new: 'Neues projekt', diff --git a/src/translations/en.ts b/src/translations/en.ts index db79dd4e17..a35ea6506b 100644 --- a/src/translations/en.ts +++ b/src/translations/en.ts @@ -38,6 +38,12 @@ export const en = { smpMod: 'Sample mode', smpRate: 'Sample rate', nofASDU: 'Samples per paket', + SmvOpts: 'Optional Information', + refreshTime: 'Add time stamp to SMV telegram', + sampleRate: 'Add sampled rete time to SMV telegram', + dataSet: 'Add DataSet name to SMV telegram', + security: 'Potential future use. E.g. dig signature', + synchSourceId: 'Add identity of sync source to SMV telegram', }, settings: { title: 'Settings', @@ -48,7 +54,7 @@ export const en = { showieds: 'Show IEDs in substation editor', selectFileButton: 'Select file', loadNsdTranslations: 'Uploading NSDoc files', - invalidFileNoIdFound: 'Invalid NSDoc; no \'id\' attribute found in file' + invalidFileNoIdFound: "Invalid NSDoc; no 'id' attribute found in file", }, menu: { new: 'New project', From 44d6b045bf46ca88c3ac60d825330dc3a5605b88 Mon Sep 17 00:00:00 2001 From: Jakob Vogelsang Date: Thu, 17 Feb 2022 17:10:30 +0100 Subject: [PATCH 2/5] feat(wizards/smvopts): add edit wizard --- src/wizards/smvopts.ts | 86 +++++++++ .../__snapshots__/smvopts.test.snap.js | 60 +++++++ test/unit/wizards/smvopts.test.ts | 170 ++++++++++++++++++ 3 files changed, 316 insertions(+) create mode 100644 src/wizards/smvopts.ts create mode 100644 test/unit/wizards/__snapshots__/smvopts.test.snap.js create mode 100644 test/unit/wizards/smvopts.test.ts diff --git a/src/wizards/smvopts.ts b/src/wizards/smvopts.ts new file mode 100644 index 0000000000..358a397f28 --- /dev/null +++ b/src/wizards/smvopts.ts @@ -0,0 +1,86 @@ +import { html, TemplateResult } from 'lit-element'; +import { get, translate } from 'lit-translate'; + +import { + cloneElement, + getValue, + Wizard, + WizardAction, + WizardActor, + WizardInput, +} from '../foundation.js'; + +interface ContentOptions { + refreshTime: string | null; + sampleRate: string | null; + dataSet: string | null; + security: string | null; + synchSourceId: string | null; +} + +function contentSmvOptsWizard(option: ContentOptions): TemplateResult[] { + return Object.entries(option).map( + ([key, value]) => + html`` + ); +} + +function updateSmvOptsAction(element: Element): WizardActor { + return (inputs: WizardInput[]): WizardAction[] => { + const attributes: Record = {}; + const attributeKeys = [ + 'refreshTime', + 'sampleRate', + 'dataSet', + 'security', + 'synchSourceId', + ]; + attributeKeys.forEach(key => { + attributes[key] = getValue(inputs.find(i => i.label === key)!); + }); + + if ( + !attributeKeys.some(key => attributes[key] !== element.getAttribute(key)) + ) + return []; + + const newElement = cloneElement(element, attributes); + return [{ old: { element }, new: { element: newElement } }]; + }; +} + +export function editSmvOptsWizard(element: Element): Wizard { + const [refreshTime, sampleRate, dataSet, security, synchSourceId] = [ + 'refreshTime', + 'sampleRate', + 'dataSet', + 'security', + 'synchSourceId', + ].map(smvopt => element.getAttribute(smvopt)); + + return [ + { + title: get('wizard.title.edit', { tagName: element.tagName }), + element, + primary: { + icon: 'save', + label: get('save'), + action: updateSmvOptsAction(element), + }, + content: [ + ...contentSmvOptsWizard({ + refreshTime, + sampleRate, + dataSet, + security, + synchSourceId, + }), + ], + }, + ]; +} diff --git a/test/unit/wizards/__snapshots__/smvopts.test.snap.js b/test/unit/wizards/__snapshots__/smvopts.test.snap.js new file mode 100644 index 0000000000..1cd6742471 --- /dev/null +++ b/test/unit/wizards/__snapshots__/smvopts.test.snap.js @@ -0,0 +1,60 @@ +/* @web/test-runner snapshot v1 */ +export const snapshots = {}; + +snapshots["Wizards for SCL SmvOpts element define an edit wizard that looks like the latest snapshot"] = +` +
+ + + + + + + + + + +
+ + + + +
+`; +/* end snapshot Wizards for SCL SmvOpts element define an edit wizard that looks like the latest snapshot */ + diff --git a/test/unit/wizards/smvopts.test.ts b/test/unit/wizards/smvopts.test.ts new file mode 100644 index 0000000000..97bfb21acf --- /dev/null +++ b/test/unit/wizards/smvopts.test.ts @@ -0,0 +1,170 @@ +import { expect, fixture, html } from '@open-wc/testing'; +import { SinonSpy, spy } from 'sinon'; + +import '../../mock-wizard.js'; +import { MockWizard } from '../../mock-wizard.js'; + +import { WizardCheckbox } from '../../../src/wizard-checkbox.js'; +import { isUpdate, Update } from '../../../src/foundation.js'; +import { editSmvOptsWizard } from '../../../src/wizards/smvopts.js'; + +describe('Wizards for SCL SmvOpts element', () => { + let element: MockWizard; + let smvOpts: Element; + let inputs: WizardCheckbox[]; + + let primaryAction: HTMLElement; + + let actionEvent: SinonSpy; + + beforeEach(async () => { + element = await fixture(html``); + smvOpts = ( + new DOMParser().parseFromString( + ``, + 'application/xml' + ).documentElement + ); + + actionEvent = spy(); + window.addEventListener('editor-action', actionEvent); + }); + + describe('define an edit wizard that', () => { + beforeEach(async () => { + const wizard = editSmvOptsWizard(smvOpts); + element.workflow.push(() => wizard); + await element.requestUpdate(); + + inputs = (Array.from(element.wizardUI.inputs)); + await element.requestUpdate(); + + primaryAction = ( + element.wizardUI.dialog?.querySelector( + 'mwc-button[slot="primaryAction"]' + ) + ); + }); + + it('looks like the latest snapshot', async () => { + await expect(element.wizardUI.dialog).dom.to.equalSnapshot(); + }).timeout(5000); + + it('does not update a SmvOpts element with no changed attributes', async () => { + primaryAction.click(); + await element.requestUpdate(); + expect(actionEvent.notCalled).to.be.true; + }); + + it('update the SmvOpts element with changed dataSet attribute', async () => { + const input = inputs[2]; + input.maybeValue = 'false'; + await input.requestUpdate(); + + primaryAction.click(); + await element.requestUpdate(); + expect(actionEvent).to.be.calledOnce; + + const action = actionEvent.args[0][0].detail.action; + expect(action).to.satisfy(isUpdate); + + const updateAction = action; + expect(updateAction.old.element).to.have.attribute('dataSet', 'true'); + expect(updateAction.new.element).to.have.attribute('dataSet', 'false'); + }); + + it('removes the SmvOpts attribute dataSet with nulled select', async () => { + const input = inputs[2]; + input.nullSwitch?.click(); + await input.requestUpdate(); + + primaryAction.click(); + await element.requestUpdate(); + expect(actionEvent).to.be.calledOnce; + + const action = actionEvent.args[0][0].detail.action; + expect(action).to.satisfy(isUpdate); + + const updateAction = action; + expect(updateAction.old.element).to.have.attribute('dataSet', 'true'); + expect(updateAction.new.element).to.not.have.attribute('dataSet'); + }); + + it('updates the SmvOpts element with changed refreshTime attribute', async () => { + const input = inputs[0]; + input.maybeValue = 'false'; + await input.requestUpdate(); + + primaryAction.click(); + await element.requestUpdate(); + expect(actionEvent).to.be.calledOnce; + + const action = actionEvent.args[0][0].detail.action; + expect(action).to.satisfy(isUpdate); + + const updateAction = action; + expect(updateAction.old.element).to.have.attribute('refreshTime', 'true'); + expect(updateAction.new.element).to.have.attribute( + 'refreshTime', + 'false' + ); + }); + + it('updates the SmvOpts element with changed sampleRate attribute', async () => { + const input = inputs[1]; + input.nullSwitch?.click(); + input.maybeValue = 'true'; + await input.requestUpdate(); + + primaryAction.click(); + await element.requestUpdate(); + expect(actionEvent).to.be.calledOnce; + + const action = actionEvent.args[0][0].detail.action; + expect(action).to.satisfy(isUpdate); + + const updateAction = action; + expect(updateAction.old.element).to.not.have.attribute('sampleRate'); + expect(updateAction.new.element).to.have.attribute('sampleRate', 'true'); + }); + + it('updates the SmvOpts element with changed security attribute', async () => { + const input = inputs[3]; + input.nullSwitch?.click(); + input.maybeValue = 'true'; + await input.requestUpdate(); + + primaryAction.click(); + await element.requestUpdate(); + expect(actionEvent).to.be.calledOnce; + + const action = actionEvent.args[0][0].detail.action; + expect(action).to.satisfy(isUpdate); + + const updateAction = action; + expect(updateAction.old.element).to.not.have.attribute('security'); + expect(updateAction.new.element).to.have.attribute('security', 'true'); + }); + + it('updates the SmvOpts element with changed synchSourceId attribute', async () => { + const input = inputs[4]; + input.nullSwitch?.click(); + input.maybeValue = 'true'; + await input.requestUpdate(); + + primaryAction.click(); + await element.requestUpdate(); + expect(actionEvent).to.be.calledOnce; + + const action = actionEvent.args[0][0].detail.action; + expect(action).to.satisfy(isUpdate); + + const updateAction = action; + expect(updateAction.old.element).to.not.have.attribute('synchSourceId'); + expect(updateAction.new.element).to.have.attribute( + 'synchSourceId', + 'true' + ); + }); + }); +}); From b068025e3f2c544a1c8038a03d154eabe676d4a2 Mon Sep 17 00:00:00 2001 From: Jakob Vogelsang Date: Thu, 17 Feb 2022 17:11:05 +0100 Subject: [PATCH 3/5] feat(wizards/sampledvaluecontrol): add smvopts button --- src/wizards/sampledvaluecontrol.ts | 12 ++++++++++ ...pledvaluecontrol-wizarding-editing.test.ts | 23 +++++++++++++++++++ .../sampledvaluecontrol.test.snap.js | 6 +++++ 3 files changed, 41 insertions(+) diff --git a/src/wizards/sampledvaluecontrol.ts b/src/wizards/sampledvaluecontrol.ts index 76761e85b8..2dd26df14e 100644 --- a/src/wizards/sampledvaluecontrol.ts +++ b/src/wizards/sampledvaluecontrol.ts @@ -28,6 +28,7 @@ import { import { securityEnableEnum, smpModEnum } from './foundation/enums.js'; import { maxLength, patterns } from './foundation/limits.js'; import { editSMvWizard } from './smv.js'; +import { editSmvOptsWizard } from './smvopts.js'; function getSMV(element: Element): Element | null { const cbName = element.getAttribute('name'); @@ -224,6 +225,7 @@ export function editSampledValueControlWizard(element: Element): Wizard { const securityEnable = element.getAttribute('securityEnabled'); const sMV = getSMV(element); + const smvOpts = element.querySelector('SmvOpts')!; return [ { @@ -257,6 +259,16 @@ export function editSampledValueControlWizard(element: Element): Wizard { }}}" >` : html``, + html``, html` { let doc: XMLDocument; @@ -239,6 +240,28 @@ describe('Wizards for SCL element SampledValueControl', () => { ); }); + it('opens a edit wizard for SMV on edit SMV button click', async () => { + const editSmvOptsButton =