Skip to content

Commit

Permalink
bug(wizards): Update attributes of Terminal when updating Voltage Lev…
Browse files Browse the repository at this point in the history
…el/Bay name. (#712)

* Update attributes of Terminal when updating VoltageLevel/Bay name.
* Added extra reference tests.
* Small change.
* Improved version that will include parent names when needed to search for matching references.
* Fixed review comment.
  • Loading branch information
Dennis Labordus authored May 9, 2022
1 parent 52a49ce commit c6d5102
Show file tree
Hide file tree
Showing 21 changed files with 677 additions and 214 deletions.
6 changes: 6 additions & 0 deletions src/translations/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,9 @@ export const de: Translations = {
edit: 'Spannungsebene bearbeiten',
},
},
action: {
updateVoltagelevel: 'Spannungsebene "{{name}}" bearbeitet',
},
},
bay: {
name: 'Feld',
Expand All @@ -269,6 +272,9 @@ export const de: Translations = {
edit: 'Feld bearbeiten',
},
},
action: {
updateBay: 'Feld "{{name}}" bearbeitet',
},
},
conductingequipment: {
name: 'Primärelement',
Expand Down
6 changes: 6 additions & 0 deletions src/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,9 @@ export const en = {
edit: 'Edit voltage level',
},
},
action: {
updateVoltagelevel: 'Edited voltagelevel "{{name}}"',
},
},
bay: {
name: 'Bay',
Expand All @@ -266,6 +269,9 @@ export const en = {
edit: 'Edit bay',
},
},
action: {
updateBay: 'Edited bay "{{name}}"',
},
},
conductingequipment: {
name: 'Conducting Equipment',
Expand Down
4 changes: 2 additions & 2 deletions src/wizards/bay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
WizardActor,
WizardInputElement,
} from '../foundation.js';
import { updateNamingAction } from './foundation/actions.js';
import { replaceNamingAttributeWithReferencesAction } from './foundation/actions.js';

