diff --git a/src/editors/substation/tapchanger-editor.ts b/src/editors/substation/tapchanger-editor.ts index 025e40178..bca1a0043 100644 --- a/src/editors/substation/tapchanger-editor.ts +++ b/src/editors/substation/tapchanger-editor.ts @@ -1,23 +1,45 @@ import { + css, customElement, html, LitElement, TemplateResult, property, state, + query, } from 'lit-element'; import { translate } from 'lit-translate'; 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-pane.js'; import './eq-function-editor.js'; import './l-node-editor.js'; import './sub-equipment-editor.js'; -import { getChildElementsByTagName, newWizardEvent } from '../../foundation.js'; -import { wizards } from '../../wizards/wizard-library.js'; + +import { styles } from './foundation.js'; +import { + getChildElementsByTagName, + newActionEvent, + 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 + ); +} @customElement('tapchanger-editor') export class TapChangerEditor extends LitElement { @@ -36,9 +58,39 @@ export class TapChangerEditor extends LitElement { const name = this.element.getAttribute('name') ?? ''; const desc = this.element.getAttribute('desc'); - return `TapChanger.${name} ${desc ? `—TapChanger.${desc}` : ''}`; + return `${name} ${desc ? `—${desc}` : ''}`; + } + + @query('mwc-menu') addMenu!: Menu; + @query('mwc-icon-button[icon="playlist_add"]') addButton!: IconButton; + + openEditWizard(): void { + const wizard = wizards['TapChanger'].edit(this.element); + if (wizard) this.dispatchEvent(newWizardEvent(wizard)); + } + + private openCreateWizard(tagName: string): void { + const wizard = wizards[tagName].create(this.element!); + + if (wizard) this.dispatchEvent(newWizardEvent(wizard)); } + updated(): void { + if (this.addMenu && this.addButton) + this.addMenu.anchor = this.addButton; + } + + remove(): void { + if (this.element.parentElement) + this.dispatchEvent( + newActionEvent({ + old: { + parent: this.element.parentElement, + element: this.element, + }, + }) + ); + } private renderLNodes(): TemplateResult { const lNodes = getChildElementsByTagName(this.element, 'LNode'); @@ -55,12 +107,7 @@ export class TapChangerEditor extends LitElement { : html``; } - openEditWizard(): void { - const wizard = wizards['TapChanger'].edit(this.element); - if (wizard) this.dispatchEvent(newWizardEvent(wizard)); - } - - renderEqFunctions(): TemplateResult { + private renderEqFunctions(): TemplateResult { if (!this.showfunctions) return html``; const eqFunctions = getChildElementsByTagName(this.element, 'EqFunction'); @@ -90,15 +137,63 @@ export class TapChangerEditor extends LitElement { )}`; } + private renderAddButtons(): TemplateResult[] { + return childTags(this.element).map( + child => + html`${child}` + ); + } + render(): TemplateResult { return html` - this.openEditWizard()} - > - + this.openEditWizard()} + > + + + this.remove()} + > + + + (this.addMenu.open = true)} + > { + const tagName = ((e.target).selected).value; + this.openCreateWizard(tagName); + }} + >${this.renderAddButtons()} ${this.renderLNodes()} - ${this.renderEqFunctions()} ${this.renderSubEquipments()}`; + ${this.renderEqFunctions()} ${this.renderSubEquipments()} + `; } + + static styles = css` + ${styles} + + :host(.moving) { + opacity: 0.3; + } + + abbr { + text-decoration: none; + border-bottom: none; + } + `; } diff --git a/src/wizards/tapChanger.ts b/src/wizards/tapchanger.ts similarity index 70% rename from src/wizards/tapChanger.ts rename to src/wizards/tapchanger.ts index b7fcde3e4..91e4ddbdb 100644 --- a/src/wizards/tapChanger.ts +++ b/src/wizards/tapchanger.ts @@ -12,6 +12,24 @@ import { WizardInputElement, } from '../foundation.js'; +function createTapChangerAction(parent: Element): WizardActor { + return (inputs: WizardInputElement[]) => { + const tapChangerAttrs: Record = {}; + const tapChangerKeys = ['name', 'desc', 'type', 'virtual']; + tapChangerKeys.forEach(key => { + tapChangerAttrs[key] = getValue(inputs.find(i => i.label === key)!); + }); + + const tapChanger = createElement( + parent.ownerDocument, + 'TapChanger', + tapChangerAttrs + ); + + return [{ new: { parent, element: tapChanger } }]; + }; +} + function updateTapChangerAction(element: Element): WizardActor { return (inputs: WizardInputElement[]): SimpleAction[] => { const tapChangerAttrs: Record = {}; @@ -79,6 +97,36 @@ export function contentTapChangerWizard( ]; } +export function createTapChangerWizard(parent: Element): Wizard { + const name = ''; + const desc = null; + const type = 'LTC'; + const virtual = null; + const reservedNames = Array.from(parent.querySelectorAll('TapChanger')).map( + TapChanger => TapChanger.getAttribute('name')! + ); + + return [ + { + title: get('wizard.title.add', { tagName: 'TapChanger' }), + primary: { + icon: 'save', + label: get('save'), + action: createTapChangerAction(parent), + }, + content: [ + ...contentTapChangerWizard({ + name, + desc, + type, + virtual, + reservedNames, + }), + ], + }, + ]; +} + export function editTapChangerWizard(element: Element): Wizard { const name = element.getAttribute('name'); const desc = element.getAttribute('desc'); diff --git a/src/wizards/wizard-library.ts b/src/wizards/wizard-library.ts index 9cebe30d3..e0203ec7e 100644 --- a/src/wizards/wizard-library.ts +++ b/src/wizards/wizard-library.ts @@ -48,7 +48,7 @@ import { createTransformerWindingWizard, editTransformerWindingWizard, } from './transformerWinding.js'; -import { editTapChangerWizard } from './tapChanger.js'; +import { createTapChangerWizard, editTapChangerWizard } from './tapchanger.js'; type SclElementWizard = ( element: Element, @@ -520,7 +520,7 @@ export const wizards: Record< }, TapChanger: { edit: editTapChangerWizard, - create: emptyWizard, + create: createTapChangerWizard, }, Terminal: { edit: editTerminalWizard, diff --git a/test/integration/editors/substation/tapchanger-editor-wizard-editing.test.ts b/test/integration/editors/substation/tapchanger-editor-wizard-editing.test.ts index 550a4df73..fba8dbf9d 100644 --- a/test/integration/editors/substation/tapchanger-editor-wizard-editing.test.ts +++ b/test/integration/editors/substation/tapchanger-editor-wizard-editing.test.ts @@ -10,6 +10,42 @@ import { WizardTextField } from '../../../../src/wizard-textfield.js'; import { WizardCheckbox } from '../../../../src/wizard-checkbox.js'; import { MenuBase } from '@material/mwc-menu/mwc-menu-base.js'; +const openAndCancelMenu: ( + parent: MockWizardEditor, + element: TapChangerEditor +) => Promise = ( + parent: MockWizardEditor, + element: TapChangerEditor +): Promise => + new Promise(async resolve => { + expect(parent.wizardUI.dialog).to.be.undefined; + + element?.shadowRoot + ?.querySelector("mwc-icon-button[icon='playlist_add']")! + .click(); + const lnodMenuItem: ListItemBase = + element?.shadowRoot?.querySelector( + `mwc-list-item[value='LNode']` + )!; + lnodMenuItem.click(); + await new Promise(resolve => setTimeout(resolve, 100)); // await animation + + expect(parent.wizardUI.dialog).to.exist; + + const secondaryAction: HTMLElement = ( + parent.wizardUI.dialog?.querySelector( + 'mwc-button[slot="secondaryAction"][dialogaction="close"]' + ) + ); + + secondaryAction.click(); + await new Promise(resolve => setTimeout(resolve, 100)); // await animation + + expect(parent.wizardUI.dialog).to.be.undefined; + + return resolve(); + }); + describe('tapchanger-editor wizarding editing integration', () => { let doc: XMLDocument; let parent: MockWizardEditor; @@ -108,5 +144,63 @@ describe('tapchanger-editor wizarding editing integration', () => { ?.getAttribute('virtual') ).to.equal('false'); }); + + describe('has a delete icon button that', () => { + let deleteButton: HTMLElement; + + beforeEach(async () => { + deleteButton = ( + element?.shadowRoot?.querySelector('mwc-icon-button[icon="delete"]') + ); + await parent.updateComplete; + }); + + it('removes the attached TapChanger element from the document', async () => { + expect( + doc.querySelector( + 'TransformerWinding[name="withTapChanger1"] > TapChanger[name="tapChComplet"]' + ) + ).to.exist; + + await deleteButton.click(); + + expect( + doc.querySelector( + 'TransformerWinding[name="withTapChanger1"] > TapChanger[name="tapChComplet"]' + ) + ).to.not.exist; + }); + }); + }); + describe('Open add wizard', () => { + let doc: XMLDocument; + let parent: MockWizardEditor; + let element: TapChangerEditor | null; + + beforeEach(async () => { + doc = await fetch('/test/testfiles/editors/substation/TapChanger.scd') + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); + parent = ( + await fixture( + html` TapChanger[name="tapChComplet"]' + )} + >` + ) + ); + + element = parent.querySelector('tapchanger-editor'); + + await parent.updateComplete; + }); + + it('Should open the same wizard for the second time', async () => { + await openAndCancelMenu(parent, element!); + await openAndCancelMenu(parent, element!); + }); }); }); diff --git a/test/unit/editors/substation/__snapshots__/tapchanger-editor.test.snap.js b/test/unit/editors/substation/__snapshots__/tapchanger-editor.test.snap.js index ac3e20139..8a8bf4a45 100644 --- a/test/unit/editors/substation/__snapshots__/tapchanger-editor.test.snap.js +++ b/test/unit/editors/substation/__snapshots__/tapchanger-editor.test.snap.js @@ -3,7 +3,7 @@ export const snapshots = {}; snapshots["web component rendering TapChanger element rendering LNode and EqFunction children looks like the latest snapshot"] = ` + + + + + + + + + + + LNode + + + + + SubEquipment + + + + + EqFunction + + + +
@@ -25,7 +78,7 @@ snapshots["web component rendering TapChanger element rendering LNode and EqFunc snapshots["web component rendering TapChanger element rendering SubEquipment looks like the latest snapshot"] = ` + + + + + + + + + + + LNode + + + + + SubEquipment + + + + + EqFunction + + + + diff --git a/test/unit/editors/substation/__snapshots__/transformer-winding-editor.test.snap.js b/test/unit/editors/substation/__snapshots__/transformer-winding-editor.test.snap.js index e547c027f..806ee2698 100644 --- a/test/unit/editors/substation/__snapshots__/transformer-winding-editor.test.snap.js +++ b/test/unit/editors/substation/__snapshots__/transformer-winding-editor.test.snap.js @@ -42,6 +42,17 @@ snapshots["transformer-winding-editor with children when EqFunction elements are LNode + + + TapChanger + + + + + TapChanger + + + + + TapChanger + + +
+ + + + + + + + +
+ + + + + +`; +/* end snapshot Wizards for SCL TapChanger element define a create wizard that looks like the the latest snapshot */ + diff --git a/test/unit/wizards/__snapshots__/transformerwinding.test.snap.js b/test/unit/wizards/__snapshots__/transformerwinding.test.snap.js index 9ef977f45..195097a35 100644 --- a/test/unit/wizards/__snapshots__/transformerwinding.test.snap.js +++ b/test/unit/wizards/__snapshots__/transformerwinding.test.snap.js @@ -55,7 +55,7 @@ snapshots["Wizards for SCL TransformerWinding element define an edit wizard that `; /* end snapshot Wizards for SCL TransformerWinding element define an edit wizard that looks like the the latest snapshot */ -snapshots["Wizards for SCL TransformerWinding element define an create wizard that looks like the the latest snapshot"] = +snapshots["Wizards for SCL TransformerWinding element define a create wizard that looks like the the latest snapshot"] = ` `; -/* end snapshot Wizards for SCL TransformerWinding element define an create wizard that looks like the the latest snapshot */ +/* end snapshot Wizards for SCL TransformerWinding element define a create wizard that looks like the the latest snapshot */ diff --git a/test/unit/wizards/tapchanger.test.ts b/test/unit/wizards/tapchanger.test.ts index 35658b1ec..2a037b315 100644 --- a/test/unit/wizards/tapchanger.test.ts +++ b/test/unit/wizards/tapchanger.test.ts @@ -12,7 +12,10 @@ import { Replace, WizardInputElement, } from '../../../src/foundation.js'; -import { editTapChangerWizard } from '../../../src/wizards/tapChanger.js'; +import { + createTapChangerWizard, + editTapChangerWizard, +} from '../../../src/wizards/tapchanger.js'; import { WizardCheckbox } from '../../../src/wizard-checkbox.js'; describe('Wizards for SCL TapChanger element', () => { @@ -117,4 +120,101 @@ describe('Wizards for SCL TapChanger element', () => { ); }); }); + + describe('define a create wizard that', () => { + beforeEach(async () => { + const wizard = createTapChangerWizard( + doc.querySelector('TransformerWinding')! + ); + 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 () => { + await primaryAction.click(); + + expect(actionEvent).to.not.have.been.called; + }); + + it('allows to create required attributes name', 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'); + }); + + it('allows to create name and non required attribute virtual', async () => { + inputs[0].value = 'someNonEmptyName'; + + 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(isCreate); + const createAction = action; + expect(createAction.new.element).to.have.attribute( + 'name', + 'someNonEmptyName' + ); + expect(createAction.new.element).to.have.attribute('virtual', 'true'); + }); + + it('allows to create name and non required attribute desc', async () => { + inputs[0].value = 'someNonEmptyName'; + const descField = ( + element.wizardUI.dialog?.querySelector('wizard-textfield[label="desc"]') + ); + + await new Promise(resolve => setTimeout(resolve, 100)); // await animation + descField.nullSwitch!.click(); + await element.updateComplete; + 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(isCreate); + const createAction = action; + + expect(createAction.new.element).to.have.attribute( + 'desc', + 'someNonEmptyDesc' + ); + }); + }); }); diff --git a/test/unit/wizards/transformerwinding.test.ts b/test/unit/wizards/transformerwinding.test.ts index 3aa4c301e..bbc4b2985 100644 --- a/test/unit/wizards/transformerwinding.test.ts +++ b/test/unit/wizards/transformerwinding.test.ts @@ -130,7 +130,7 @@ describe('Wizards for SCL TransformerWinding element', () => { }); }); - describe('define an create wizard that', () => { + describe('define a create wizard that', () => { beforeEach(async () => { const wizard = createTransformerWindingWizard( doc.querySelector('PowerTransformer')!