From 64bf40ab2b8fe2a3b47bf8994d782c4582afe074 Mon Sep 17 00:00:00 2001 From: Jakob Vogelsang Date: Thu, 19 May 2022 19:53:34 +0200 Subject: [PATCH] feat(wizards/eqsubfunction): add create wizard (#757) --- src/editors/substation/eq-function-editor.ts | 65 ++++++++++- .../substation/eq-sub-function-editor.ts | 65 ++++++++++- src/wizards/eqsubfunction.ts | 56 ++++++++++ src/wizards/wizard-library.ts | 4 +- .../eq-function-wizarding-editing.test.ts | 91 +++++++++++++++ ...-function-editor-wizarding-editing.test.ts | 91 +++++++++++++++ .../eq-function-editor.test.snap.js | 70 ++++++++++++ .../eq-sub-function-editor.test.snap.js | 70 ++++++++++++ .../__snapshots__/eqsubfunction.test.snap.js | 52 +++++++++ test/unit/wizards/eqsubfunction.test.ts | 104 ++++++++++++++++++ 10 files changed, 664 insertions(+), 4 deletions(-) create mode 100644 src/wizards/eqsubfunction.ts create mode 100644 test/integration/editors/substation/eq-function-wizarding-editing.test.ts create mode 100644 test/integration/editors/substation/eq-sub-function-editor-wizarding-editing.test.ts create mode 100644 test/unit/wizards/__snapshots__/eqsubfunction.test.snap.js create mode 100644 test/unit/wizards/eqsubfunction.test.ts diff --git a/src/editors/substation/eq-function-editor.ts b/src/editors/substation/eq-function-editor.ts index 204a7d4a3b..f3807ed962 100644 --- a/src/editors/substation/eq-function-editor.ts +++ b/src/editors/substation/eq-function-editor.ts @@ -5,11 +5,34 @@ import { property, customElement, state, + query, } from 'lit-element'; +import { translate } from 'lit-translate'; + +import '@material/mwc-icon-button'; +import '@material/mwc-list/mwc-list-item'; +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-pane.js'; import './eq-sub-function-editor.js'; -import { getChildElementsByTagName } from '../../foundation.js'; +import { + getChildElementsByTagName, + newWizardEvent, + SCLTag, + tags, +} from '../../foundation.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 + ); +} /** Pane rendering `EqFunction` element with its children */ @customElement('eq-function-editor') @@ -26,6 +49,19 @@ export class EqFunctionEditor extends LitElement { return `${name}${desc ? ` - ${desc}` : ''}${type ? ` (${type})` : ''}`; } + @query('mwc-menu') addMenu!: Menu; + @query('mwc-icon-button[icon="playlist_add"]') addButton!: IconButton; + + private openCreateWizard(tagName: string): void { + const wizard = wizards[tagName].create(this.element!); + + if (wizard) this.dispatchEvent(newWizardEvent(wizard)); + } + + firstUpdated(): void { + this.addMenu.anchor = this.addButton; + } + private renderEqSubFunctions(): TemplateResult { const eqSubFunctions = getChildElementsByTagName( this.element, @@ -39,12 +75,39 @@ export class EqFunctionEditor extends LitElement { )}`; } + private renderAddButtons(): TemplateResult[] { + return childTags(this.element).map( + child => + html`${child}` + ); + } + render(): TemplateResult { return html` + (this.addMenu.open = true)} + > { + const tagName = ((e.target).selected).value; + this.openCreateWizard(tagName); + }} + >${this.renderAddButtons()}${this.renderEqSubFunctions()}`; } diff --git a/src/editors/substation/eq-sub-function-editor.ts b/src/editors/substation/eq-sub-function-editor.ts index 4981a12260..a80108baa7 100644 --- a/src/editors/substation/eq-sub-function-editor.ts +++ b/src/editors/substation/eq-sub-function-editor.ts @@ -5,11 +5,34 @@ import { property, customElement, state, + query, } from 'lit-element'; +import { translate } from 'lit-translate'; + +import '@material/mwc-icon-button'; +import '@material/mwc-list/mwc-list-item'; +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-pane.js'; -import { getChildElementsByTagName } from '../../foundation.js'; +import { + getChildElementsByTagName, + newWizardEvent, + SCLTag, + tags, +} from '../../foundation.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 + ); +} /** Pane rendering `EqSubFunction` element with its children */ @customElement('eq-sub-function-editor') export class EqSubFunctionEditor extends LitElement { @@ -25,6 +48,19 @@ export class EqSubFunctionEditor extends LitElement { return `${name}${desc ? ` - ${desc}` : ''}${type ? ` (${type})` : ''}`; } + @query('mwc-menu') addMenu!: Menu; + @query('mwc-icon-button[icon="playlist_add"]') addButton!: IconButton; + + private openCreateWizard(tagName: string): void { + const wizard = wizards[tagName].create(this.element!); + + if (wizard) this.dispatchEvent(newWizardEvent(wizard)); + } + + firstUpdated(): void { + this.addMenu.anchor = this.addButton; + } + private renderEqSubFunctions(): TemplateResult { const eqSubFunctions = getChildElementsByTagName( this.element, @@ -38,8 +74,35 @@ export class EqSubFunctionEditor extends LitElement { )}`; } + private renderAddButtons(): TemplateResult[] { + return childTags(this.element).map( + child => + html`${child}` + ); + } + render(): TemplateResult { return html` + (this.addMenu.open = true)} + > { + const tagName = ((e.target).selected).value; + this.openCreateWizard(tagName); + }} + >${this.renderAddButtons()}${this.renderEqSubFunctions()}`; } diff --git a/src/wizards/eqsubfunction.ts b/src/wizards/eqsubfunction.ts new file mode 100644 index 0000000000..4763fdb1c1 --- /dev/null +++ b/src/wizards/eqsubfunction.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 createEqSubFunctionAction(parent: Element): WizardActor { + return (inputs: WizardInputElement[]) => { + const eqSubFunctionAttrs: Record = {}; + const eqSubFunctionKeys = ['name', 'desc', 'type']; + eqSubFunctionKeys.forEach(key => { + eqSubFunctionAttrs[key] = getValue(inputs.find(i => i.label === key)!); + }); + + const eqSubFunction = createElement( + parent.ownerDocument, + 'EqSubFunction', + eqSubFunctionAttrs + ); + + return [{ new: { parent, element: eqSubFunction } }]; + }; +} + +export function createEqSubFunctionWizard(parent: Element): Wizard { + const name = ''; + const desc = null; + const type = null; + const reservedNames = Array.from( + parent.querySelectorAll('EqSubFunction') + ).map(eqSubFunction => eqSubFunction.getAttribute('name')!); + + return [ + { + title: get('wizard.title.add', { tagName: 'EqSubFunction' }), + primary: { + icon: 'save', + label: get('save'), + action: createEqSubFunctionAction(parent), + }, + content: [ + ...contentFunctionWizard({ + name, + desc, + type, + reservedNames, + }), + ], + }, + ]; +} diff --git a/src/wizards/wizard-library.ts b/src/wizards/wizard-library.ts index 6a31874a23..dd53ee9e58 100644 --- a/src/wizards/wizard-library.ts +++ b/src/wizards/wizard-library.ts @@ -25,10 +25,10 @@ import { editTrgOpsWizard } from './trgops.js'; import { createDaWizard } from './da.js'; import { editDAIWizard } from './dai.js'; import { createFunctionWizard } from './function.js'; +import { createEqSubFunctionWizard } from './eqsubfunction.js'; import { createEqFunctionWizard } from './eqfunction.js'; import { createSubFunctionWizard } from './subfunction.js'; - type SclElementWizard = ( element: Element, instanceElement?: Element @@ -195,7 +195,7 @@ export const wizards: Record< }, EqSubFunction: { edit: emptyWizard, - create: emptyWizard, + create: createEqSubFunctionWizard, }, ExtRef: { edit: emptyWizard, diff --git a/test/integration/editors/substation/eq-function-wizarding-editing.test.ts b/test/integration/editors/substation/eq-function-wizarding-editing.test.ts new file mode 100644 index 0000000000..eb29d5bba4 --- /dev/null +++ b/test/integration/editors/substation/eq-function-wizarding-editing.test.ts @@ -0,0 +1,91 @@ +import { fixture, html, expect } from '@open-wc/testing'; + +import '../../../mock-wizard-editor.js'; +import { MockWizardEditor } from '../../../mock-wizard-editor.js'; + +import '../../../../src/editors/substation/eq-function-editor.js'; +import { EqFunctionEditor } from '../../../../src/editors/substation/eq-function-editor.js'; +import { WizardTextField } from '../../../../src/wizard-textfield.js'; + +describe('eq-function-editor wizarding editing integration', () => { + describe('open create wizard for element EqSubFunction', () => { + let doc: XMLDocument; + let parent: MockWizardEditor; + let element: EqFunctionEditor | 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` EqFunction' + )} + >` + ) + ); + + element = parent.querySelector('eq-function-editor'); + + (( + element?.shadowRoot?.querySelector( + 'mwc-list-item[value="EqSubFunction"]' + ) + )).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 EqSubFunction if name attribute is not unique', async () => { + expect( + doc.querySelector( + 'ConductingEquipment[name="QA1"] EqSubFunction[name="myEqSubFunc"]' + ) + ).to.exist; + + nameField.value = 'myEqSubFunc'; + primaryAction.click(); + await parent.updateComplete; + + expect( + doc.querySelectorAll( + 'ConductingEquipment[name="QA1"] EqSubFunction[name="myEqSubFunc"]' + ).length + ).to.equal(1); + }); + + it('does add EqFunction if name attribute is unique', async () => { + expect( + doc.querySelector( + 'ConductingEquipment[name="QA1"] EqSubFunction[name="someNewEqSubFunction"]' + ) + ).to.not.exist; + + nameField.value = 'someNewEqSubFunction'; + await parent.updateComplete; + primaryAction.click(); + + expect( + doc.querySelector( + 'ConductingEquipment[name="QA1"] EqSubFunction[name="someNewEqSubFunction"]' + ) + ).to.exist; + }); + }); +}); diff --git a/test/integration/editors/substation/eq-sub-function-editor-wizarding-editing.test.ts b/test/integration/editors/substation/eq-sub-function-editor-wizarding-editing.test.ts new file mode 100644 index 0000000000..f4b12ce3f4 --- /dev/null +++ b/test/integration/editors/substation/eq-sub-function-editor-wizarding-editing.test.ts @@ -0,0 +1,91 @@ +import { fixture, html, expect } from '@open-wc/testing'; + +import '../../../mock-wizard-editor.js'; +import { MockWizardEditor } from '../../../mock-wizard-editor.js'; + +import '../../../../src/editors/substation/eq-sub-function-editor.js'; +import { EqSubFunctionEditor } from '../../../../src/editors/substation/eq-sub-function-editor.js'; +import { WizardTextField } from '../../../../src/wizard-textfield.js'; + +describe('eq-sub-function-editor wizarding editing integration', () => { + describe('open create wizard for element EqSubFunction', () => { + let doc: XMLDocument; + let parent: MockWizardEditor; + let element: EqSubFunctionEditor | 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('eq-sub-function-editor'); + + (( + element?.shadowRoot?.querySelector( + 'mwc-list-item[value="EqSubFunction"]' + ) + )).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 EqSubFunction if name attribute is not unique', async () => { + expect( + doc.querySelector( + 'ConductingEquipment[name="QA1"] EqSubFunction[name="myEqSubSubFunction"]' + ) + ).to.exist; + + nameField.value = 'myEqSubSubFunction'; + primaryAction.click(); + await parent.updateComplete; + + expect( + doc.querySelectorAll( + 'ConductingEquipment[name="QA1"] EqSubFunction[name="myEqSubSubFunction"]' + ).length + ).to.equal(1); + }); + + it('does add EqFunction if name attribute is unique', async () => { + expect( + doc.querySelector( + 'ConductingEquipment[name="QA1"] EqSubFunction[name="someNewEqSubFunction"]' + ) + ).to.not.exist; + + nameField.value = 'someNewEqSubFunction'; + await parent.updateComplete; + primaryAction.click(); + + expect( + doc.querySelector( + 'ConductingEquipment[name="QA1"] EqSubFunction[name="someNewEqSubFunction"]' + ) + ).to.exist; + }); + }); +}); diff --git a/test/unit/editors/substation/__snapshots__/eq-function-editor.test.snap.js b/test/unit/editors/substation/__snapshots__/eq-function-editor.test.snap.js index da906462c9..86aba8c6d6 100644 --- a/test/unit/editors/substation/__snapshots__/eq-function-editor.test.snap.js +++ b/test/unit/editors/substation/__snapshots__/eq-function-editor.test.snap.js @@ -9,6 +9,41 @@ snapshots["web component rendering EqFunction element with complete attribute se secondary="" tabindex="0" > + + + + + + + LNode + + + + + EqSubFunction + + + + @@ -23,6 +58,41 @@ snapshots["web component rendering EqFunction element with missing desc and type secondary="" tabindex="0" > + + + + + + + LNode + + + + + EqSubFunction + + + + diff --git a/test/unit/editors/substation/__snapshots__/eq-sub-function-editor.test.snap.js b/test/unit/editors/substation/__snapshots__/eq-sub-function-editor.test.snap.js index 40b9949528..9038e7b37a 100644 --- a/test/unit/editors/substation/__snapshots__/eq-sub-function-editor.test.snap.js +++ b/test/unit/editors/substation/__snapshots__/eq-sub-function-editor.test.snap.js @@ -8,6 +8,41 @@ snapshots["web component rendering EqSubFunction element with complete attribute secondary="" tabindex="0" > + + + + + + + LNode + + + + + EqSubFunction + + + + `; /* end snapshot web component rendering EqSubFunction element with complete attribute set and existing children looks like the latest snapshot */ @@ -19,6 +54,41 @@ snapshots["web component rendering EqSubFunction element with missing desc and t secondary="" tabindex="0" > + + + + + + + LNode + + + + + EqSubFunction + + + + diff --git a/test/unit/wizards/__snapshots__/eqsubfunction.test.snap.js b/test/unit/wizards/__snapshots__/eqsubfunction.test.snap.js new file mode 100644 index 0000000000..4453caaea6 --- /dev/null +++ b/test/unit/wizards/__snapshots__/eqsubfunction.test.snap.js @@ -0,0 +1,52 @@ +/* @web/test-runner snapshot v1 */ +export const snapshots = {}; + +snapshots["Wizards for SCL EqSubFunction element define an create wizard that looks like the the latest snapshot"] = +` +
+ + + + + + +
+ + + + +
+`; +/* end snapshot Wizards for SCL EqSubFunction element define an create wizard that looks like the the latest snapshot */ + diff --git a/test/unit/wizards/eqsubfunction.test.ts b/test/unit/wizards/eqsubfunction.test.ts new file mode 100644 index 0000000000..2419558b27 --- /dev/null +++ b/test/unit/wizards/eqsubfunction.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 { createEqSubFunctionWizard } from '../../../src/wizards/eqsubfunction.js'; + +describe('Wizards for SCL EqSubFunction 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 = createEqSubFunctionWizard( + doc.querySelector('EqFunction')! + ); + 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'); + }); + }); +});