From a2f1d161200960332e5f52cadb440bcb5aeaec8f Mon Sep 17 00:00:00 2001 From: Jakob Vogelsang Date: Wed, 11 May 2022 17:04:48 +0200 Subject: [PATCH] feat(wizards/eqfunction): add create function --- .../substation/conducting-equipment-editor.ts | 62 ++++++++++- .../substation/powertransformer-editor.ts | 62 ++++++++++- src/wizards/eqfunction.ts | 56 ++++++++++ src/wizards/function.ts | 4 +- src/wizards/wizard-library.ts | 3 +- ...equipment-editor-wizarding-editing.test.ts | 78 +++++++++++++ ...ansformer-editor-wizarding-editing.test.ts | 88 +++++++++++++++ .../conducting-equipment-editor.test.snap.js | 70 ++++++++++++ .../powertransformer-editor.test.snap.js | 70 ++++++++++++ .../__snapshots__/eqfunction.test.snap.js | 52 +++++++++ test/unit/wizards/eqfunction.test.ts | 104 ++++++++++++++++++ 11 files changed, 641 insertions(+), 8 deletions(-) create mode 100644 src/wizards/eqfunction.ts create mode 100644 test/integration/editors/substation/powertransformer-editor-wizarding-editing.test.ts create mode 100644 test/unit/wizards/__snapshots__/eqfunction.test.snap.js create mode 100644 test/unit/wizards/eqfunction.test.ts diff --git a/src/editors/substation/conducting-equipment-editor.ts b/src/editors/substation/conducting-equipment-editor.ts index 19629c4e9..e8f345542 100644 --- a/src/editors/substation/conducting-equipment-editor.ts +++ b/src/editors/substation/conducting-equipment-editor.ts @@ -4,6 +4,7 @@ import { html, LitElement, property, + query, TemplateResult, } from 'lit-element'; import { translate } from 'lit-translate'; @@ -11,6 +12,10 @@ import { translate } from 'lit-translate'; import '@material/mwc-fab'; import '@material/mwc-icon'; import '@material/mwc-icon-button'; +import '@material/mwc-menu'; +import { Menu } from '@material/mwc-menu'; +import { IconButton } from '@material/mwc-icon-button'; +import { ListItem } from '@material/mwc-list/mwc-list-item'; import '../../action-icon.js'; import '../../action-pane.js'; @@ -20,9 +25,19 @@ import { getChildElementsByTagName, newActionEvent, newWizardEvent, + SCLTag, + tags, } from '../../foundation.js'; import { BayEditor } from './bay-editor.js'; -import { wizards } from '../../wizards/wizard-library.js'; +import { emptyWizard, wizards } from '../../wizards/wizard-library.js'; + +function childTags(element: Element | null | undefined): SCLTag[] { + if (!element) return []; + + return tags[element.tagName].children.filter( + child => wizards[child].create !== emptyWizard + ); +} /** [[`SubstationEditor`]] subeditor for a `ConductingEquipment` element. */ @customElement('conducting-equipment-editor') @@ -39,6 +54,9 @@ export class ConductingEquipmentEditor extends LitElement { @property({ type: Boolean }) showfunctions = false; + @query('mwc-menu') addMenu!: Menu; + @query('mwc-icon-button[icon="playlist_add"]') addButton!: IconButton; + private openEditWizard(): void { const wizard = wizards['ConductingEquipment'].edit(this.element); if (wizard) this.dispatchEvent(newWizardEvent(wizard)); @@ -49,6 +67,12 @@ export class ConductingEquipmentEditor extends LitElement { if (wizard) this.dispatchEvent(newWizardEvent(wizard)); } + private openCreateWizard(tagName: string): void { + const wizard = wizards[tagName].create(this.element!); + + if (wizard) this.dispatchEvent(newWizardEvent(wizard)); + } + remove(): void { if (this.element) this.dispatchEvent( @@ -62,6 +86,11 @@ export class ConductingEquipmentEditor extends LitElement { ); } + firstUpdated(): void { + if (this.addMenu && this.addButton) + this.addMenu.anchor = this.addButton; + } + renderEqFunctions(): TemplateResult { if (!this.showfunctions) return html``; @@ -72,6 +101,15 @@ export class ConductingEquipmentEditor extends LitElement { )}`; } + private renderAddButtons(): TemplateResult[] { + return childTags(this.element).map( + child => + html`${child}` + ); + } + renderContentPane(): TemplateResult { return html`${getIcon(this.element)} - `; + > + (this.addMenu.open = true)} + > { + const tagName = ((e.target).selected).value; + this.openCreateWizard(tagName); + }} + >${this.renderAddButtons()} + `; } renderContentIcon(): TemplateResult { diff --git a/src/editors/substation/powertransformer-editor.ts b/src/editors/substation/powertransformer-editor.ts index 6f6b97700..685ce9acd 100644 --- a/src/editors/substation/powertransformer-editor.ts +++ b/src/editors/substation/powertransformer-editor.ts @@ -4,6 +4,7 @@ import { html, LitElement, property, + query, TemplateResult, } from 'lit-element'; import { translate } from 'lit-translate'; @@ -11,21 +12,35 @@ import { translate } from 'lit-translate'; import '@material/mwc-fab'; import '@material/mwc-icon'; import '@material/mwc-icon-button'; +import '@material/mwc-menu'; +import { IconButton } from '@material/mwc-icon-button'; +import { ListItem } from '@material/mwc-list/mwc-list-item'; +import { Menu } from '@material/mwc-menu'; import '../../action-icon.js'; import '../../action-pane.js'; import { powerTransformerTwoWindingIcon } from '../../icons/icons.js'; -import { wizards } from '../../wizards/wizard-library.js'; +import { emptyWizard, wizards } from '../../wizards/wizard-library.js'; import { getChildElementsByTagName, newActionEvent, newWizardEvent, + SCLTag, + tags, } from '../../foundation.js'; import { startMove } from './foundation.js'; import { SubstationEditor } from './substation-editor.js'; import { BayEditor } from './bay-editor.js'; import { VoltageLevelEditor } from './voltage-level-editor.js'; +function childTags(element: Element | null | undefined): SCLTag[] { + if (!element) return []; + + return tags[element.tagName].children.filter( + child => wizards[child].create !== emptyWizard + ); +} + /** [[`SubstationEditor`]] subeditor for a child-less `PowerTransformer` element. */ @customElement('powertransformer-editor') export class PowerTransformerEditor extends LitElement { @@ -42,6 +57,9 @@ export class PowerTransformerEditor extends LitElement { @property({ type: Boolean }) showfunctions = false; + @query('mwc-menu') addMenu!: Menu; + @query('mwc-icon-button[icon="playlist_add"]') addButton!: IconButton; + private openEditWizard(): void { const wizard = wizards['PowerTransformer'].edit(this.element); if (wizard) this.dispatchEvent(newWizardEvent(wizard)); @@ -65,6 +83,17 @@ export class PowerTransformerEditor extends LitElement { ); } + private openCreateWizard(tagName: string): void { + const wizard = wizards[tagName].create(this.element!); + + if (wizard) this.dispatchEvent(newWizardEvent(wizard)); + } + + firstUpdated(): void { + if (this.addMenu && this.addButton) + this.addMenu.anchor = this.addButton; + } + renderEqFunctions(): TemplateResult { if (!this.showfunctions) return html``; @@ -75,6 +104,15 @@ export class PowerTransformerEditor extends LitElement { )}`; } + private renderAddButtons(): TemplateResult[] { + return childTags(this.element).map( + child => + html`${child}` + ); + } + renderContentPane(): TemplateResult { return html`${powerTransformerTwoWindingIcon} - `; + > + (this.addMenu.open = true)} + > { + const tagName = ((e.target).selected).value; + this.openCreateWizard(tagName); + }} + >${this.renderAddButtons()} + `; } renderContentIcon(): TemplateResult { diff --git a/src/wizards/eqfunction.ts b/src/wizards/eqfunction.ts new file mode 100644 index 000000000..62b5a71ef --- /dev/null +++ b/src/wizards/eqfunction.ts @@ -0,0 +1,56 @@ +import { get } from 'lit-translate'; + +import { + createElement, + getValue, + Wizard, + WizardActor, + WizardInputElement, +} from '../foundation.js'; +import { contentFunctionWizard } from './function.js'; + +function createEqFunctionAction(parent: Element): WizardActor { + return (inputs: WizardInputElement[]) => { + const eqFunctionAttrs: Record = {}; + const eqFunctionKeys = ['name', 'desc', 'type']; + eqFunctionKeys.forEach(key => { + eqFunctionAttrs[key] = getValue(inputs.find(i => i.label === key)!); + }); + + const eqFunction = createElement( + parent.ownerDocument, + 'EqFunction', + eqFunctionAttrs + ); + + return [{ new: { parent, element: eqFunction } }]; + }; +} + +export function createEqFunctionWizard(parent: Element): Wizard { + const name = ''; + const desc = null; + const type = null; + const reservedNames = Array.from(parent.querySelectorAll('EqFunction')).map( + fUnction => fUnction.getAttribute('name')! + ); + + return [ + { + title: get('wizard.title.add', { tagName: 'EqFunction' }), + primary: { + icon: 'save', + label: get('save'), + action: createEqFunctionAction(parent), + }, + content: [ + ...contentFunctionWizard({ + name, + desc, + type, + reservedNames, + }), + ], + }, + ]; +} diff --git a/src/wizards/function.ts b/src/wizards/function.ts index e1115e358..a828a8e75 100644 --- a/src/wizards/function.ts +++ b/src/wizards/function.ts @@ -16,7 +16,9 @@ interface ContentOptions { reservedNames: string[]; } -function contentFunctionWizard(content: ContentOptions): TemplateResult[] { +export function contentFunctionWizard( + content: ContentOptions +): TemplateResult[] { return [ html` { expect(doc.querySelector('ConductingEquipment[name="QA1"]')).to.not.exist; }); }); + + describe('open create wizard for element EqFunction', () => { + let doc: XMLDocument; + let parent: MockWizardEditor; + let element: ConductingEquipmentEditor | null; + + let nameField: WizardTextField; + let primaryAction: HTMLElement; + + beforeEach(async () => { + doc = await fetch('/test/testfiles/zeroline/functions.scd') + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); + parent = ( + await fixture( + html`` + ) + ); + + element = parent.querySelector('conducting-equipment-editor'); + + (( + element?.shadowRoot?.querySelector('mwc-list-item[value="EqFunction"]') + )).click(); + await parent.updateComplete; + + nameField = ( + parent.wizardUI.dialog?.querySelector('wizard-textfield[label="name"]') + ); + + primaryAction = ( + parent.wizardUI.dialog?.querySelector( + 'mwc-button[slot="primaryAction"]' + ) + ); + }); + + it('does not add EqFunction if name attribute is not unique', async () => { + expect( + doc.querySelector( + 'ConductingEquipment > EqFunction[name="myEqFuncQA1"]' + ) + ).to.exist; + + nameField.value = 'myEqFuncQA1'; + primaryAction.click(); + await parent.updateComplete; + + expect( + doc.querySelectorAll( + 'ConductingEquipment > EqFunction[name="myEqFuncQA1"]' + ).length + ).to.equal(1); + }); + + it('does add EqFunction if name attribute is unique', async () => { + expect( + doc.querySelector( + 'ConductingEquipment > EqFunction[name="someNewFunction"]' + ) + ).to.not.exist; + + nameField.value = 'someNewFunction'; + await parent.updateComplete; + primaryAction.click(); + + expect( + doc.querySelector( + 'ConductingEquipment > EqFunction[name="someNewFunction"]' + ) + ).to.exist; + }); + }); }); diff --git a/test/integration/editors/substation/powertransformer-editor-wizarding-editing.test.ts b/test/integration/editors/substation/powertransformer-editor-wizarding-editing.test.ts new file mode 100644 index 000000000..4ac0fa7a1 --- /dev/null +++ b/test/integration/editors/substation/powertransformer-editor-wizarding-editing.test.ts @@ -0,0 +1,88 @@ +import { fixture, html, expect } from '@open-wc/testing'; + +import '../../../mock-wizard-editor.js'; +import { MockWizardEditor } from '../../../mock-wizard-editor.js'; + +import '../../../../src/editors/substation/powertransformer-editor.js'; +import { PowerTransformerEditor } from '../../../../src/editors/substation/powertransformer-editor.js'; +import { WizardTextField } from '../../../../src/wizard-textfield.js'; + +describe('powertransformer-editor wizarding editing integration', () => { + describe('open create wizard for element EqFunction', () => { + let doc: XMLDocument; + let parent: MockWizardEditor; + let element: PowerTransformerEditor | null; + + let nameField: WizardTextField; + let primaryAction: HTMLElement; + + beforeEach(async () => { + doc = await fetch('/test/testfiles/zeroline/functions.scd') + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); + parent = ( + await fixture( + html`` + ) + ); + + element = parent.querySelector('powertransformer-editor'); + + (( + element?.shadowRoot?.querySelector('mwc-list-item[value="EqFunction"]') + )).click(); + await parent.updateComplete; + + nameField = ( + parent.wizardUI.dialog?.querySelector('wizard-textfield[label="name"]') + ); + + primaryAction = ( + parent.wizardUI.dialog?.querySelector( + 'mwc-button[slot="primaryAction"]' + ) + ); + }); + + it('does not add EqFunction if name attribute is not unique', async () => { + expect( + doc.querySelector( + 'PowerTransformer[name="myPtr2"] > EqFunction[name="myEqFuncPtr2"]' + ) + ).to.exist; + + nameField.value = 'myEqFuncPtr2'; + primaryAction.click(); + await parent.updateComplete; + + expect( + doc.querySelectorAll( + 'PowerTransformer[name="myPtr2"] > EqFunction[name="myEqFuncPtr2"]' + ).length + ).to.equal(1); + }); + + it('does add EqFunction if name attribute is unique', async () => { + expect( + doc.querySelector( + 'PowerTransformer[name="myPtr2"] > EqFunction[name="someNewFunction"]' + ) + ).to.not.exist; + + nameField.value = 'someNewFunction'; + await parent.updateComplete; + primaryAction.click(); + + expect( + doc.querySelector( + 'PowerTransformer[name="myPtr2"] > EqFunction[name="someNewFunction"]' + ) + ).to.exist; + }); + }); +}); diff --git a/test/unit/editors/substation/__snapshots__/conducting-equipment-editor.test.snap.js b/test/unit/editors/substation/__snapshots__/conducting-equipment-editor.test.snap.js index 8e145f8b9..e2dd5f1ae 100644 --- a/test/unit/editors/substation/__snapshots__/conducting-equipment-editor.test.snap.js +++ b/test/unit/editors/substation/__snapshots__/conducting-equipment-editor.test.snap.js @@ -90,6 +90,41 @@ snapshots["conducting-equipment-editor rendered as action pane looks like the la > + + + + + + + LNode + + + + + EqFunction + + + + `; /* end snapshot conducting-equipment-editor rendered as action pane looks like the latest snapshot */ @@ -148,6 +183,41 @@ snapshots["conducting-equipment-editor rendered as action pane with EqFunction c > + + + + + + + LNode + + + + + EqFunction + + + + diff --git a/test/unit/editors/substation/__snapshots__/powertransformer-editor.test.snap.js b/test/unit/editors/substation/__snapshots__/powertransformer-editor.test.snap.js index bb06c2da0..7cecef609 100644 --- a/test/unit/editors/substation/__snapshots__/powertransformer-editor.test.snap.js +++ b/test/unit/editors/substation/__snapshots__/powertransformer-editor.test.snap.js @@ -91,6 +91,41 @@ snapshots["powertransformer-editor rendered as action pane looks like the latest > + + + + + + + LNode + + + + + EqFunction + + + + `; /* end snapshot powertransformer-editor rendered as action pane looks like the latest snapshot */ @@ -149,6 +184,41 @@ snapshots["powertransformer-editor rendered as action pane with EqFunction child > + + + + + + + LNode + + + + + EqFunction + + + + diff --git a/test/unit/wizards/__snapshots__/eqfunction.test.snap.js b/test/unit/wizards/__snapshots__/eqfunction.test.snap.js new file mode 100644 index 000000000..fc5b64d35 --- /dev/null +++ b/test/unit/wizards/__snapshots__/eqfunction.test.snap.js @@ -0,0 +1,52 @@ +/* @web/test-runner snapshot v1 */ +export const snapshots = {}; + +snapshots["Wizards for SCL EqFunction element define an create wizard that looks like the the latest snapshot"] = +` +
+ + + + + + +
+ + + + +
+`; +/* end snapshot Wizards for SCL EqFunction element define an create wizard that looks like the the latest snapshot */ + diff --git a/test/unit/wizards/eqfunction.test.ts b/test/unit/wizards/eqfunction.test.ts new file mode 100644 index 000000000..7d1a6fa1a --- /dev/null +++ b/test/unit/wizards/eqfunction.test.ts @@ -0,0 +1,104 @@ +import { expect, fixture, html } from '@open-wc/testing'; +import { SinonSpy, spy } from 'sinon'; + +import '../../mock-wizard.js'; +import { MockWizard } from '../../mock-wizard.js'; + +import { WizardTextField } from '../../../src/wizard-textfield.js'; +import { + Create, + isCreate, + WizardInputElement, +} from '../../../src/foundation.js'; +import { createEqFunctionWizard } from '../../../src/wizards/eqfunction.js'; + +describe('Wizards for SCL EqFunction element', () => { + let doc: XMLDocument; + let element: MockWizard; + let inputs: WizardInputElement[]; + + let primaryAction: HTMLElement; + + let actionEvent: SinonSpy; + + beforeEach(async () => { + element = await fixture(html``); + doc = await fetch('test/testfiles/zeroline/functions.scd') + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); + + actionEvent = spy(); + window.addEventListener('editor-action', actionEvent); + }); + + describe('define an create wizard that', () => { + beforeEach(async () => { + const wizard = createEqFunctionWizard( + doc.querySelector('ConductingEquipment')! + ); + element.workflow.push(() => wizard); + await element.requestUpdate(); + + inputs = Array.from(element.wizardUI.inputs); + + primaryAction = ( + element.wizardUI.dialog?.querySelector( + 'mwc-button[slot="primaryAction"]' + ) + ); + + await element.wizardUI.requestUpdate(); // make sure wizard is rendered + }); + + it('looks like the the latest snapshot', async () => + expect(element.wizardUI.dialog).dom.to.equalSnapshot()); + + it('does not accept empty name attribute', async () => { + await primaryAction.click(); + + expect(actionEvent).to.not.have.been.called; + }); + + it('triggers simple create action on primary action click', async () => { + inputs[0].value = 'someNonEmptyName'; + await element.requestUpdate(); + await primaryAction.click(); + + expect(actionEvent).to.be.calledOnce; + const action = actionEvent.args[0][0].detail.action; + expect(action).to.satisfy(isCreate); + const createAction = action; + + expect(createAction.new.element).to.have.attribute( + 'name', + 'someNonEmptyName' + ); + expect(createAction.new.element).to.not.have.attribute('desc'); + expect(createAction.new.element).to.not.have.attribute('type'); + }); + + it('allows to create non required attributes desc and type', async () => { + inputs[0].value = 'someNonEmptyName'; + + (inputs[1]).nullSwitch?.click(); + (inputs[2]).nullSwitch?.click(); + inputs[1].value = 'SomeDesc'; + inputs[2].value = 'SomeType'; + + await element.requestUpdate(); + await primaryAction.click(); + + expect(actionEvent).to.be.calledOnce; + const action = actionEvent.args[0][0].detail.action; + expect(action).to.satisfy(isCreate); + const createAction = action; + + expect(createAction.new.element).to.have.attribute( + 'name', + 'someNonEmptyName' + ); + expect(createAction.new.element).to.have.attribute('desc', 'SomeDesc'); + expect(createAction.new.element).to.have.attribute('type', 'SomeType'); + }); + }); +});