From ef2c4f282aeddf235f2988dda5a7b23206acd99b Mon Sep 17 00:00:00 2001 From: Jakob Vogelsang Date: Wed, 11 May 2022 10:25:47 +0200 Subject: [PATCH 1/2] feat(wizards/subfunction): add create wizard --- src/editors/substation/function-editor.ts | 67 +++++++++++- src/wizards/function.ts | 4 +- src/wizards/subfunction.ts | 56 ++++++++++ src/wizards/wizard-library.ts | 3 +- .../substation/function-editor.test.ts | 87 +++++++++++++++ .../function-editor.test.snap.js | 92 ++++++++++++++++ .../__snapshots__/function.test.snap.js | 4 +- .../__snapshots__/subfunction.test.snap.js | 52 +++++++++ test/unit/wizards/function.test.ts | 2 +- test/unit/wizards/subfunction.test.ts | 102 ++++++++++++++++++ 10 files changed, 461 insertions(+), 8 deletions(-) create mode 100644 src/wizards/subfunction.ts create mode 100644 test/integration/editors/substation/function-editor.test.ts create mode 100644 test/unit/wizards/__snapshots__/subfunction.test.snap.js create mode 100644 test/unit/wizards/subfunction.test.ts diff --git a/src/editors/substation/function-editor.ts b/src/editors/substation/function-editor.ts index d83fb8be83..28700856c1 100644 --- a/src/editors/substation/function-editor.ts +++ b/src/editors/substation/function-editor.ts @@ -5,11 +5,30 @@ import { property, customElement, state, + query, } from 'lit-element'; import '../../action-pane.js'; import './sub-function-editor.js'; -import { getChildElementsByTagName } from '../../foundation.js'; +import { + getChildElementsByTagName, + newWizardEvent, + SCLTag, + tags, +} from '../../foundation.js'; +import { translate } from 'lit-translate'; +import { emptyWizard, wizards } from '../../wizards/wizard-library.js'; +import { Menu } from '@material/mwc-menu'; +import { ListItem } from '@material/mwc-list/mwc-list-item'; +import { IconButton } from '@material/mwc-icon-button'; + +function childTags(element: Element | null | undefined): SCLTag[] { + if (!element) return []; + + return tags[element.tagName].children.filter( + child => wizards[child].create !== emptyWizard + ); +} /** Pane rendering `Function` element with its children */ @customElement('function-editor') @@ -26,6 +45,19 @@ export class FunctionEditor 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 renderSubFunctions(): TemplateResult { const subfunctions = getChildElementsByTagName(this.element, 'SubFunction'); return html` ${subfunctions.map( @@ -36,13 +68,42 @@ export class FunctionEditor extends LitElement { )}`; } + private renderAddButtons(): TemplateResult[] { + return childTags(this.element).map( + child => + html`${child}` + ); + } + render(): TemplateResult { return html`${this.renderSubFunctions()}`; + > + + (this.addMenu.open = true)} + > { + const tagName = ((e.target).selected).value; + this.openCreateWizard(tagName); + }} + >${this.renderAddButtons()} + + ${this.renderSubFunctions()} + `; } } diff --git a/src/wizards/function.ts b/src/wizards/function.ts index e1115e358a..a828a8e755 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` { + const subFunctionAttrs: Record = {}; + const subFunctionKeys = ['name', 'desc', 'type']; + subFunctionKeys.forEach(key => { + subFunctionAttrs[key] = getValue(inputs.find(i => i.label === key)!); + }); + + const subFunction = createElement( + parent.ownerDocument, + 'SubFunction', + subFunctionAttrs + ); + + return [{ new: { parent, element: subFunction } }]; + }; +} + +export function createSubFunctionWizard(parent: Element): Wizard { + const name = ''; + const desc = null; + const type = null; + const reservedNames = Array.from(parent.querySelectorAll('SubFunction')).map( + fUnction => fUnction.getAttribute('name')! + ); + + return [ + { + title: get('wizard.title.add', { tagName: 'SubFunction' }), + primary: { + icon: 'save', + label: get('save'), + action: createSubFunctionAction(parent), + }, + content: [ + ...contentFunctionWizard({ + name, + desc, + type, + reservedNames, + }), + ], + }, + ]; +} diff --git a/src/wizards/wizard-library.ts b/src/wizards/wizard-library.ts index d628e62141..a4a79f7cd0 100644 --- a/src/wizards/wizard-library.ts +++ b/src/wizards/wizard-library.ts @@ -25,6 +25,7 @@ import { editTrgOpsWizard } from './trgops.js'; import { createDaWizard } from './da.js'; import { editDAIWizard } from './dai.js'; import { createFunctionWizard } from './function.js'; +import { createSubFunctionWizard } from './subfunction.js'; type SclElementWizard = ( element: Element, @@ -476,7 +477,7 @@ export const wizards: Record< }, SubFunction: { edit: emptyWizard, - create: emptyWizard, + create: createSubFunctionWizard, }, SubNetwork: { edit: editSubNetworkWizard, diff --git a/test/integration/editors/substation/function-editor.test.ts b/test/integration/editors/substation/function-editor.test.ts new file mode 100644 index 0000000000..f5bef7542f --- /dev/null +++ b/test/integration/editors/substation/function-editor.test.ts @@ -0,0 +1,87 @@ +import { fixture, html, expect } from '@open-wc/testing'; + +import '../../../mock-wizard-editor.js'; +import { MockWizardEditor } from '../../../mock-wizard-editor.js'; + +import '../../../../src/editors/substation/function-editor.js'; +import { FunctionEditor } from '../../../../src/editors/substation/function-editor.js'; +import { WizardTextField } from '../../../../src/wizard-textfield.js'; + +describe('function-editor wizarding editing integration', () => { + describe('open create wizard for element SubFunction', () => { + let doc: XMLDocument; + let parent: MockWizardEditor; + let element: FunctionEditor | 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('function-editor'); + + (( + element?.shadowRoot?.querySelector('mwc-list-item[value="SubFunction"]') + )).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 SubFunction if name attribute is not unique', async () => { + expect( + doc.querySelector( + 'Substation > Function > SubFunction[name="mySubFunc"]' + ) + ).to.exist; + + nameField.value = 'mySubFunc'; + primaryAction.click(); + await parent.updateComplete; + + expect( + doc.querySelectorAll( + 'Substation > Function > SubFunction[name="mySubFunc"]' + ).length + ).to.equal(1); + }); + + it('does add SubFunction if name attribute is unique', async () => { + expect( + doc.querySelector( + 'Substation > Function > SubFunction[name="someNewFunction"]' + ) + ).to.not.exist; + + nameField.value = 'someNewFunction'; + await parent.updateComplete; + primaryAction.click(); + + expect( + doc.querySelector( + 'Substation > Function > SubFunction[name="someNewFunction"]' + ) + ).to.exist; + }); + }); +}); diff --git a/test/unit/editors/substation/__snapshots__/function-editor.test.snap.js b/test/unit/editors/substation/__snapshots__/function-editor.test.snap.js index f1eea4690c..39aa698e90 100644 --- a/test/unit/editors/substation/__snapshots__/function-editor.test.snap.js +++ b/test/unit/editors/substation/__snapshots__/function-editor.test.snap.js @@ -9,6 +9,52 @@ snapshots["web component rendering Function element with complete attribute set secondary="" tabindex="0" > + + + + + + + LNode + + + + + SubFunction + + + + + ConductingEquipment + + + + @@ -23,6 +69,52 @@ snapshots["web component rendering Function element with missing desc and type a secondary="" tabindex="0" > + + + + + + + LNode + + + + + SubFunction + + + + + ConductingEquipment + + + + diff --git a/test/unit/wizards/__snapshots__/function.test.snap.js b/test/unit/wizards/__snapshots__/function.test.snap.js index 743cff0382..e7e6c00390 100644 --- a/test/unit/wizards/__snapshots__/function.test.snap.js +++ b/test/unit/wizards/__snapshots__/function.test.snap.js @@ -1,7 +1,7 @@ /* @web/test-runner snapshot v1 */ export const snapshots = {}; -snapshots["Wizards for SCL ReportControl element define an create wizard that looks like the the latest snapshot"] = +snapshots["Wizards for SCL Function element define an create wizard that looks like the the latest snapshot"] = ` `; -/* end snapshot Wizards for SCL ReportControl element define an create wizard that looks like the the latest snapshot */ +/* end snapshot Wizards for SCL Function element define an create wizard that looks like the the latest snapshot */ diff --git a/test/unit/wizards/__snapshots__/subfunction.test.snap.js b/test/unit/wizards/__snapshots__/subfunction.test.snap.js new file mode 100644 index 0000000000..e7e6c00390 --- /dev/null +++ b/test/unit/wizards/__snapshots__/subfunction.test.snap.js @@ -0,0 +1,52 @@ +/* @web/test-runner snapshot v1 */ +export const snapshots = {}; + +snapshots["Wizards for SCL Function element define an create wizard that looks like the the latest snapshot"] = +` +
+ + + + + + +
+ + + + +
+`; +/* end snapshot Wizards for SCL Function element define an create wizard that looks like the the latest snapshot */ + diff --git a/test/unit/wizards/function.test.ts b/test/unit/wizards/function.test.ts index 6038f55fa5..593a3e0ccb 100644 --- a/test/unit/wizards/function.test.ts +++ b/test/unit/wizards/function.test.ts @@ -12,7 +12,7 @@ import { } from '../../../src/foundation.js'; import { createFunctionWizard } from '../../../src/wizards/function.js'; -describe('Wizards for SCL ReportControl element', () => { +describe('Wizards for SCL Function element', () => { let doc: XMLDocument; let element: MockWizard; let inputs: WizardInputElement[]; diff --git a/test/unit/wizards/subfunction.test.ts b/test/unit/wizards/subfunction.test.ts new file mode 100644 index 0000000000..bcf1109570 --- /dev/null +++ b/test/unit/wizards/subfunction.test.ts @@ -0,0 +1,102 @@ +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 { createSubFunctionWizard } from '../../../src/wizards/subfunction.js'; + +describe('Wizards for SCL Function 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 = createSubFunctionWizard(doc.querySelector('Function')!); + 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'); + }); + }); +}); From c518d890dbb66984968e9c2b23c430df6e3a0505 Mon Sep 17 00:00:00 2001 From: Jakob Vogelsang Date: Tue, 17 May 2022 23:34:48 +0200 Subject: [PATCH 2/2] feat(editors/substation/sub-function-editor): add add button --- src/editors/substation/sub-function-editor.ts | 67 +++++++++++++- .../substation/sub-function-editor.test.ts | 89 ++++++++++++++++++ .../sub-function-editor.test.snap.js | 92 +++++++++++++++++++ 3 files changed, 246 insertions(+), 2 deletions(-) create mode 100644 test/integration/editors/substation/sub-function-editor.test.ts diff --git a/src/editors/substation/sub-function-editor.ts b/src/editors/substation/sub-function-editor.ts index 81f733258a..88878a235e 100644 --- a/src/editors/substation/sub-function-editor.ts +++ b/src/editors/substation/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 './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 `SubFunction` element with its children */ @customElement('sub-function-editor') @@ -26,6 +49,19 @@ export class SubFunctionEditor 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 renderSubFunctions(): TemplateResult { const subfunctions = getChildElementsByTagName(this.element, 'SubFunction'); return html` ${subfunctions.map( @@ -36,8 +72,35 @@ export class SubFunctionEditor 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.renderSubFunctions()}`; } diff --git a/test/integration/editors/substation/sub-function-editor.test.ts b/test/integration/editors/substation/sub-function-editor.test.ts new file mode 100644 index 0000000000..6c7035ad9d --- /dev/null +++ b/test/integration/editors/substation/sub-function-editor.test.ts @@ -0,0 +1,89 @@ +import { fixture, html, expect } from '@open-wc/testing'; + +import '../../../mock-wizard-editor.js'; +import { MockWizardEditor } from '../../../mock-wizard-editor.js'; + +import '../../../../src/editors/substation/sub-function-editor.js'; +import { SubFunctionEditor } from '../../../../src/editors/substation/sub-function-editor.js'; +import { WizardTextField } from '../../../../src/wizard-textfield.js'; + +describe('sub-function-editor wizarding editing integration', () => { + describe('open create wizard for element SubFunction', () => { + let doc: XMLDocument; + let parent: MockWizardEditor; + let element: SubFunctionEditor | 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` SubFunction' + )} + >` + ) + ); + + element = parent.querySelector('sub-function-editor'); + + (( + element?.shadowRoot?.querySelector('mwc-list-item[value="SubFunction"]') + )).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 SubFunction if name attribute is not unique', async () => { + expect( + doc.querySelector( + 'Function[name="voltLvName"] > SubFunction > SubFunction' + ) + ).to.exist; + + nameField.value = 'mySubSubFunction'; + primaryAction.click(); + await parent.updateComplete; + + expect( + doc.querySelectorAll( + 'Function[name="voltLvName"] > SubFunction > SubFunction' + ).length + ).to.equal(1); + }); + + it('does add SubFunction if name attribute is unique', async () => { + expect( + doc.querySelector( + 'Function[name="voltLvName"] > SubFunction > SubFunction[name="someNewSubFunction"]' + ) + ).to.not.exist; + + nameField.value = 'someNewSubFunction'; + await parent.updateComplete; + primaryAction.click(); + + expect( + doc.querySelector( + 'Function[name="voltLvName"] > SubFunction > SubFunction[name="someNewSubFunction"]' + ) + ).to.exist; + }); + }); +}); diff --git a/test/unit/editors/substation/__snapshots__/sub-function-editor.test.snap.js b/test/unit/editors/substation/__snapshots__/sub-function-editor.test.snap.js index 0dce3e0a8c..57ef0e0619 100644 --- a/test/unit/editors/substation/__snapshots__/sub-function-editor.test.snap.js +++ b/test/unit/editors/substation/__snapshots__/sub-function-editor.test.snap.js @@ -8,6 +8,52 @@ snapshots["web component rendering SubFunction element with complete attribute s secondary="" tabindex="0" > + + + + + + + LNode + + + + + ConductingEquipment + + + + + SubFunction + + + + @@ -21,6 +67,52 @@ snapshots["web component rendering SubFunction element with missing desc and typ secondary="" tabindex="0" > + + + + + + + LNode + + + + + ConductingEquipment + + + + + SubFunction + + + + `; /* end snapshot web component rendering SubFunction element with missing desc and type attribute looks like the latest snapshot */