From bba9b3c7154a6e7d81a0776d10b1a41738bad388 Mon Sep 17 00:00:00 2001 From: Dennis Labordus Date: Mon, 16 May 2022 08:16:18 +0200 Subject: [PATCH] feat(editor/ied): Add wizard/action to remove IED including references (#732) * Add action to remove IED, including references. * Updated German translations. * Fixed small issues. * Added remove button to IED Editor. * Refactor cleanup Inputs and add logic to removing IED. * Updated comments. --- src/editors/IED.ts | 4 +- src/editors/ied/ied-container.ts | 64 +++++-- .../sampledvalues/subscriber-ied-list-smv.ts | 80 +------- src/editors/subscription/subscriber-list.ts | 85 +-------- src/editors/substation/ied-editor.ts | 26 ++- src/foundation.ts | 2 +- src/foundation/ied.ts | 78 ++++++++ src/translations/de.ts | 3 + src/translations/en.ts | 3 + src/wizards/foundation/references.ts | 62 +++++- src/wizards/ied.ts | 99 +++++++++- .../ied-editor-wizarding-integration.test.ts | 13 ++ .../__snapshots__/ied-container.test.snap.js | 7 + .../__snapshots__/ied-editor.test.snap.js | 13 +- .../editors/substation/ied-editor.test.ts | 13 ++ .../wizards/__snapshots__/ied.test.snap.js | 180 +++++++++++++++++- .../wizards/foundation/references.test.ts | 124 +++++++----- test/unit/wizards/ied.test.ts | 45 ++++- test/unit/wizards/test-support.ts | 12 ++ 19 files changed, 677 insertions(+), 236 deletions(-) create mode 100644 src/foundation/ied.ts diff --git a/src/editors/IED.ts b/src/editors/IED.ts index bbc1bb1266..89503d2ffc 100644 --- a/src/editors/IED.ts +++ b/src/editors/IED.ts @@ -49,7 +49,9 @@ export default class IedPlugin extends LitElement { } private get selectedIed(): Element | undefined { - if (iedEditorSelectedIed === undefined) { + // When there is no IED selected, or the selected IED has no parent (IED has been removed) + // select the first IED from the List. + if (iedEditorSelectedIed === undefined || iedEditorSelectedIed.parentElement === null) { const iedList = this.alphabeticOrderedIeds; if (iedList.length > 0) { iedEditorSelectedIed = iedList[0]; diff --git a/src/editors/ied/ied-container.ts b/src/editors/ied/ied-container.ts index 209cb22cb7..b9a7d8d74b 100644 --- a/src/editors/ied/ied-container.ts +++ b/src/editors/ied/ied-container.ts @@ -9,11 +9,17 @@ import { import { nothing } from 'lit-html'; import { translate } from "lit-translate"; -import {wizards} from "../../wizards/wizard-library.js"; import '../../action-pane.js'; import './access-point-container.js'; + +import { wizards } from "../../wizards/wizard-library.js"; import { Nsdoc } from '../../foundation/nsdoc.js'; -import { getDescriptionAttribute, getNameAttribute, newWizardEvent } from '../../foundation.js'; +import { + getDescriptionAttribute, + getNameAttribute, + newActionEvent, + newWizardEvent} from '../../foundation.js'; +import { removeIEDWizard } from "../../wizards/ied.js"; /** [[`IED`]] plugin subeditor for editing `IED` element. */ @customElement('ied-container') @@ -24,12 +30,22 @@ export class IedContainer extends LitElement { @property() nsdoc!: Nsdoc; - + private openEditWizard(): void { const wizard = wizards['IED'].edit(this.element); if (wizard) this.dispatchEvent(newWizardEvent(wizard)); } + private removeIED(): void { + const wizard = removeIEDWizard(this.element); + if (wizard) { + this.dispatchEvent(newWizardEvent(() => wizard)); + } else { + // If no Wizard is needed, just remove the element. + this.dispatchEvent(newActionEvent({ old: { parent: this.element.parentElement!, element: this.element } })); + } + } + private header(): TemplateResult { const name = getNameAttribute(this.element); const desc = getDescriptionAttribute(this.element); @@ -38,22 +54,34 @@ export class IedContainer extends LitElement { } render(): TemplateResult { - return html` - developer_board - - this.openEditWizard()} - > - - ${Array.from(this.element.querySelectorAll(':scope > AccessPoint')).map( - ap => html``)} + return html` + + developer_board + + this.removeIED()} + > + + + this.openEditWizard()} + > + + ${Array.from(this.element.querySelectorAll(':scope > AccessPoint')).map( + ap => html``)} `; } - static styles = css``; + static styles = css` + abbr { + text-decoration: none; + border-bottom: none; + } + `; } diff --git a/src/editors/sampledvalues/subscriber-ied-list-smv.ts b/src/editors/sampledvalues/subscriber-ied-list-smv.ts index 5ea8d83142..c69c9f7915 100644 --- a/src/editors/sampledvalues/subscriber-ied-list-smv.ts +++ b/src/editors/sampledvalues/subscriber-ied-list-smv.ts @@ -18,9 +18,7 @@ import { Create, createElement, Delete, - identity, newActionEvent, - selector, } from '../../foundation.js'; import { SampledValuesSelectEvent, @@ -28,6 +26,10 @@ import { styles, SubscribeStatus, } from './foundation.js'; +import { + emptyInputsDeleteActions, + getFcdaReferences +} from "../../foundation/ied.js"; /** * An IED within this IED list has 2 properties: @@ -39,33 +41,6 @@ interface IED { partial?: boolean; } -/** - * All available FCDA references that are used to link ExtRefs. - */ -const fcdaReferences = [ - 'ldInst', - 'lnClass', - 'lnInst', - 'prefix', - 'doName', - 'daName', -]; - -/** - * Get all the FCDA attributes containing values from a specific element. - * @param elementContainingFcdaReferences - The element to use - * @returns FCDA references - */ -function getFcdaReferences(elementContainingFcdaReferences: Element): string { - return fcdaReferences - .map(fcdaRef => - elementContainingFcdaReferences.getAttribute(fcdaRef) - ? `[${fcdaRef}="${elementContainingFcdaReferences.getAttribute(fcdaRef)}"]` - : '' - ) - .join(''); -} - /** An sub element for subscribing and unsubscribing IEDs to Sampled Values messages. */ @customElement('subscriber-ied-list-smv') export class SubscriberIEDListSmv extends LitElement { @@ -261,56 +236,17 @@ export class SubscriberIEDListSmv extends LitElement { }); }); + // Check if empty Input Element should also be removed. + actions.push(...emptyInputsDeleteActions(actions)); + this.dispatchEvent( newActionEvent({ title: 'Disconnect', - actions: this.extendDeleteActions(actions), + actions: actions, }) ); } - /** - * Creating Delete actions in case Inputs elements are empty. - * @param extRefDeleteActions - All Delete actions for ExtRefs. - * @returns Possible delete actions for empty Inputs elements. - */ - private extendDeleteActions(extRefDeleteActions: Delete[]): Delete[] { - if (!extRefDeleteActions.length) return []; - - // Initialize with the already existing ExtRef Delete actions. - const extendedDeleteActions: Delete[] = extRefDeleteActions; - const inputsMap: Record = {}; - - for (const extRefDeleteAction of extRefDeleteActions) { - const extRef = extRefDeleteAction.old.element; - const inputsElement = extRefDeleteAction.old.parent; - - const id = identity(inputsElement); - if (!inputsMap[id]) inputsMap[id] = (inputsElement.cloneNode(true)); - - const linkedExtRef = inputsMap[id].querySelector(`ExtRef[iedName=${extRef.getAttribute('iedName')}]` + - `${getFcdaReferences(extRef)}`); - - if (linkedExtRef) inputsMap[id].removeChild(linkedExtRef); - } - - // create delete action for each empty inputs - Object.entries(inputsMap).forEach(([key, value]) => { - if (value.children.length ! == 0) { - const doc = extRefDeleteActions[0].old.parent.ownerDocument!; - const inputs = doc.querySelector(selector('Inputs', key)); - - if (inputs && inputs.parentElement) { - extendedDeleteActions.push({ - old: { parent: inputs.parentElement, element: inputs }, - }); - } - } - }); - - return extendedDeleteActions; - } - protected updated(): void { if (this.subscriberWrapper) { this.subscriberWrapper.scrollTo(0, 0); diff --git a/src/editors/subscription/subscriber-list.ts b/src/editors/subscription/subscriber-list.ts index cf8646ecb1..2bc6aefd0e 100644 --- a/src/editors/subscription/subscriber-list.ts +++ b/src/editors/subscription/subscriber-list.ts @@ -18,9 +18,7 @@ import { Create, createElement, Delete, - identity, newActionEvent, - selector, } from '../../foundation.js'; import { newSubscriptionEvent, @@ -32,6 +30,10 @@ import { View, ViewEvent, } from './foundation.js'; +import { + emptyInputsDeleteActions, + getFcdaReferences +} from "../../foundation/ied.js"; /** * An element within this list has 2 properties: @@ -43,35 +45,6 @@ interface ListElement { partial?: boolean; } -/** - * All available FCDA references that are used to link ExtRefs. - */ -const fcdaReferences = [ - 'ldInst', - 'lnClass', - 'lnInst', - 'prefix', - 'doName', - 'daName', -]; - -/** - * Get all the FCDA attributes containing values from a specific element. - * @param elementContainingFcdaReferences - The element to use - * @returns FCDA references - */ -function getFcdaReferences(elementContainingFcdaReferences: Element): string { - return fcdaReferences - .map(fcdaRef => - elementContainingFcdaReferences.getAttribute(fcdaRef) - ? `[${fcdaRef}="${elementContainingFcdaReferences.getAttribute( - fcdaRef - )}"]` - : '' - ) - .join(''); -} - /** Defining view outside the class, which makes it persistent. */ let view: View = View.GOOSE_PUBLISHER; @@ -351,59 +324,17 @@ export class SubscriberList extends LitElement { }); }); + // Check if empty Input Element should also be removed. + actions.push(...emptyInputsDeleteActions(actions)); + this.dispatchEvent( newActionEvent({ title: 'Disconnect', - actions: this.extendDeleteActions(actions), + actions: actions, }) ); } - /** - * Creating Delete actions in case Inputs elements are empty. - * @param extRefDeleteActions - All Delete actions for ExtRefs. - * @returns Possible delete actions for empty Inputs elements. - */ - private extendDeleteActions(extRefDeleteActions: Delete[]): Delete[] { - if (!extRefDeleteActions.length) return []; - - // Initialize with the already existing ExtRef Delete actions. - const extendedDeleteActions: Delete[] = extRefDeleteActions; - const inputsMap: Record = {}; - - for (const extRefDeleteAction of extRefDeleteActions) { - const extRef = extRefDeleteAction.old.element; - const inputsElement = extRefDeleteAction.old.parent; - - const id = identity(inputsElement); - if (!inputsMap[id]) - inputsMap[id] = inputsElement.cloneNode(true); - - const linkedExtRef = inputsMap[id].querySelector( - `ExtRef[iedName=${extRef.getAttribute('iedName')}]` + - `${getFcdaReferences(extRef)}` - ); - - if (linkedExtRef) inputsMap[id].removeChild(linkedExtRef); - } - - // create delete action for each empty inputs - Object.entries(inputsMap).forEach(([key, value]) => { - if (value.children.length! == 0) { - const doc = extRefDeleteActions[0].old.parent.ownerDocument!; - const inputs = doc.querySelector(selector('Inputs', key)); - - if (inputs && inputs.parentElement) { - extendedDeleteActions.push({ - old: { parent: inputs.parentElement, element: inputs }, - }); - } - } - }); - - return extendedDeleteActions; - } - private resetElements() { this.subscribedElements = []; this.availableElements = []; diff --git a/src/editors/substation/ied-editor.ts b/src/editors/substation/ied-editor.ts index 61e9b35eb7..e609bc361d 100644 --- a/src/editors/substation/ied-editor.ts +++ b/src/editors/substation/ied-editor.ts @@ -15,10 +15,11 @@ import '../../action-icon.js'; import { createClientLnWizard } from '../../wizards/clientln.js'; import { gooseIcon, smvIcon, reportIcon } from '../../icons/icons.js'; import { wizards } from '../../wizards/wizard-library.js'; -import { newWizardEvent } from '../../foundation.js'; +import { newActionEvent, newWizardEvent } from '../../foundation.js'; import { selectGseControlWizard } from '../../wizards/gsecontrol.js'; import { selectSampledValueControlWizard } from '../../wizards/sampledvaluecontrol.js'; import { selectReportControlWizard } from '../../wizards/reportcontrol.js'; +import { removeIEDWizard } from "../../wizards/ied.js"; /** [[`SubstationEditor`]] subeditor for a child-less `IED` element. */ @customElement('ied-editor') @@ -65,6 +66,16 @@ export class IedEditor extends LitElement { if (wizard) this.dispatchEvent(newWizardEvent(wizard)); } + private removeIED(): void { + const wizard = removeIEDWizard(this.element); + if (wizard) { + this.dispatchEvent(newWizardEvent(() => wizard)); + } else { + // If no Wizard is needed, just remove the element. + this.dispatchEvent(newActionEvent({ old: { parent: this.element.parentElement!, element: this.element } })); + } + } + render(): TemplateResult { return html` ${gooseIcon}${gooseIcon} `; } } diff --git a/src/foundation.ts b/src/foundation.ts index eab6306dec..02643c4d9a 100644 --- a/src/foundation.ts +++ b/src/foundation.ts @@ -662,7 +662,7 @@ function kDCSelector(tagName: SCLTag, identity: string): string { } function associationIdentity(e: Element): string { - return `${identity(e.parentElement)}>${e.getAttribute('associationID')}`; + return `${identity(e.parentElement)}>${e.getAttribute('associationID')??''}`; } function associationSelector(tagName: SCLTag, identity: string): string { diff --git a/src/foundation/ied.ts b/src/foundation/ied.ts new file mode 100644 index 0000000000..21fb2f6190 --- /dev/null +++ b/src/foundation/ied.ts @@ -0,0 +1,78 @@ +import { + Delete, + identity, + selector +} from "../foundation.js"; + +/** + * All available FCDA references that are used to link ExtRefs. + */ +const fcdaReferences = [ + 'ldInst', + 'lnClass', + 'lnInst', + 'prefix', + 'doName', + 'daName', +]; + +/** + * Get all the FCDA attributes containing values from a specific element. + * + * @param elementContainingFcdaReferences - The element to use + * @returns FCDA references + */ +export function getFcdaReferences(elementContainingFcdaReferences: Element): string { + return fcdaReferences + .map(fcdaRef => + elementContainingFcdaReferences.getAttribute(fcdaRef) + ? `[${fcdaRef}="${elementContainingFcdaReferences.getAttribute( + fcdaRef + )}"]` + : '' + ) + .join(''); +} + +/** + * Creating Delete actions in case Inputs elements are empty. + * + * @param extRefDeleteActions - All Delete actions for ExtRefs. + * @returns Possible delete actions for empty Inputs elements. + */ +export function emptyInputsDeleteActions(extRefDeleteActions: Delete[]): Delete[] { + if (!extRefDeleteActions.length) return []; + + const inputDeleteActions: Delete[] = []; + const inputsMap: Record = {}; + + for (const extRefDeleteAction of extRefDeleteActions) { + const extRef = extRefDeleteAction.old.element; + const inputsElement = extRefDeleteAction.old.parent; + + const id = identity(inputsElement); + if (!inputsMap[id]) inputsMap[id] = (inputsElement.cloneNode(true)); + + // Search the ExtRef in the Cloned Inputs Element + const linkedExtRef = inputsMap[id].querySelector(`ExtRef[iedName=${extRef.getAttribute('iedName')}]` + + `${getFcdaReferences(extRef)}`); + // And if found remove it as child from the Cloned Inputs Element + if (linkedExtRef) inputsMap[id].removeChild(linkedExtRef); + } + + // Create delete action for each empty inputs + Object.entries(inputsMap).forEach(([key, value]) => { + if (value.children.length ! == 0) { + const doc = extRefDeleteActions[0].old.parent.ownerDocument!; + const inputs = doc.querySelector(selector('Inputs', key)); + + if (inputs && inputs.parentElement) { + inputDeleteActions.push({ + old: { parent: inputs.parentElement, element: inputs }, + }); + } + } + }); + + return inputDeleteActions; +} diff --git a/src/translations/de.ts b/src/translations/de.ts index fcdc76e097..ce8fe8653b 100644 --- a/src/translations/de.ts +++ b/src/translations/de.ts @@ -228,10 +228,13 @@ export const de: Translations = { descHelper: 'Beschreibung des IED', title: { edit: 'IED bearbeiten', + delete: 'IED mit Abhängigkeiten entfernen', + references: 'Gelöschte Abhängikeiten', }, }, action: { updateied: 'IED "{{name}}" bearbeitet', + deleteied: 'IED "{{name}}" entfernt', }, }, powertransformer: { diff --git a/src/translations/en.ts b/src/translations/en.ts index f22128d12b..02a695c372 100644 --- a/src/translations/en.ts +++ b/src/translations/en.ts @@ -225,10 +225,13 @@ export const en = { descHelper: 'IED description', title: { edit: 'Edit IED', + delete: 'Remove IED with references', + references: 'References to be removed', }, }, action: { updateied: 'Edited IED "{{name}}"', + deleteied: 'Removed IED "{{name}}"', }, }, powertransformer: { diff --git a/src/wizards/foundation/references.ts b/src/wizards/foundation/references.ts index 8606ab011e..e23a2019ff 100644 --- a/src/wizards/foundation/references.ts +++ b/src/wizards/foundation/references.ts @@ -1,4 +1,6 @@ import { + Delete, + getNameAttribute, isPublic, Replace } from "../../foundation.js"; @@ -127,13 +129,9 @@ function attributeFilterWithParentNameAttribute(tagName: string, parentInfo: Rec * @param value - The value to set on the cloned element or if null remove the attribute. * @returns Returns the cloned element. */ -function cloneElement(element: Element, attributeName: string, value: string | null): Element { +function cloneElement(element: Element, attributeName: string, value: string): Element { const newElement = element.cloneNode(false); - if (value === null) { - newElement.removeAttribute(attributeName); - } else { - newElement.setAttribute(attributeName, value); - } + newElement.setAttribute(attributeName, value); return newElement; } @@ -163,7 +161,7 @@ function cloneElementAndTextContent(element: Element, value: string | null): Ele * @returns Returns a list of Replace Actions that can be added to a Complex Action or returned directly for execution. */ export function updateReferences(element: Element, oldName: string | null, newName: string): Replace[] { - if (oldName === newName) { + if (oldName === null || oldName === newName) { return []; } @@ -176,8 +174,8 @@ export function updateReferences(element: Element, oldName: string | null, newNa referenceInfo.forEach(info => { // Depending on if an attribute value needs to be updated or the text content of an element // different scenarios need to be executed. + const filter = info.filter(element, info.attributeName, oldName); if (info.attributeName) { - const filter = info.filter(element, info.attributeName, oldName); Array.from(element.ownerDocument.querySelectorAll(`${filter}`)) .filter(isPublic) .forEach(element => { @@ -187,7 +185,6 @@ export function updateReferences(element: Element, oldName: string | null, newNa } else { // If the text content needs to be updated, filter on the text content can't be done in a CSS Selector. // So we query all elements the may need to be updated and filter them afterwards. - const filter = info.filter(element, info.attributeName, oldName); Array.from(element.ownerDocument.querySelectorAll(`${filter}`)) .filter(element => element.textContent === oldName) .filter(isPublic) @@ -199,3 +196,50 @@ export function updateReferences(element: Element, oldName: string | null, newNa }) return actions; } + +/** + * Function to create Delete actions to remove reference which point to the name of the element being removed. + * For instance the IED Name is used in other SCL Elements as attribute 'iedName' to reference the IED. + * These elements need to be removed if the IED is removed. + * + * @param element - The element that will be removed and it's name is used to search for references. + * @returns Returns a list of Delete Actions that can be added to a Complex Action or returned directly for execution. + */ +export function deleteReferences(element: Element): Delete[] { + const name = getNameAttribute(element) ?? null; + if (name === null) { + return []; + } + + const referenceInfo = referenceInfos[element.tagName]; + if (referenceInfo === undefined) { + return []; + } + + const actions: Delete[] = []; + referenceInfo.forEach(info => { + // Depending on if an attribute value is used for filtering or the text content of an element + // different scenarios need to be executed. + const filter = info.filter(element, info.attributeName, name); + if (info.attributeName) { + Array.from(element.ownerDocument.querySelectorAll(`${filter}`)) + .filter(isPublic) + .forEach(element => { + actions.push({old: { parent: element.parentElement!, element }}); + }) + } else { + // If the text content needs to be used for filtering, filter on the text content can't be done in a CSS Selector. + // So we query all elements the may need to be deleted and filter them afterwards. + Array.from(element.ownerDocument.querySelectorAll(`${filter}`)) + .filter(element => element.textContent === name) + .filter(isPublic) + .forEach(element => { + // We not only need to remove the element containing the text content, but the parent of this element. + if (element.parentElement) { + actions.push({old: {parent: element.parentElement.parentElement!, element: element.parentElement}}); + } + }) + } + }) + return actions; +} diff --git a/src/wizards/ied.ts b/src/wizards/ied.ts index 7fe15499b1..3cb02ed55b 100644 --- a/src/wizards/ied.ts +++ b/src/wizards/ied.ts @@ -1,10 +1,28 @@ import { html, TemplateResult } from 'lit-element'; import { get, translate } from 'lit-translate'; +import '@material/mwc-list'; +import '@material/mwc-list/mwc-list-item'; + import '../wizard-textfield.js'; -import { isPublic, Wizard } from '../foundation.js'; +import { + ComplexAction, + Delete, + EditorAction, + identity, + isPublic, + newWizardEvent, + Wizard, + WizardAction, + WizardActor, + WizardInputElement, + WizardMenuActor +} from '../foundation.js'; import { patterns } from "./foundation/limits.js"; + import { updateNamingAttributeWithReferencesAction } from "./foundation/actions.js"; +import { deleteReferences } from "./foundation/references.js"; +import { emptyInputsDeleteActions } from "../foundation/ied.js"; const iedNamePattern = "[A-Za-z][0-9A-Za-z_]{0,2}|" + "[A-Za-z][0-9A-Za-z_]{4,63}|" + @@ -39,6 +57,23 @@ export function renderIEDWizard( ]; } +function renderIEDReferencesWizard(references: Delete[]): TemplateResult[] { + return [html ` +
+

${translate('ied.wizard.title.references')}

+ + ${references.map(reference => { + const oldElement = reference.old.element + return html ` + + ${oldElement.tagName} + ${identity(reference.old.element)} + `; + })} + +
`]; +} + export function reservedNamesIED(currentElement: Element): string[] { return Array.from( currentElement.parentNode!.querySelectorAll('IED') @@ -48,11 +83,73 @@ export function reservedNamesIED(currentElement: Element): string[] { .filter(name => name !== currentElement.getAttribute('name')); } +export function removeIEDAndReferences(element: Element): WizardActor { + return (inputs: WizardInputElement[], wizard: Element): EditorAction[] => { + // Close Edit Wizard, if open. + wizard.dispatchEvent(newWizardEvent()); + + // Get Delete Actions for other elements that also need to be removed + const referencesDeleteActions = deleteReferences(element); + // Use the ExtRef Elements to check if after removing the ExtRef there are empty Inputs that can also be removed. + const extRefsDeleteActions = referencesDeleteActions + .filter(deleteAction => (deleteAction.old.element).tagName === 'ExtRef') + const inputsDeleteActions = emptyInputsDeleteActions(extRefsDeleteActions) + + // Create Complex Action to remove IED and all references. + const name = element.getAttribute('name') ?? 'Unknown'; + const complexAction: ComplexAction = { + actions: [], + title: get('ied.action.deleteied', {name}), + }; + complexAction.actions.push({ old: { parent: element.parentElement!, element } }); + complexAction.actions.push(...referencesDeleteActions); + complexAction.actions.push(...inputsDeleteActions); + return [complexAction]; + } +} + +export function removeIEDWizard(element: Element): Wizard | null { + // Check if the IED has any references, if so show wizard with all references. + const references = deleteReferences(element); + if (references.length > 0) { + return [ + { + title: get('ied.wizard.title.delete'), + content: renderIEDReferencesWizard(references), + primary: { + icon: 'delete', + label: get('remove'), + action: removeIEDAndReferences(element), + }, + }, + ]; + } + return null; +} + export function editIEDWizard(element: Element): Wizard { + function removeIED(element: Element): WizardMenuActor { + return (): WizardAction[] => { + const wizard = removeIEDWizard(element); + if (wizard) { + return [() => wizard]; + } + // If no Wizard is needed, just remove the element. + return [{ old: { parent: element.parentElement!, element } }]; + }; + } + return [ { title: get('ied.wizard.title.edit'), element, + menuActions: [ + { + icon: 'delete', + label: get('remove'), + action: removeIED(element), + }, + ], primary: { icon: 'edit', label: get('save'), diff --git a/test/integration/editors/substation/ied-editor-wizarding-integration.test.ts b/test/integration/editors/substation/ied-editor-wizarding-integration.test.ts index 5ea927cdf6..9b32b6439c 100644 --- a/test/integration/editors/substation/ied-editor-wizarding-integration.test.ts +++ b/test/integration/editors/substation/ied-editor-wizarding-integration.test.ts @@ -63,4 +63,17 @@ describe('IED editor component wizarding editing integration', () => { doc.querySelectorAll('IED[name="IED2"] ReportControl').length ); }); + + it('opens wizard showing References of one IED', async () => { + (( + iededitor.shadowRoot?.querySelector('mwc-fab[class="delete"]') + )).click(); + await parent.updateComplete; + + expect(parent.wizardUI.dialog).to.exist; + const referencesList = parent.wizardUI.dialog?.querySelectorAll('mwc-list-item'); + + expect(referencesList).to.be.not.undefined; + expect(referencesList!.length).to.equal(7); + }); }); diff --git a/test/unit/editors/ied/__snapshots__/ied-container.test.snap.js b/test/unit/editors/ied/__snapshots__/ied-container.test.snap.js index fe2e72b654..f8b2ceb345 100644 --- a/test/unit/editors/ied/__snapshots__/ied-container.test.snap.js +++ b/test/unit/editors/ied/__snapshots__/ied-container.test.snap.js @@ -6,6 +6,13 @@ snapshots["ied-container looks like the latest snapshot"] = developer_board + + + + - - + + + + `; /* end snapshot A component to visualize SCL element IED looks like the latest snapshot */ diff --git a/test/unit/editors/substation/ied-editor.test.ts b/test/unit/editors/substation/ied-editor.test.ts index df4b22f16c..184234e568 100644 --- a/test/unit/editors/substation/ied-editor.test.ts +++ b/test/unit/editors/substation/ied-editor.test.ts @@ -78,6 +78,19 @@ describe('A component to visualize SCL element IED', () => { ); }); + it('triggers reference wizard for removing IED on action button click', async () => { + (( + element.shadowRoot?.querySelector('mwc-fab[class="delete"]') + )).click(); + + await element.requestUpdate(); + + expect(wizardEvent).to.have.be.calledOnce; + expect(wizardEvent.args[0][0].detail.wizard()[0].title).to.contain( + 'delete' + ); + }); + it('triggers create wizard for ClientLN element on action button click', async () => { (( element.shadowRoot?.querySelector('mwc-fab[class="connectreport"]') diff --git a/test/unit/wizards/__snapshots__/ied.test.snap.js b/test/unit/wizards/__snapshots__/ied.test.snap.js index c08a7b1020..92bfff5ee7 100644 --- a/test/unit/wizards/__snapshots__/ied.test.snap.js +++ b/test/unit/wizards/__snapshots__/ied.test.snap.js @@ -1,13 +1,36 @@ /* @web/test-runner snapshot v1 */ export const snapshots = {}; -snapshots["Wizards for SCL element IED edit existing IED looks like the latest snapshot"] = +snapshots["Wizards for SCL element IED edit IED looks like the latest snapshot"] = ` +
`; -/* end snapshot Wizards for SCL element IED edit existing IED looks like the latest snapshot */ +/* end snapshot Wizards for SCL element IED edit IED looks like the latest snapshot */ + +snapshots["Wizards for SCL element IED remove IED looks like the latest snapshot"] = +` +
+
+

+ [ied.wizard.title.references] +

+ + + + Association + + + IED2>P1> + + + + + ClientLN + + + IED2>>CBSW> XSWI 1>ReportCb>IED1 P1 CircuitBreaker_CB1/ XCBR 1 + + + + + ClientLN + + + IED2>>CBSW> XSWI 2>ReportCb>IED1 P1 CircuitBreaker_CB1/ XCBR 1 + + + + + ConnectedAP + + + IED1 P1 + + + + + ExtRef + + + IED2>>CBSW> XSWI 1>IED1 Disconnectors/DC XSWI 1 Pos stVal + + + + + ExtRef + + + IED2>>CBSW> XSWI 1>IED1 Disconnectors/DC XSWI 1 Pos q + + + + + ExtRef + + + IED2>>CircuitBreaker_CB1> CSWI 1>IED1 CircuitBreaker_CB1/ XCBR 1 Pos stVal + + + + + ExtRef + + + IED2>>CircuitBreaker_CB1> CSWI 1>IED1 CircuitBreaker_CB1/ XCBR 1 Pos q + + + + + KDC + + + IED1>IED1 P1 + + + +
+
+ + + + +
+`; +/* end snapshot Wizards for SCL element IED remove IED looks like the latest snapshot */ diff --git a/test/unit/wizards/foundation/references.test.ts b/test/unit/wizards/foundation/references.test.ts index 1bffb5a5ca..fa80c1683e 100644 --- a/test/unit/wizards/foundation/references.test.ts +++ b/test/unit/wizards/foundation/references.test.ts @@ -1,21 +1,63 @@ import { + expectDeleteAction, expectReplaceAction, expectUpdateTextValue, fetchDoc, } from '../test-support.js'; -import { updateReferences } from '../../../../src/wizards/foundation/references.js'; +import { + deleteReferences, + updateReferences +} from '../../../../src/wizards/foundation/references.js'; import { expect } from '@open-wc/testing'; -import { Replace } from '../../../../src/foundation.js'; describe('Update reference for ', () => { let doc: XMLDocument; + describe('element without Reference Info (ConductingEquipment)', () => { + const ceName = 'QA1'; + let conductingEquipment: Element; + + beforeEach(async () => { + doc = await fetchDoc('/test/testfiles/wizards/ied.scd'); + conductingEquipment = doc.querySelector(`ConductingEquipment[name="${ceName}"]`)!; + }); + + it('will update no references to ConductingEquipment', function () { + const updateActions = updateReferences(conductingEquipment, ceName, 'Other CE Name'); + expect(updateActions.length).to.equal(0); + }); + + it('will delete no references to ConductingEquipment', function () { + const updateActions = deleteReferences(conductingEquipment); + expect(updateActions.length).to.equal(0); + }); + }); + + describe('element without Name Attribute (Value)', () => { + let connectAP: Element; + + beforeEach(async () => { + doc = await fetchDoc('/test/testfiles/wizards/ied.scd'); + connectAP = doc.querySelector(`ConnectedAP[iedName="IED1"][apName="P1"]`)!; + }); + + it('will update no references to ConnectedAP', function () { + const updateActions = updateReferences(connectAP, null, 'New Name'); + expect(updateActions.length).to.equal(0); + }); + + it('will delete no references to ConnectedAP', function () { + const updateActions = deleteReferences(connectAP); + expect(updateActions.length).to.equal(0); + }); + }); + describe('IED', () => { beforeEach(async () => { doc = await fetchDoc('/test/testfiles/wizards/ied.scd'); }); - it('will update all references to IED IED1', async function () { + it('will update all references to IED IED1', function () { const oldName = 'IED1'; const newName = 'NewIED1'; const ied = doc.querySelector(`IED[name="${oldName}"]`)!; @@ -24,43 +66,22 @@ describe('Update reference for ', () => { expect(updateActions.length).to.equal(9); expectReplaceAction( - updateActions[0], + updateActions[0], 'Association', 'iedName', oldName, newName ); expectReplaceAction( - updateActions[1], + updateActions[1], 'ClientLN', 'iedName', oldName, newName ); - expectReplaceAction( - updateActions[3], - 'ConnectedAP', - 'iedName', - oldName, - newName - ); - expectReplaceAction( - updateActions[4], - 'ExtRef', - 'iedName', - oldName, - newName - ); - expectReplaceAction( - updateActions[8], - 'KDC', - 'iedName', - oldName, - newName - ); }); - it('will update all references to IED IED2', async function () { + it('will update all references to IED IED2', function () { const oldName = 'IED2'; const newName = 'NewIED2'; const ied = doc.querySelector(`IED[name="${oldName}"]`)!; @@ -68,26 +89,41 @@ describe('Update reference for ', () => { const updateActions = updateReferences(ied, oldName, newName); expect(updateActions.length).to.equal(8); - expectReplaceAction( - updateActions[4], - 'LNode', - 'iedName', - oldName, - newName - ); expectUpdateTextValue( - updateActions[6], + updateActions[6], 'GSEControl', oldName, newName ); expectUpdateTextValue( - updateActions[7], + updateActions[7], 'SampledValueControl', oldName, newName ); }); + + it('will delete all references to IED IED1', function () { + const name = 'IED1'; + const ied = doc.querySelector(`IED[name="${name}"]`)!; + + const updateActions = deleteReferences(ied); + expect(updateActions.length).to.equal(9); + + expectDeleteAction(updateActions[0], 'Association'); + expectDeleteAction(updateActions[1], 'ClientLN'); + }); + + it('will delete all references to IED IED2', async function () { + const name = 'IED2'; + const ied = doc.querySelector(`IED[name="${name}"]`)!; + + const updateActions = deleteReferences(ied); + expect(updateActions.length).to.equal(8); + + expectDeleteAction(updateActions[6], 'GSEControl'); + expectDeleteAction(updateActions[7], 'SampledValueControl'); + }); }); describe('Substation', () => { @@ -95,7 +131,7 @@ describe('Update reference for ', () => { doc = await fetchDoc('/test/testfiles/wizards/references.scd'); }); - it('will update all references to Substation AA1', async function () { + it('will update all references to Substation AA1', function () { const oldName = 'AA1'; const newName = 'NewAA1'; const substation = doc.querySelector(`Substation[name="${oldName}"]`)!; @@ -104,7 +140,7 @@ describe('Update reference for ', () => { expect(updateActions.length).to.equal(48); expectReplaceAction( - updateActions[0], + updateActions[0], 'Terminal', 'substationName', oldName, @@ -118,7 +154,7 @@ describe('Update reference for ', () => { doc = await fetchDoc('/test/testfiles/wizards/references.scd'); }); - it('will update all references to VoltageLevel "J1"', async function () { + it('will update all references to VoltageLevel "J1"', function () { const oldName = 'J1'; const newName = 'J1 UPD'; const voltageLevel = doc.querySelector(`VoltageLevel[name="${oldName}"]`)!; @@ -127,7 +163,7 @@ describe('Update reference for ', () => { expect(updateActions.length).to.equal(48); expectReplaceAction( - updateActions[0], + updateActions[0], 'Terminal', 'voltageLevelName', oldName, @@ -141,7 +177,7 @@ describe('Update reference for ', () => { doc = await fetchDoc('/test/testfiles/wizards/references.scd'); }); - it('will update all references to BusBar "BusBar A"', async function () { + it('will update all references to BusBar "BusBar A"', function () { const oldName = 'BusBar A'; const newName = 'BusBar A UPD'; const bay = doc.querySelector(`Bay[name="${oldName}"]`)!; @@ -150,7 +186,7 @@ describe('Update reference for ', () => { expect(updateActions.length).to.equal(6); expectReplaceAction( - updateActions[0], + updateActions[0], 'Terminal', 'bayName', oldName, @@ -158,7 +194,7 @@ describe('Update reference for ', () => { ); }); - it('will update all references to Bay "Bay A"', async function () { + it('will update all references to Bay "Bay A"', function () { const oldName = 'Bay A'; const newName = 'Bay A UPD'; const bay = doc.querySelector(`Bay[name="${oldName}"]`)!; @@ -167,7 +203,7 @@ describe('Update reference for ', () => { expect(updateActions.length).to.equal(8); expectReplaceAction( - updateActions[0], + updateActions[0], 'Terminal', 'bayName', oldName, diff --git a/test/unit/wizards/ied.test.ts b/test/unit/wizards/ied.test.ts index c5ef050026..b5ea837b88 100644 --- a/test/unit/wizards/ied.test.ts +++ b/test/unit/wizards/ied.test.ts @@ -9,9 +9,14 @@ import { isSimple, WizardInputElement, } from '../../../src/foundation.js'; -import { editIEDWizard } from '../../../src/wizards/ied.js'; +import { + editIEDWizard, + removeIEDAndReferences, + removeIEDWizard +} from '../../../src/wizards/ied.js'; import { + expectDeleteAction, expectReplaceAction, expectUpdateAction, expectWizardNoUpdateAction, @@ -27,9 +32,12 @@ describe('Wizards for SCL element IED', () => { let element: MockWizard; let inputs: WizardInputElement[]; - describe('edit existing IED', () => { + beforeEach(async () => { + doc = await fetchDoc('/test/testfiles/wizards/ied.scd'); + }); + + describe('edit IED', () => { beforeEach(async () => { - doc = await fetchDoc('/test/testfiles/wizards/ied.scd'); ied = doc.querySelector('IED[name="IED3"]')!; element = await fixture(html``); @@ -82,4 +90,35 @@ describe('Wizards for SCL element IED', () => { await expect(element.wizardUI.dialog).dom.to.equalSnapshot(); }); }); + + describe('remove IED', () => { + beforeEach(async () => { + ied = doc.querySelector('IED[name="IED1"]')!; + + element = await fixture(html``); + const wizard = removeIEDWizard(ied); + element.workflow.push(() => wizard!); + await element.requestUpdate(); + inputs = Array.from(element.wizardUI.inputs); + }); + + it('remove IED should return expected actions', async function () { + const complexAction = removeIEDAndReferences(ied)(inputs, newWizard()); + + expect(complexAction.length).to.equal(1); + expect(complexAction[0]).to.not.satisfy(isSimple); + + const simpleActions = (complexAction[0]).actions; + expect(simpleActions.length).to.equal(12); + + expectDeleteAction(simpleActions[0], 'IED'); + expectDeleteAction(simpleActions[1], 'Association'); + expectDeleteAction(simpleActions[2], 'ClientLN'); + expectDeleteAction(simpleActions[11], 'Inputs'); + }); + + it('looks like the latest snapshot', async () => { + await expect(element.wizardUI.dialog).dom.to.equalSnapshot(); + }); + }); }); diff --git a/test/unit/wizards/test-support.ts b/test/unit/wizards/test-support.ts index 6f1dd4769b..243bc3f703 100644 --- a/test/unit/wizards/test-support.ts +++ b/test/unit/wizards/test-support.ts @@ -2,7 +2,9 @@ import { expect } from '@open-wc/testing'; import { Create, + Delete, isCreate, + isDelete, isReplace, isUpdate, Replace, @@ -138,3 +140,13 @@ export async function fetchDoc(docName: string): Promise { .then(response => response.text()) .then(str => new DOMParser().parseFromString(str, 'application/xml')); } + +export function expectDeleteAction( + simpleAction: SimpleAction, + tagName: string, +): void { + expect(simpleAction).to.satisfy(isDelete); + + const oldElement = (simpleAction).old.element; + expect((oldElement).tagName).to.be.equal(tagName); +}