export function renderBayWizard(name: string | null, desc: string | null): TemplateResult[] {
return [
Expand Down Expand Up @@ -74,7 +74,7 @@ export function editBayWizard(element: Element): Wizard {
primary: {
icon: 'edit',
label: get('save'),
action: updateNamingAction(element),
action: replaceNamingAttributeWithReferencesAction(element, 'bay.action.updateBay'),
},
content: renderBayWizard(
element.getAttribute('name'),
Expand Down
4 changes: 2 additions & 2 deletions src/wizards/conductingequipment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
WizardActor,
WizardInputElement,
} from '../foundation.js';
import { updateNamingAction } from './foundation/actions.js';
import { replaceNamingAction } from './foundation/actions.js';

const types: Partial<Record<string, string>> = {
// standard
Expand Down Expand Up @@ -313,7 +313,7 @@ export function editConductingEquipmentWizard(element: Element): Wizard {
primary: {
icon: 'edit',
label: get('save'),
action: updateNamingAction(element),
action: replaceNamingAction(element),
},
content: renderConductingEquipmentWizard(
element.getAttribute('name'),
Expand Down
30 changes: 29 additions & 1 deletion src/wizards/foundation/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
import { get } from "lit-translate";
import { updateReferences } from "./references.js";

export function updateNamingAction(element: Element): WizardActor {
export function replaceNamingAction(element: Element): WizardActor {
return (inputs: WizardInputElement[]): EditorAction[] => {
const name = getValue(inputs.find(i => i.label === 'name')!)!;
const desc = getValue(inputs.find(i => i.label === 'desc')!);
Expand All @@ -28,6 +28,34 @@ export function updateNamingAction(element: Element): WizardActor {
};
}

export function replaceNamingAttributeWithReferencesAction(
element: Element,
messageTitleKey: string
): WizardActor {
return (inputs: WizardInputElement[]): EditorAction[] => {
const newName = getValue(inputs.find(i => i.label === 'name')!)!;
const oldName = element.getAttribute('name');
const newDesc = getValue(inputs.find(i => i.label === 'desc')!);

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

const newElement = cloneElement(element, { name: newName, desc: newDesc });

const complexAction: ComplexAction = {
actions: [],
title: get(messageTitleKey, {name: newName}),
};
complexAction.actions.push({ old: { element }, new: { element: newElement } });
complexAction.actions.push(...updateReferences(element, oldName, newName));
return complexAction.actions.length ? [complexAction] : [];
};
}

export function updateNamingAttributeWithReferencesAction(
element: Element,
messageTitleKey: string
Expand Down
169 changes: 136 additions & 33 deletions src/wizards/foundation/references.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,132 @@
import {isPublic, SimpleAction} from "../../foundation.js";
import {
isPublic,
Replace
} from "../../foundation.js";

const referenceInfoTags = ['IED', 'Substation'] as const;
const referenceInfoTags = ['IED', 'Substation', 'VoltageLevel', 'Bay'] as const;
type ReferencesInfoTag = typeof referenceInfoTags[number];

type FilterFunction = (element: Element, attributeName: string | null, oldName: string | null) => string;

/*
* 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.
* If the attributeName is null the text content of the found element will be replaced.
*/
const referenceInfos: Record<
ReferencesInfoTag,
{
elementQuery: string;
attribute: string | null;
attributeName: string | null;
filter: FilterFunction;
}[]
> = {
IED:
[{
elementQuery: `Association`,
attribute: 'iedName'
attributeName: 'iedName',
filter: simpleAttributeFilter(`Association`)
}, {
elementQuery: `ClientLN`,
attribute: 'iedName'
attributeName: 'iedName',
filter: simpleAttributeFilter(`ClientLN`)
}, {
elementQuery: `ConnectedAP`,
attribute: 'iedName'
attributeName: 'iedName',
filter: simpleAttributeFilter(`ConnectedAP`)
}, {
elementQuery: `ExtRef`,
attribute: 'iedName'
attributeName: 'iedName',
filter: simpleAttributeFilter(`ExtRef`)
}, {
elementQuery: `KDC`,
attribute: 'iedName'
attributeName: 'iedName',
filter: simpleAttributeFilter(`KDC`)
}, {
elementQuery: `LNode`,
attribute: 'iedName'
attributeName: 'iedName',
filter: simpleAttributeFilter(`LNode`)
}, {
elementQuery: `GSEControl > IEDName`,
attribute: null
attributeName: null,
filter: simpleTextContentFilter(`GSEControl > IEDName`)
}, {
elementQuery: `SampledValueControl > IEDName`,
attribute: null
attributeName: null,
filter: simpleTextContentFilter(`SampledValueControl > IEDName`)
}],
Substation:
[{
elementQuery: `Terminal`,
attribute: 'substationName'
}]
attributeName: 'substationName',
filter: simpleAttributeFilter(`Terminal`)
}],
VoltageLevel:
[{
attributeName: 'voltageLevelName',
filter: attributeFilterWithParentNameAttribute(`Terminal`,
{'Substation': 'substationName'})
}],
Bay:
[{
attributeName: 'bayName',
filter: attributeFilterWithParentNameAttribute(`Terminal`,
{'Substation': 'substationName', 'VoltageLevel': 'voltageLevelName'})
}],
}

/**
* Simple function to create a filter to find Elements where the value of an attribute equals the old name.
*
* @param tagName - The tagName of the elements to search for.
*/
function simpleAttributeFilter(tagName: string) {
return function filter(element: Element, attributeName: string | null, oldName: string | null): string {
return `${tagName}[${attributeName}="${oldName}"]`;
}
}

/**
* Simple function to search for Elements for which the text content may contain the old name.
* Because the text content of an element can't be search for in a CSS Selector this is done afterwards.
*
* @param elementQuery - The CSS Query to search for the Elements.
*/
function simpleTextContentFilter(elementQuery: string) {
return function filter(): string {
return `${elementQuery}`;
}
}

/**
* More complex function to search for elements for which the value of an attribute needs to be updated.
* To find the correct element the name of a parent element also needs to be included in the search.
*
* For instance when the name of a Bay is updated only the terminals need to be updated where of course
* the old name of the bay is the value of the attribute 'bayName', but also the voltage level and substation
* name need to be included, because the name of the bay is only unique within the voltage level.
* The query will then become
* `Terminal[substationName="<substationName>"][voltageLevelName="<voltageLevelName>"][bayName="<oldName>"]`
*
* @param tagName - The tagName of the elements to search for.
* @param parentInfo - The records of parent to search for, the key is the tagName of the parent, the value
* is the name of the attribuet to use in the query.
*/
function attributeFilterWithParentNameAttribute(tagName: string, parentInfo: Record<string, string>) {
return function filter(element: Element, attributeName: string | null, oldName: string | null): string {
return `${tagName}${Object.entries(parentInfo)
.map(([parentTag, parentAttribute]) => {
const parentElement = element.closest(parentTag);
if (parentElement && parentElement.hasAttribute('name')) {
const name = parentElement.getAttribute('name');
return `[${parentAttribute}="${name}"]`;
}
return null;
}).join('') // Join the strings to 1 string without a separator.
}[${attributeName}="${oldName}"]`;
}
}

/**
* Clone an element with the attribute name passed and process the new value. If the new value
* is null the attribute will be removed otherwise the value of the attribute is updated.
*
* @param element - The element to clone.
* @param attributeName - The name of the attribute to copy.
* @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 {
const newElement = <Element>element.cloneNode(false);
if (value === null) {
Expand All @@ -59,14 +137,33 @@ function cloneElement(element: Element, attributeName: string, value: string | n
return newElement;
}

/**
* Clone an element and set the value as text content on the cloned element.
*
* @param element - The element to clone.
* @param value - The value to set.
* @returns Returns the cloned element.
*/
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) {
/**
* Function to create Replace actions to update reference which point to the name of the element being updated.
* For instance the IED Name is used in other SCL Elements as attribute 'iedName' to reference the IED.
* These attribute values need to be updated if the name of the IED changes.
*
* An empty array will be returned if the old and new value are the same or no references need to be updated.
*
* @param element - The element for which the name is updated.
* @param oldName - The old name of the element.
* @param newName - The new name of the element.
* @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) {
return [];
}

Expand All @@ -75,21 +172,27 @@ export function updateReferences(element: Element, oldValue: string | null, newV
return [];
}

const actions: SimpleAction[] = [];
const actions: Replace[] = [];
referenceInfo.forEach(info => {
if (info.attribute !== null) {
Array.from(element.ownerDocument.querySelectorAll(`${info.elementQuery}[${info.attribute}="${oldValue}"]`))
// Depending on if an attribute value needs to be updated or the text content of an element
// different scenarios need to be executed.
if (info.attributeName) {
const filter = info.filter(element, info.attributeName, oldName);
Array.from(element.ownerDocument.querySelectorAll(`${filter}`))
.filter(isPublic)
.forEach(element => {
const newElement = cloneElement(element, info.attribute!, newValue);
const newElement = cloneElement(element, info.attributeName!, newName);
actions.push({old: {element}, new: {element: newElement}});
})
} else {
Array.from(element.ownerDocument.querySelectorAll(`${info.elementQuery}`))
.filter(element => element.textContent === oldValue)
// 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)
.forEach(element => {
const newElement = cloneElementAndTextContent(element, newValue);
const newElement = cloneElementAndTextContent(element, newName);
actions.push({old: {element}, new: {element: newElement}});
})
}
Expand Down
4 changes: 2 additions & 2 deletions src/wizards/powertransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
WizardInputElement,
} from '../foundation.js';

import { updateNamingAction } from "./foundation/actions.js";
import { replaceNamingAction } from "./foundation/actions.js";

const defaultPowerTransformerType = 'PTR';

Expand Down Expand Up @@ -103,7 +103,7 @@ export function editPowerTransformerWizard(element: Element): Wizard {
primary: {
icon: 'edit',
label: get('save'),
action: updateNamingAction(element),
action: replaceNamingAction(element),
},
content: renderPowerTransformerWizard(
element.getAttribute('name'),
Expand Down
Loading

0 comments on commit c6d5102

Please sign in to comment.