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

bug(wizards): Update attributes of Terminal when updating Voltage Level/Bay name. #712

Merged
merged 7 commits into from
May 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
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
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