From a238cc97c271ce6fb55116c438d8f449a5e8d54c Mon Sep 17 00:00:00 2001 From: Juan Munoz Date: Wed, 17 May 2023 09:21:40 +0200 Subject: [PATCH] feat(openscd):Add multiple Val element into DAI distinguished by sGroup (#1221) * feat:add multiple Val elements in createDAIWizard * test: fixing dai-field-type snapshot * test:update DAI unit tests with settingGroups file * test: integration test for multiple Val wizard * fix: remove unnecessary type * fix: update typing in checkForMultipleSettings fn * refactor: helper fn emptyIfNull reduces complexity --- src/wizards/dai.ts | 91 +++++- src/wizards/foundation/dai-field-type.ts | 189 +++++++---- test/integration/editors/IED.test.ts | 82 +++++ test/testfiles/wizards/settingGroups.scd | 294 ++++++++++++++++++ test/unit/wizards/dai.test.ts | 14 +- .../__snapshots__/dai-field-type.test.snap.js | 2 +- 6 files changed, 583 insertions(+), 89 deletions(-) create mode 100644 test/testfiles/wizards/settingGroups.scd diff --git a/src/wizards/dai.ts b/src/wizards/dai.ts index bc78435938..24a2968721 100644 --- a/src/wizards/dai.ts +++ b/src/wizards/dai.ts @@ -41,18 +41,41 @@ export function createValue( parent: Element, element: Element, newElement: Element, - instanceElement: Element + instanceElement: Element, + numberOfmultipleSettings?: number ): WizardActor { return (inputs: WizardInputElement[]): EditorAction[] => { const bType = element.getAttribute('bType')!; - const newValue = getCustomField()[bType].value(inputs); - - let valElement = instanceElement.querySelector('Val'); - if (!valElement) { - valElement = parent.ownerDocument.createElementNS(SCL_NAMESPACE, 'Val'); - instanceElement.append(valElement); + if (numberOfmultipleSettings) { + //Should we remove all Val elements before adding new ones? + Array.from(instanceElement.querySelectorAll('Val')).forEach(item => + item.remove() + ); + // Adds a new Val element for each sGroup value from the wizard + [...Array(numberOfmultipleSettings)].forEach((item, i) => { + const newValue = getCustomField()[bType].value( + inputs, + i + 1 + ); + + const valElement = parent.ownerDocument.createElementNS( + SCL_NAMESPACE, + 'Val' + ); + valElement.textContent = newValue; + valElement.setAttribute('sGroup', `${i + 1}`); + instanceElement.append(valElement); + }); + } else { + const newValue = getCustomField()[bType].value(inputs); + + let valElement = instanceElement.querySelector('Val'); + if (!valElement) { + valElement = parent.ownerDocument.createElementNS(SCL_NAMESPACE, 'Val'); + instanceElement.append(valElement); + } + valElement.textContent = newValue; } - valElement.textContent = newValue; const name = instanceElement.getAttribute('name'); const complexAction: ComplexAction = { @@ -65,7 +88,8 @@ export function createValue( export function renderDAIWizard( element: Element, - instanceElement?: Element + instanceElement?: Element, + numberOfmultipleSettings: number | null = null ): TemplateResult[] { const bType = element.getAttribute('bType')!; const daValue = element.querySelector('Val')?.textContent?.trim() ?? ''; @@ -73,7 +97,8 @@ export function renderDAIWizard( return [ html` ${getCustomField()[bType].render( element, - instanceElement + instanceElement, + numberOfmultipleSettings )} ${daValue ? html`element.getRootNode()).querySelector( + `DOType>DA[type="${element.parentElement!.id}"]` + )!; + const fc = da.getAttribute('fc') ?? ''; + // Check if the closest IED to the parent element has a SettingControl element with a numOfSGs attribute + const ied = parent.closest('IED'); + const settingControl = ied?.querySelector('SettingControl'); + const numOfSGsAttribute = settingControl?.getAttribute('numOfSGs') ?? ''; + const numberOfmultipleSettings = parseInt(numOfSGsAttribute); + // If the DA has the functional constraint SG or SE and the IED has a SettingControl element with a numOfSGs attribute, then the DAI is a multiple setting group + return (fc === 'SG' || fc === 'SE') && + numOfSGsAttribute !== '' && + !isNaN(numberOfmultipleSettings) + ? numberOfmultipleSettings + : undefined; +} + export function createDAIWizard( parent: Element, newElement: Element, element: Element ): Wizard { // Retrieve the created DAI, can be the new element or one of the child elements below. + const numberOfmultipleSettings = checkForMultipleSettings(parent, element); const instanceElement = newElement.tagName === 'DAI' ? newElement @@ -108,9 +165,19 @@ export function createDAIWizard( primary: { icon: 'edit', label: get('save'), - action: createValue(parent, element, newElement, instanceElement), + action: createValue( + parent, + element, + newElement, + instanceElement, + numberOfmultipleSettings + ), }, - content: renderDAIWizard(element, instanceElement), + content: renderDAIWizard( + element, + instanceElement, + numberOfmultipleSettings + ), }, ]; } diff --git a/src/wizards/foundation/dai-field-type.ts b/src/wizards/foundation/dai-field-type.ts index c16a8465c1..0933c9e62a 100644 --- a/src/wizards/foundation/dai-field-type.ts +++ b/src/wizards/foundation/dai-field-type.ts @@ -9,8 +9,12 @@ import '../../../src/wizard-select.js'; import { getValue, WizardInputElement } from '../../foundation.js'; export interface CustomField { - render(element: Element, instanceElement?: Element): TemplateResult[]; - value(inputs: WizardInputElement[]): string | null; + render( + element: Element, + instanceElement?: Element, + numOfSGs?: number | null + ): TemplateResult[]; + value(inputs: WizardInputElement[], sGroup?: number | null): string | null; } const daiFieldTypes = [ @@ -36,7 +40,9 @@ const daiFieldTypes = [ 'VisString255', ] as const; export type DaiFieldTypes = typeof daiFieldTypes[number]; - +const emptyIfNull = (item: T | null, value: string): string => { + return item === null ? '' : value; +}; export function getCustomField(): Record { return { BOOLEAN: booleanField(), @@ -63,32 +69,44 @@ export function getCustomField(): Record { function booleanField(): CustomField { return { - render: (element: Element, instanceElement?: Element) => { - return [ - html` { + // If numOfSGs is -1, then it is a single value, otherwise it is treated as a group of values + return (numOfSGs ? [...Array(numOfSGs)] : [numOfSGs]).map((item, i) => { + return html` true false - `, - ]; + `; + }); }, - value: (inputs: WizardInputElement[]) => { - return getValue(inputs.find(input => input.id === 'Val')!); + value: (inputs: WizardInputElement[], sGroup: number | null) => { + return getValue( + inputs.find(input => input.id === `Val${sGroup || ''}`)! + ); }, }; } function enumField(): CustomField { return { - render: (element: Element, instanceElement?: Element) => { - return [ - html` { + // If numOfSGs is -1, then it is a single value, otherwise it is treated as a group of values + return (numOfSGs ? [...Array(numOfSGs)] : [numOfSGs]).map((item, i) => { + return html` @@ -97,22 +115,29 @@ export function getCustomField(): Record { >${enumValue}`; })} - `, - ]; + `; + }); }, - value: (inputs: WizardInputElement[]) => { - return getValue(inputs.find(input => input.id === 'Val')!); + value: (inputs: WizardInputElement[], sGroup: number | null) => { + return getValue( + inputs.find(input => input.id === `Val${sGroup || ''}`)! + ); }, }; } function floatField(type: string, min: number, max: number): CustomField { return { - render: (element: Element, instanceElement?: Element) => { - return [ - html` { + // If numOfSGs is -1, then it is a single value, otherwise it is treated as a group of values + return (numOfSGs ? [...Array(numOfSGs)] : [numOfSGs]).map((item, i) => { + return html` { max=${max} step="0.1" > - `, - ]; + `; + }); }, - value: (inputs: WizardInputElement[]) => { - return getValue(inputs.find(input => input.id === 'Val')!); + value: (inputs: WizardInputElement[], sGroup: number | null) => { + return getValue( + inputs.find(input => input.id === `Val${sGroup || ''}`)! + ); }, }; } function integerField(type: string, min: number, max: number): CustomField { return { - render: (element: Element, instanceElement?: Element) => { - return [ - html` { + // If numOfSGs is -1, then it is a single value, otherwise it is treated as a group of values + return (numOfSGs ? [...Array(numOfSGs)] : [numOfSGs]).map((item, i) => { + return html` - `, - ]; + `; + }); }, - value: (inputs: WizardInputElement[]) => { - return getValue(inputs.find(input => input.id === 'Val')!); + value: (inputs: WizardInputElement[], sGroup: number | null) => { + return getValue( + inputs.find(input => input.id === `Val${sGroup || ''}`)! + ); }, }; } function timestampField(): CustomField { return { - render: (element: Element, instanceElement?: Element) => { + render: ( + element: Element, + instanceElement?: Element, + numOfSGs: number | null = null + ) => { + // If numOfSGs is -1, then it is a single value, otherwise it is treated as a group of values const value = getInstanceValue(instanceElement); - return [ - html` - `, - html` - `, - ]; + return (numOfSGs ? [...Array(numOfSGs)] : [numOfSGs]).reduce( + (acc: TemplateResult[], item, i) => { + return acc.concat([ + html` + `, + html` + `, + ]); + }, + [] + ); }, - value: (inputs: WizardInputElement[]) => { - const values = ['ValDate', 'ValTime'].map(id => - getValue(inputs.find(input => input.id === id)!) + value: (inputs: WizardInputElement[], sGroup: number | null) => { + const values = [`ValDate${sGroup || ''}`, `ValTime${sGroup || ''}`].map( + id => getValue(inputs.find(input => input.id === id)!) ); const dateValue = values[0] ? values[0] : '0000-00-00'; @@ -188,21 +232,28 @@ export function getCustomField(): Record { function stringField(type: string, maxNrOfCharacters: number): CustomField { return { - render: (element: Element, instanceElement?: Element) => { - return [ - html` { + // If numOfSGs is -1, then it is a single value, otherwise it is treated as a group of values + return (numOfSGs ? [...Array(numOfSGs)] : [numOfSGs]).map((item, i) => { + return html` - `, - ]; + `; + }); }, - value: (inputs: WizardInputElement[]) => { - return getValue(inputs.find(input => input.id === 'Val')!); + value: (inputs: WizardInputElement[], sGroup: number | null) => { + return getValue( + inputs.find(input => input.id === `Val${sGroup || ''}`)! + ); }, }; } diff --git a/test/integration/editors/IED.test.ts b/test/integration/editors/IED.test.ts index c0f8d1fa4d..a83eb038d8 100644 --- a/test/integration/editors/IED.test.ts +++ b/test/integration/editors/IED.test.ts @@ -12,6 +12,10 @@ import { initializeNsdoc, Nsdoc } from '../../../src/foundation/nsdoc.js'; import { FilterButton } from '../../../src/oscd-filter-button.js'; import IED from '../../../src/editors/IED.js'; +import { LDeviceContainer } from '../../../src/editors/ied/ldevice-container.js'; +import { LNContainer } from '../../../src/editors/ied/ln-container.js'; +import { DOContainer } from '../../../src/editors/ied/do-container.js'; +import { DAContainer } from '../../../src/editors/ied/da-container.js'; describe('IED Plugin', () => { if (customElements.get('ied-plugin') === undefined) @@ -176,6 +180,70 @@ describe('IED Plugin', () => { expect(getElementPathValue()).to.be.empty; }); + // Add test for create wizard DAI when clicking add icon in DA Container (see issue #1139) + describe('when DA allows for multiple Val', () => { + beforeEach(async () => { + doc = await fetch('/test/testfiles/wizards/settingGroups.scd') + .then(response => response.text()) + .then(str => + new DOMParser().parseFromString(str, 'application/xml') + ); + nsdoc = await initializeNsdoc(); + element = await fixture( + html`` + ); + await element.requestUpdate(); + await element.updateComplete; + }); + it('shows correct wizard when navigating to the DA container that allows for multiple Val and clicking Add', async () => { + const lnContainer: LNContainer = getLDeviceContainerByInst( + getIedContainer(), + 'stage1' + )!.shadowRoot!.querySelectorAll('ln-container')[1] as LNContainer; + + lnContainer.shadowRoot!.querySelector('mwc-icon-button-toggle')!.on = + true; + await lnContainer.requestUpdate(); + + // await new Promise(resolve => setTimeout(resolve, 100)); // await animation + const doContainer: DOContainer = lnContainer + .shadowRoot!.querySelector('action-pane')! + .querySelector('do-container')!; + + doContainer.shadowRoot!.querySelector('mwc-icon-button-toggle')!.on = + true; + await doContainer.requestUpdate(); + + // await new Promise(resolve => setTimeout(resolve, 100)); // await animation + + const daContainer: DAContainer = + doContainer.shadowRoot!.querySelector('da-container')!; + daContainer + .shadowRoot!.querySelector('action-pane')! + .querySelector('mwc-icon-button-toggle')!.on = true; + await daContainer.requestUpdate(); + + // await new Promise(resolve => setTimeout(resolve, 100)); // await animation + + (daContainer + .shadowRoot!.querySelector('da-container')! + .shadowRoot!.querySelector( + 'mwc-icon-button[icon="add"]' + ) as HTMLElement)!.click(); + + await element.updateComplete; + + expect( + (element as any as WizardingElement).wizardUI.dialogs.length + ).to.equal(1); + expect( + ( + element as any as WizardingElement + ).wizardUI.dialogs[0]!.querySelectorAll('wizard-textfield').length + ).to.equal(3); + }); + }); + function getIedContainer(): Element { return element.shadowRoot!.querySelector('ied-container')!; } @@ -187,6 +255,20 @@ describe('IED Plugin', () => { .shadowRoot!.querySelector('ldevice-container')!; } + function getLDeviceContainerByInst( + iedContainer: Element, + instName: string + ): Element | undefined { + return ( + Array.from( + iedContainer! + .shadowRoot!.querySelector('access-point-container')! + .shadowRoot!.querySelector('server-container')! + .shadowRoot!.querySelectorAll('ldevice-container') + ) as LDeviceContainer[] + ).find(lDevice => lDevice.element.getAttribute('inst') === instName); + } + function getElementPathValue(): string { return ( element.shadowRoot diff --git a/test/testfiles/wizards/settingGroups.scd b/test/testfiles/wizards/settingGroups.scd new file mode 100644 index 0000000000..0bb21d742c --- /dev/null +++ b/test/testfiles/wizards/settingGroups.scd @@ -0,0 +1,294 @@ + + +
+ + + 110 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 500 + 600 + 700 + + + + + + 100 + 200 + 300 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + IEC 61850-7-4:2007B4 + + + + + + + + + + + + + + + + + + + sbo-with-enhanced-security + + + 30000 + + + 600 + + + + + + + + s + + + + + + + + + + + + A + + + + + + + + + IEC 61850-8-1:2003 + + + + + + + + + IEC 61850-8-1:2003 + + + + + + + + m + kg + s + A + K + mol + cd + deg + rad + sr + Gy + Bq + °C + Sv + F + C + S + H + V + ohm + J + N + Hz + lx + Lm + Wb + T + W + Pa + + + m/s + m/s² + m³/s + m/m³ + M + kg/m³ + m²/s + W/m K + J/K + ppm + 1/s + rad/s + W/m² + J/m² + S/m + K/s + Pa/s + J/kg K + VA + Watts + VAr + phi + cos(phi) + Vs + + As + + A²t + VAh + Wh + VArh + V/Hz + Hz/s + char + char/s + kgm² + dB + J/Wh + W/s + l/s + dBm + h + min + Ohm/m + percent/s + + + unknown + forward + backward + both + + + Ok + Warning + Alarm + + + status-only + direct-with-normal-security + sbo-with-normal-security + direct-with-enhanced-security + sbo-with-enhanced-security + + + on + blocked + test + test/blocked + off + + + not-supported + bay-control + station-control + remote-control + automatic-bay + automatic-station + automatic-remote + maintenance + process + + + \ No newline at end of file diff --git a/test/unit/wizards/dai.test.ts b/test/unit/wizards/dai.test.ts index 168212d360..8fedd572c4 100644 --- a/test/unit/wizards/dai.test.ts +++ b/test/unit/wizards/dai.test.ts @@ -141,14 +141,14 @@ describe('Wizards for SCL element DAI', () => { let val: Element; beforeEach(async () => { - doc = await fetchDoc('/test/testfiles/wizards/ied.scd'); + doc = await fetchDoc('/test/testfiles/wizards/settingGroups.scd'); dai = doc.querySelector( - ':root > IED[name="IED3"] > AccessPoint[name="P1"] > Server > ' + - 'LDevice[inst="MU01"] > LN[lnType="DummyTCTR"] > DOI[name="Amp"] > SDI[name="sVC"] > DAI[name="scaleFactor"]' + ':root > IED[name="IED1"] > AccessPoint[name="AP1"] > Server > ' + + 'LDevice[inst="stage2"] > LN[lnType="OpenSCD_PTOC"] > DOI[name="StrVal"] > SDI[name="setMag"] > DAI[name="f"]' )!; val = dai.querySelectorAll('Val')[1]; da = doc.querySelector( - 'DAType[id="ScaledValueConfig"] > BDA[name="scaleFactor"]' + 'DAType[id="OpenSCD_AnVal_FLOAT32"] > BDA[name="f"]' )!; element = await fixture(html``); @@ -159,14 +159,14 @@ describe('Wizards for SCL element DAI', () => { }); it('update value should be updated in document', async function () { - await setWizardTextFieldValue(inputs[0], '0.10'); + await setWizardTextFieldValue(inputs[0], '800'); const complexActions = updateValue(da, val)(inputs, element.wizardUI); expectUpdateComplexAction(complexActions); const replace = (complexActions[0]).actions[0]; - expect(replace.old.element.textContent).to.equal('0.005'); - expect(replace.new.element.textContent).to.equal('0.10'); + expect(replace.old.element.textContent).to.equal('600'); + expect(replace.new.element.textContent).to.equal('800'); }); }); }); diff --git a/test/unit/wizards/foundation/__snapshots__/dai-field-type.test.snap.js b/test/unit/wizards/foundation/__snapshots__/dai-field-type.test.snap.js index 89e3556610..125ff1536f 100644 --- a/test/unit/wizards/foundation/__snapshots__/dai-field-type.test.snap.js +++ b/test/unit/wizards/foundation/__snapshots__/dai-field-type.test.snap.js @@ -54,7 +54,7 @@ snapshots["dai-field-type getCustomField ENUM field render function returns the