Skip to content

Commit

Permalink
feat(wizards/reportcontrol): added new IED wizard to update name/desc…
Browse files Browse the repository at this point in the history
…ription (#494)

* Added first version of IED Wizard.
* Added function to update reference to IED Name.
* Updated values referring to the IED Name.
* Updated values referring to the IED Name.
* Processed review comments.
* Fixed and improved tests.
  • Loading branch information
Dennis Labordus authored Jan 19, 2022
1 parent 4eabdb3 commit 110c83d
Show file tree
Hide file tree
Showing 14 changed files with 1,122 additions and 4 deletions.
16 changes: 15 additions & 1 deletion src/editors/ied/ied-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import {
TemplateResult,
} from 'lit-element';
import { nothing } from 'lit-html';
import { translate } from "lit-translate";

import {wizards} from "../../wizards/wizard-library.js";
import '../../action-pane.js';
import { getDescriptionAttribute, getNameAttribute } from '../../foundation.js';
import {getDescriptionAttribute, getNameAttribute, newWizardEvent} from '../../foundation.js';
import './access-point-container.js';

/** [[`IED`]] plugin subeditor for editing `IED` element. */
Expand All @@ -19,6 +21,11 @@ export class IedContainer extends LitElement {
@property({ attribute: false })
element!: Element;

private openEditWizard(): void {
const wizard = wizards['IED'].edit(this.element);
if (wizard) this.dispatchEvent(newWizardEvent(wizard));
}

private header(): TemplateResult {
const name = getNameAttribute(this.element);
const desc = getDescriptionAttribute(this.element);
Expand All @@ -28,6 +35,13 @@ export class IedContainer extends LitElement {

render(): TemplateResult {
return html`<action-pane .label="${this.header()}">
<abbr slot="action" title="${translate('edit')}">
<mwc-icon-button
icon="edit"
@click=${() => this.openEditWizard()}
></mwc-icon-button>
</abbr>
${Array.from(this.element.querySelectorAll(':scope > AccessPoint')).map(
ap => html`<access-point-container
.element=${ap}
Expand Down
12 changes: 12 additions & 0 deletions src/translations/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,18 @@ export const de: Translations = {
missing: 'Kein IED vorhanden',
toggleChildElements: '???',
},
ied: {
wizard: {
nameHelper: 'Name des IED',
descHelper: 'Beschreibung des IED',
title: {
edit: 'IED bearbeiten',
},
},
action: {
updateied: 'IED "{{iedName}}" bearbeitet',
},
},
powertransformer: {
wizard: {
nameHelper: '`Name des Leistungstransformators',
Expand Down
12 changes: 12 additions & 0 deletions src/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,18 @@ export const en = {
missing: 'No IED',
toggleChildElements: 'Toggle child elements',
},
ied: {
wizard: {
nameHelper: 'IED name',
descHelper: 'IED description',
title: {
edit: 'Edit IED',
},
},
action: {
updateied: 'Edited IED "{{iedName}}"',
},
},
powertransformer: {
wizard: {
nameHelper: 'Power transformer name',
Expand Down
93 changes: 93 additions & 0 deletions src/wizards/foundation/references.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {isPublic, SimpleAction} from "../../foundation.js";

const referenceInfoTags = ['IED'] as const;
type ReferencesInfoTag = typeof referenceInfoTags[number];

/*
* For every supported tag a list of information about which elements to search for and which attribute value
* to replace with the new value typed in the screen by the user. This is used to update references to a name
* of an element by other elements.
* If the attribute is null the text content of the found element will be replaced.
*/
const referenceInfos: Record<
ReferencesInfoTag,
{
elementQuery: string;
attribute: string | null;
}[]
> = {
IED:
[{
elementQuery: `Association`,
attribute: 'iedName'
}, {
elementQuery: `ClientLN`,
attribute: 'iedName'
}, {
elementQuery: `ConnectedAP`,
attribute: 'iedName'
}, {
elementQuery: `ExtRef`,
attribute: 'iedName'
}, {
elementQuery: `KDC`,
attribute: 'iedName'
}, {
elementQuery: `LNode`,
attribute: 'iedName'
}, {
elementQuery: `GSEControl > IEDName`,
attribute: null
}, {
elementQuery: `SampledValueControl > IEDName`,
attribute: null
}]
}

function cloneElement(element: Element, attributeName: string, value: string | null): Element {
const newElement = <Element>element.cloneNode(false);
if (value === null) {
newElement.removeAttribute(attributeName);
} else {
newElement.setAttribute(attributeName, value);
}
return newElement;
}

function cloneElementAndTextContent(element: Element, value: string | null): Element {
const newElement = <Element>element.cloneNode(false);
newElement.textContent = value;
return newElement;
}

export function updateReferences(element: Element, oldValue: string | null, newValue: string): SimpleAction[] {
if (oldValue === newValue) {
return [];
}

const referenceInfo = referenceInfos[<ReferencesInfoTag>element.tagName];
if (referenceInfo === undefined) {
return [];
}

const actions: SimpleAction[] = [];
referenceInfo.forEach(info => {
if (info.attribute !== null) {
Array.from(element.ownerDocument.querySelectorAll(`${info.elementQuery}[${info.attribute}="${oldValue}"]`))
.filter(isPublic)
.forEach(element => {
const newElement = cloneElement(element, info.attribute!, newValue);
actions.push({old: {element}, new: {element: newElement}});
})
} else {
Array.from(element.ownerDocument.querySelectorAll(`${info.elementQuery}`))
.filter(element => element.textContent === oldValue)
.filter(isPublic)
.forEach(element => {
const newElement = cloneElementAndTextContent(element, newValue);
actions.push({old: {element}, new: {element: newElement}});
})
}
})
return actions;
}
99 changes: 99 additions & 0 deletions src/wizards/ied.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import {html, TemplateResult} from 'lit-element';
import {get, translate} from 'lit-translate';

import {
cloneElement,
ComplexAction,
EditorAction,
getValue,
isPublic,
Wizard,
WizardActor,
WizardInput,
} from '../foundation.js';
import {patterns} from "./foundation/limits.js";
import {updateReferences} from "./foundation/references.js";

const iedNamePattern = "[A-Za-z][0-9A-Za-z_]{0,2}|" +
"[A-Za-z][0-9A-Za-z_]{4,63}|" +
"[A-MO-Za-z][0-9A-Za-z_]{3}|" +
"N[0-9A-Za-np-z_][0-9A-Za-z_]{2}|" +
"No[0-9A-Za-mo-z_][0-9A-Za-z_]|" +
"Non[0-9A-Za-df-z_]";

export function updateIED(element: Element): WizardActor {
return (inputs: WizardInput[]): EditorAction[] => {
const name = getValue(inputs.find(i => i.label === 'name')!)!;
const oldName = element.getAttribute('name');
const desc = getValue(inputs.find(i => i.label === 'desc')!);

if ( name === oldName &&
desc === element.getAttribute('desc')) {
return [];
}

const complexAction: ComplexAction = {
actions: [],
title: get('ied.action.updateied', {iedName: name}),
};

const newElement = cloneElement(element, { name, desc });
complexAction.actions.push({ old: { element }, new: { element: newElement } });
complexAction.actions.push(...updateReferences(element, oldName, name));
return complexAction.actions.length ? [complexAction] : [];
};
}

export function renderIEDWizard(
name: string | null,
desc: string | null,
reservedNames: string[]
): TemplateResult[] {
return [
html`<wizard-textfield
label="name"
.maybeValue=${name}
helper="${translate('ied.wizard.nameHelper')}"
required
validationMessage="${translate('textfield.required')}"
dialogInitialFocus
.reservedValues=${reservedNames}
pattern="${iedNamePattern}"
></wizard-textfield>`,
html`<wizard-textfield
label="desc"
.maybeValue=${desc}
nullable
helper="${translate('ied.wizard.descHelper')}"
pattern="${patterns.normalizedString}"
></wizard-textfield>`,
];
}

export function reservedNamesIED(currentElement: Element): string[] {
return Array.from(
currentElement.parentNode!.querySelectorAll('IED')
)
.filter(isPublic)
.map(ied => ied.getAttribute('name') ?? '')
.filter(name => name !== currentElement.getAttribute('name'));
}

export function editIEDWizard(element: Element): Wizard {
return [
{
title: get('ied.wizard.title.edit'),
element,
primary: {
icon: 'edit',
label: get('save'),
action: updateIED(element),
},
content: renderIEDWizard(
element.getAttribute('name'),
element.getAttribute('desc'),
reservedNamesIED(element)
),
},
];
}
3 changes: 2 additions & 1 deletion src/wizards/wizard-library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { createSubstationWizard, substationEditWizard } from './substation.js';
import { editTerminalWizard } from './terminal.js';
import { voltageLevelCreateWizard, voltageLevelEditWizard } from './voltagelevel.js';
import { editPowerTransformerWizard } from "./powertransformer.js";
import { editIEDWizard } from "./ied.js";

type SclElementWizard = (element: Element) => Wizard | undefined;

Expand Down Expand Up @@ -252,7 +253,7 @@ export const wizards: Record<
create: emptyWizard,
},
IED: {
edit: emptyWizard,
edit: editIEDWizard,
create: emptyWizard,
},
IEDName: {
Expand Down
13 changes: 13 additions & 0 deletions src/zeroline/ied-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import '../action-icon.js';
import { createClientLnWizard } from '../wizards/clientln.js';
import { gooseIcon, smvIcon } from '../icons.js';
import { newWizardEvent } from '../foundation.js';
import { wizards } from "../wizards/wizard-library.js";
import { selectGseControlWizard } from '../wizards/gsecontrol.js';
import { selectSampledValueControlWizard } from '../wizards/sampledvaluecontrol.js';

Expand All @@ -32,6 +33,11 @@ export class IedEditor extends LitElement {

@query('.connectreport') connectReport!: Fab;

private openEditWizard(): void {
const wizard = wizards['IED'].edit(this.element);
if (wizard) this.dispatchEvent(newWizardEvent(wizard));
}

private openCommunicationMapping(): void {
const sendingIeds = Array.from(
this.element.closest('SCL')?.querySelectorAll('IED') ?? []
Expand Down Expand Up @@ -71,6 +77,13 @@ export class IedEditor extends LitElement {
mini
@click="${() => this.openSmvControlSelection()}"
><mwc-icon slot="icon">${smvIcon}</mwc-icon></mwc-fab
><mwc-fab
slot="action"
class="edit"
mini
@click="${() => this.openEditWizard()}"
icon="edit"
></mwc-fab
></action-icon
> `;
}
Expand Down
Loading

0 comments on commit 110c83d

Please sign in to comment.