Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(editor/ied): Add wizard/action to remove IED including references #732

Merged
merged 8 commits into from
May 16, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions src/editors/substation/ied-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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`<action-icon label="${this.name}" icon="developer_board">
<mwc-fab
Expand All @@ -76,10 +87,11 @@ export class IedEditor extends LitElement {
></mwc-fab
><mwc-fab
slot="action"
class="selectgse"
class="delete"
mini
@click="${() => this.openGseControlSelection()}"
><mwc-icon slot="icon">${gooseIcon}</mwc-icon></mwc-fab
@click="${() => this.removeIED() }"
icon="delete"
></mwc-fab
><mwc-fab
slot="action"
class="selectreport"
Expand All @@ -99,6 +111,12 @@ export class IedEditor extends LitElement {
@click="${() => this.openCommunicationMapping()}"
icon="add_link"
></mwc-fab
><mwc-fab
slot="action"
class="selectgse"
mini
@click="${() => this.openGseControlSelection()}"
><mwc-icon slot="icon">${gooseIcon}</mwc-icon></mwc-fab
></action-icon> `;
}
}
2 changes: 1 addition & 1 deletion src/foundation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions src/translations/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,10 +228,13 @@ export const de: Translations = {
descHelper: 'Beschreibung des IED',
title: {
edit: 'IED bearbeiten',
delete: '???',
JakobVogelsang marked this conversation as resolved.
Show resolved Hide resolved
JakobVogelsang marked this conversation as resolved.
Show resolved Hide resolved
dlabordus marked this conversation as resolved.
Show resolved Hide resolved
references: '???',
dlabordus marked this conversation as resolved.
Show resolved Hide resolved
},
},
action: {
updateied: 'IED "{{name}}" bearbeitet',
deleteied: 'IED "{{name}}" entfernt',
},
},
powertransformer: {
Expand Down
3 changes: 3 additions & 0 deletions src/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
62 changes: 53 additions & 9 deletions src/wizards/foundation/references.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {
Delete,
getNameAttribute,
isPublic,
Replace
} from "../../foundation.js";
Expand Down Expand Up @@ -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>element.cloneNode(false);
if (value === null) {
newElement.removeAttribute(attributeName);
} else {
newElement.setAttribute(attributeName, value);
}
newElement.setAttribute(attributeName, value);
return newElement;
}

Expand Down Expand Up @@ -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 [];
}

Expand All @@ -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 => {
Expand All @@ -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)
Expand All @@ -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[<ReferencesInfoTag>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;
}
89 changes: 88 additions & 1 deletion src/wizards/ied.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
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, getNameAttribute, identity,
isPublic,
newActionEvent, 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";

const iedNamePattern = "[A-Za-z][0-9A-Za-z_]{0,2}|" +
"[A-Za-z][0-9A-Za-z_]{4,63}|" +
Expand Down Expand Up @@ -39,6 +54,23 @@ export function renderIEDWizard(
];
}

function renderIEDReferencesWizard(references: Delete[]): TemplateResult[] {
return [html `
<section>
<h1>${translate('ied.wizard.title.references')}</h1>
<mwc-list>
${references.map(reference => {
const oldElement = <Element>reference.old.element
return html `
<mwc-list-item noninteractive twoline>
<span>${oldElement.tagName}</span>
<span slot="secondary">${identity(<Element>reference.old.element)}</span>
</mwc-list-item>`;
})}
</mwc-list>
</section>`];
}

export function reservedNamesIED(currentElement: Element): string[] {
return Array.from(
currentElement.parentNode!.querySelectorAll('IED')
Expand All @@ -48,11 +80,66 @@ 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());

// 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(...deleteReferences(element));
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'),
element,
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'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { MockWizardEditor } from '../../../mock-wizard-editor.js';
import '../../../../src/editors/substation/ied-editor.js';
import { FilteredList } from '../../../../src/filtered-list.js';
import { IedEditor } from '../../../../src/editors/substation/ied-editor.js';
import {ListBase} from "@material/mwc-list/mwc-list-base";

describe('IED editor component wizarding editing integration', () => {
let doc: XMLDocument;
Expand Down Expand Up @@ -63,4 +64,17 @@ describe('IED editor component wizarding editing integration', () => {
doc.querySelectorAll('IED[name="IED2"] ReportControl').length
);
});

it('opens wizard showing References of one IED', async () => {
(<HTMLElement>(
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);
});
});
13 changes: 10 additions & 3 deletions test/unit/editors/substation/__snapshots__/ied-editor.test.snap.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,11 @@ snapshots["A component to visualize SCL element IED looks like the latest snapsh
>
</mwc-fab>
<mwc-fab
class="selectgse"
class="delete"
icon="delete"
mini=""
slot="action"
>
<mwc-icon slot="icon">
</mwc-icon>
</mwc-fab>
<mwc-fab
class="selectreport"
Expand All @@ -45,6 +44,14 @@ snapshots["A component to visualize SCL element IED looks like the latest snapsh
slot="action"
>
</mwc-fab>
<mwc-fab
class="selectgse"
mini=""
slot="action"
>
<mwc-icon slot="icon">
</mwc-icon>
</mwc-fab>
</action-icon>
`;
/* end snapshot A component to visualize SCL element IED looks like the latest snapshot */
Expand Down
13 changes: 13 additions & 0 deletions test/unit/editors/substation/ied-editor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,19 @@ describe('A component to visualize SCL element IED', () => {
);
});

it('triggers reference wizard for removing IED on action button click', async () => {
(<HTMLElement>(
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 () => {
(<HTMLElement>(
element.shadowRoot?.querySelector('mwc-fab[class="connectreport"]')
Expand Down
Loading