From 95ba5aba9df7348da92011c2f091b0b2b317260b Mon Sep 17 00:00:00 2001 From: marcvanraalte <86408026+marcvanraalte@users.noreply.github.com> Date: Mon, 28 Nov 2022 20:45:53 +0100 Subject: [PATCH] feat(substation/general-equipment-editor): edit wizard (#1089) * fix(rebasing 976 in main) * fix(generalEquipment.ts): added pattern and maxLength * fix(generalEquipment.ts): changed maxLenght into minLength --- .../substation/general-equipment-editor.ts | 29 +++- src/wizards/generalEquipment.ts | 114 ++++++++++++++ src/wizards/wizard-library.ts | 3 +- ...al-equipment-editor-wizard-editing.test.ts | 112 +++++++++++++ .../editors/substation/generalequipment.scd | 2 + .../general-equipment-editor.test.snap.js | 14 ++ .../substation-editor.test.snap.js | 4 + .../generalequipment.test.snap.js | 58 +++++++ test/unit/wizards/generalequipment.test.ts | 147 ++++++++++++++++++ 9 files changed, 474 insertions(+), 9 deletions(-) create mode 100644 src/wizards/generalEquipment.ts create mode 100644 test/integration/editors/substation/general-equipment-editor-wizard-editing.test.ts create mode 100644 test/unit/wizards/__snapshots__/generalequipment.test.snap.js create mode 100644 test/unit/wizards/generalequipment.test.ts diff --git a/src/editors/substation/general-equipment-editor.ts b/src/editors/substation/general-equipment-editor.ts index c9007040fa..26a5633ad1 100644 --- a/src/editors/substation/general-equipment-editor.ts +++ b/src/editors/substation/general-equipment-editor.ts @@ -15,8 +15,9 @@ import '../../action-pane.js'; import '../../editors/substation/eq-function-editor.js'; import '../../editors/substation/l-node-editor.js'; import { generalConductingEquipmentIcon } from '../../icons/icons.js'; -import { getChildElementsByTagName } from '../../foundation.js'; -//import { styles } from './foundation.js'; +import { getChildElementsByTagName, newWizardEvent } from '../../foundation.js'; +import { translate } from 'lit-translate'; +import { wizards } from '../../wizards/wizard-library.js'; @customElement('general-equipment-editor') export class GeneralEquipmentEditor extends LitElement { @@ -24,8 +25,7 @@ export class GeneralEquipmentEditor extends LitElement { doc!: XMLDocument; @property({ attribute: false }) element!: Element; - @property({ type: Boolean }) - readonly = false; + /** Whether `Function` and `SubFunction` are rendered */ @property({ type: Boolean }) showfunctions = false; @@ -40,9 +40,12 @@ export class GeneralEquipmentEditor extends LitElement { return `${name} ${desc ? `— ${desc}` : ''}`; } - private renderLNodes(): TemplateResult { - if (!this.showfunctions) return html``; + openEditWizard(): void { + const wizard = wizards['GeneralEquipment'].edit(this.element); + if (wizard) this.dispatchEvent(newWizardEvent(wizard)); + } + private renderLNodes(): TemplateResult { const lNodes = getChildElementsByTagName(this.element, 'LNode'); return lNodes.length @@ -59,8 +62,6 @@ export class GeneralEquipmentEditor extends LitElement { } private renderEqFunctions(): TemplateResult { - if (!this.showfunctions) return html``; - const eFunctions = getChildElementsByTagName(this.element, 'EqFunction'); return eFunctions.length @@ -77,10 +78,22 @@ export class GeneralEquipmentEditor extends LitElement { render(): TemplateResult { if (this.showfunctions) return html` + + this.openEditWizard()} + > + ${this.renderLNodes()} ${this.renderEqFunctions()} `; return html` + + this.openEditWizard()} + > + ${generalConductingEquipmentIcon} `; } diff --git a/src/wizards/generalEquipment.ts b/src/wizards/generalEquipment.ts new file mode 100644 index 0000000000..4ad0e3b3fa --- /dev/null +++ b/src/wizards/generalEquipment.ts @@ -0,0 +1,114 @@ +import { html, TemplateResult } from 'lit-element'; +import { get, translate } from 'lit-translate'; +import { + cloneElement, + getChildElementsByTagName, + getValue, + SimpleAction, + Wizard, + WizardActor, + WizardInputElement, +} from '../foundation'; + +export function editGeneralEquipmentWizard(element: Element): Wizard { + const name = element.getAttribute('name'); + const desc = element.getAttribute('desc'); + const type = element.getAttribute('type'); + const virtual = element.getAttribute('virtual'); + const reservedNames: string[] = getChildElementsByTagName( + element.parentElement!, + 'GeneralEquipment' + ) + .filter(sibling => sibling !== element) + .map(sibling => sibling.getAttribute('name')!); + + return [ + { + title: get('wizard.title.edit', { tagName: 'GeneralEquipment' }), + primary: { + icon: 'save', + label: get('save'), + action: updateGeneralEquipmentAction(element), + }, + content: [ + ...contentGeneralEquipmentWizard({ + name, + desc, + type, + virtual, + reservedNames, + }), + ], + }, + ]; +} + +function updateGeneralEquipmentAction(element: Element): WizardActor { + return (inputs: WizardInputElement[]): SimpleAction[] => { + const generalEquipmentAttrs: Record = {}; + const generalEquipmentKeys = ['name', 'desc', 'type', 'virtual']; + generalEquipmentKeys.forEach(key => { + generalEquipmentAttrs[key] = getValue(inputs.find(i => i.label === key)!); + }); + + if ( + generalEquipmentKeys.some( + key => generalEquipmentAttrs[key] !== element.getAttribute(key) + ) + ) { + const newElement = cloneElement(element, generalEquipmentAttrs); + return [ + { + old: { element }, + new: { element: newElement }, + }, + ]; + } + + return []; + }; +} + +interface ContentOptions { + name: string | null; + desc: string | null; + type: string | null; + virtual: string | null; + reservedNames: string[]; +} + +export function contentGeneralEquipmentWizard( + content: ContentOptions +): TemplateResult[] { + return [ + html``, + html``, + html``, + html``, + ]; +} diff --git a/src/wizards/wizard-library.ts b/src/wizards/wizard-library.ts index 1a99d6ef5c..23e68d2f51 100644 --- a/src/wizards/wizard-library.ts +++ b/src/wizards/wizard-library.ts @@ -37,6 +37,7 @@ import { } from './subfunction.js'; import { editSampledValueControlWizard } from './sampledvaluecontrol.js'; import { editSubEquipmentWizard } from './subequipment.js'; +import { editGeneralEquipmentWizard } from './generalEquipment.js'; type SclElementWizard = ( element: Element, @@ -223,7 +224,7 @@ export const wizards: Record< create: createFunctionWizard, }, GeneralEquipment: { - edit: emptyWizard, + edit: editGeneralEquipmentWizard, create: emptyWizard, }, GetCBValues: { diff --git a/test/integration/editors/substation/general-equipment-editor-wizard-editing.test.ts b/test/integration/editors/substation/general-equipment-editor-wizard-editing.test.ts new file mode 100644 index 0000000000..a4472328eb --- /dev/null +++ b/test/integration/editors/substation/general-equipment-editor-wizard-editing.test.ts @@ -0,0 +1,112 @@ +import { expect, fixture, html } from '@open-wc/testing'; + +import '../../../mock-wizard-editor.js'; +import { MockWizardEditor } from '../../../mock-wizard-editor.js'; + +import '../../../../src/editors/substation/general-equipment-editor.js'; +import { GeneralEquipmentEditor } from '../../../../src/editors/substation/general-equipment-editor.js'; +import { WizardTextField } from '../../../../src/wizard-textfield.js'; + +describe('general-equipment-editor wizarding editing integration', () => { + describe('edit wizard', () => { + let doc: XMLDocument; + let parent: MockWizardEditor; + let element: GeneralEquipmentEditor | null; + + let nameField: WizardTextField; + let descField: WizardTextField; + let typeField: WizardTextField; + let secondaryAction: HTMLElement; + let primaryAction: HTMLElement; + + beforeEach(async () => { + doc = await fetch( + '/test/testfiles/editors/substation/generalequipment.scd' + ) + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); + parent = ( + await fixture( + html`` + ) + ); + + element = parent.querySelector('general-equipment-editor'); + await (( + element?.shadowRoot?.querySelector('mwc-icon-button[icon="edit"]') + )).click(); + await parent.updateComplete; + + nameField = ( + parent.wizardUI.dialog?.querySelector('wizard-textfield[label="name"]') + ); + descField = ( + parent.wizardUI.dialog?.querySelector('wizard-textfield[label="desc"]') + ); + typeField = ( + parent.wizardUI.dialog?.querySelector('wizard-textfield[label="type"]') + ); + secondaryAction = ( + parent.wizardUI.dialog?.querySelector( + 'mwc-button[slot="secondaryAction"]' + ) + ); + primaryAction = ( + parent.wizardUI.dialog?.querySelector( + 'mwc-button[slot="primaryAction"]' + ) + ); + }); + it('closes on secondary action', async () => { + secondaryAction.click(); + await new Promise(resolve => setTimeout(resolve, 100)); // await animation + expect(parent.wizardUI.dialog).to.not.exist; + }); + it('does not change name attribute if not unique within parent element', async () => { + const oldName = nameField.value; + nameField.value = 'genSub2'; + primaryAction.click(); + await parent.updateComplete; + expect( + doc.querySelector('GeneralEquipment')?.getAttribute('name') + ).to.equal(oldName); + }); + it('changes name attribute on primary action', async () => { + parent.wizardUI.inputs[0].value = 'newName'; + primaryAction.click(); + await parent.updateComplete; + expect( + doc.querySelector('GeneralEquipment')?.getAttribute('name') + ).to.equal('newName'); + }); + it('changes type attribute on primary action', async () => { + parent.wizardUI.inputs[2].value = 'newAXN'; + primaryAction.click(); + await parent.updateComplete; + expect( + doc.querySelector('GeneralEquipment')?.getAttribute('type') + ).to.equal('newAXN'); + }); + it('changes desc attribute on primary action', async () => { + descField.value = 'newDesc'; + primaryAction.click(); + await parent.updateComplete; + expect( + doc.querySelector('GeneralEquipment')?.getAttribute('desc') + ).to.equal('newDesc'); + }); + it('deletes desc attribute if wizard-textfield is deactivated', async () => { + await new Promise(resolve => setTimeout(resolve, 100)); // await animation + descField.nullSwitch!.click(); + await parent.updateComplete; + await primaryAction.click(); + await parent.updateComplete; + expect(doc.querySelector('GeneralEquipment')?.getAttribute('desc')).to.be + .null; + }); + }); +}); diff --git a/test/testfiles/editors/substation/generalequipment.scd b/test/testfiles/editors/substation/generalequipment.scd index 3734b373d0..ee6eef54df 100644 --- a/test/testfiles/editors/substation/generalequipment.scd +++ b/test/testfiles/editors/substation/generalequipment.scd @@ -6,6 +6,8 @@ + + diff --git a/test/unit/editors/substation/__snapshots__/general-equipment-editor.test.snap.js b/test/unit/editors/substation/__snapshots__/general-equipment-editor.test.snap.js index 502cf5884c..a0afdbe338 100644 --- a/test/unit/editors/substation/__snapshots__/general-equipment-editor.test.snap.js +++ b/test/unit/editors/substation/__snapshots__/general-equipment-editor.test.snap.js @@ -6,6 +6,13 @@ snapshots["Editor web component for GeneralEquipment SCL element rendered as act label="genSub" tabindex="0" > + + + + @@ -17,6 +24,13 @@ snapshots["Editor web component for GeneralEquipment SCL element rendered as act label="genSub — someDesc" tabindex="0" > + + + +
diff --git a/test/unit/editors/substation/__snapshots__/substation-editor.test.snap.js b/test/unit/editors/substation/__snapshots__/substation-editor.test.snap.js index 37b7084eb4..62031993e6 100644 --- a/test/unit/editors/substation/__snapshots__/substation-editor.test.snap.js +++ b/test/unit/editors/substation/__snapshots__/substation-editor.test.snap.js @@ -430,6 +430,8 @@ snapshots["substation-editor with general-equipment children with showfunctions
+ +
@@ -537,6 +539,8 @@ snapshots["substation-editor with general-equipment children with showfunctions
+ +
diff --git a/test/unit/wizards/__snapshots__/generalequipment.test.snap.js b/test/unit/wizards/__snapshots__/generalequipment.test.snap.js new file mode 100644 index 0000000000..6858ec4606 --- /dev/null +++ b/test/unit/wizards/__snapshots__/generalequipment.test.snap.js @@ -0,0 +1,58 @@ +/* @web/test-runner snapshot v1 */ +export const snapshots = {}; + +snapshots["Wizards for SCL GeneralEquipment element define an edit wizard that looks like the the latest snapshot"] = +` +
+ + + + + + + + +
+ + + + +
+`; +/* end snapshot Wizards for SCL GeneralEquipment element define an edit wizard that looks like the the latest snapshot */ + diff --git a/test/unit/wizards/generalequipment.test.ts b/test/unit/wizards/generalequipment.test.ts new file mode 100644 index 0000000000..0b6ac2a352 --- /dev/null +++ b/test/unit/wizards/generalequipment.test.ts @@ -0,0 +1,147 @@ +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 { + EditorAction, + isCreate, + isReplace, + Replace, + WizardInputElement, +} from '../../../src/foundation.js'; +import { editGeneralEquipmentWizard } from '../../../src/wizards/generalEquipment.js'; +import { boolean } from 'fast-check'; +import { WizardCheckbox } from '../../../src/wizard-checkbox.js'; + +describe('Wizards for SCL GeneralEquipment 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/editors/substation/generalequipment.scd') + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); + + actionEvent = spy(); + window.addEventListener('editor-action', actionEvent); + }); + + describe('define an edit wizard that', () => { + beforeEach(async () => { + const wizard = editGeneralEquipmentWizard( + doc.querySelector('GeneralEquipment')! + ); + 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 () => + await expect(element.wizardUI.dialog).dom.to.equalSnapshot()); + + it('does not accept empty name attribute', async () => { + inputs[0].value = ''; + await element.requestUpdate(); + await primaryAction.click(); + expect(actionEvent).to.not.have.been.called; + }); + + it('triggers simple edit 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(isReplace); + const editAction = action; + + expect(editAction.new.element).to.have.attribute( + 'name', + 'someNonEmptyName' + ); + }); + + it('allows to create non required attribute virtual', async () => { + const virtualCheckbox = ( + element.wizardUI.dialog?.querySelector( + 'wizard-checkbox[label="virtual"]' + ) + ); + + virtualCheckbox.nullSwitch!.click(); + virtualCheckbox.maybeValue = 'true'; + await element.requestUpdate(); + await primaryAction.click(); + + expect(actionEvent).to.be.calledOnce; + const action = actionEvent.args[0][0].detail.action; + expect(action).to.satisfy(isReplace); + const editAction = action; + + expect(editAction.new.element).to.have.attribute('virtual', 'true'); + }); + + it('does not accept empty type attribute', async () => { + inputs[2].value = ''; + await element.requestUpdate(); + await primaryAction.click(); + expect(actionEvent).to.not.have.been.called; + }); + + it('does not accept invalid type attribute', async () => { + inputs[2].value = 'notAXN'; + await element.requestUpdate(); + await primaryAction.click(); + expect(actionEvent).to.not.have.been.called; + }); + + it('allows to create type attribute', async () => { + inputs[2].value = 'BAT'; + await element.requestUpdate(); + await primaryAction.click(); + expect(actionEvent).to.be.calledOnce; + const action = actionEvent.args[0][0].detail.action; + expect(action).to.satisfy(isReplace); + const editAction = action; + + expect(editAction.new.element).to.have.attribute('type', 'BAT'); + }); + + it('allows to create non required attribute desc', async () => { + inputs[1].value = 'someNonEmptyDesc'; + + await element.requestUpdate(); + await primaryAction.click(); + + expect(actionEvent).to.be.calledOnce; + const action = actionEvent.args[0][0].detail.action; + expect(action).to.satisfy(isReplace); + const editAction = action; + + expect(editAction.new.element).to.have.attribute( + 'desc', + 'someNonEmptyDesc' + ); + }); + }); +});