Skip to content

Commit c6d5102

Browse files
author
Dennis Labordus
authored
bug(wizards): Update attributes of Terminal when updating Voltage Level/Bay name. (openscd#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.
1 parent 52a49ce commit c6d5102

21 files changed

+677
-214
lines changed

src/translations/de.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,9 @@ export const de: Translations = {
258258
edit: 'Spannungsebene bearbeiten',
259259
},
260260
},
261+
action: {
262+
updateVoltagelevel: 'Spannungsebene "{{name}}" bearbeitet',
263+
},
261264
},
262265
bay: {
263266
name: 'Feld',
@@ -269,6 +272,9 @@ export const de: Translations = {
269272
edit: 'Feld bearbeiten',
270273
},
271274
},
275+
action: {
276+
updateBay: 'Feld "{{name}}" bearbeitet',
277+
},
272278
},
273279
conductingequipment: {
274280
name: 'Primärelement',

src/translations/en.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,9 @@ export const en = {
255255
edit: 'Edit voltage level',
256256
},
257257
},
258+
action: {
259+
updateVoltagelevel: 'Edited voltagelevel "{{name}}"',
260+
},
258261
},
259262
bay: {
260263
name: 'Bay',
@@ -266,6 +269,9 @@ export const en = {
266269
edit: 'Edit bay',
267270
},
268271
},
272+
action: {
273+
updateBay: 'Edited bay "{{name}}"',
274+
},
269275
},
270276
conductingequipment: {
271277
name: 'Conducting Equipment',

src/wizards/bay.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
WizardActor,
1111
WizardInputElement,
1212
} from '../foundation.js';
13-
import { updateNamingAction } from './foundation/actions.js';
13+
import { replaceNamingAttributeWithReferencesAction } from './foundation/actions.js';
1414

1515
export function renderBayWizard(name: string | null, desc: string | null): TemplateResult[] {
1616
return [
@@ -74,7 +74,7 @@ export function editBayWizard(element: Element): Wizard {
7474
primary: {
7575
icon: 'edit',
7676
label: get('save'),
77-
action: updateNamingAction(element),
77+
action: replaceNamingAttributeWithReferencesAction(element, 'bay.action.updateBay'),
7878
},
7979
content: renderBayWizard(
8080
element.getAttribute('name'),

src/wizards/conductingequipment.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
WizardActor,
1616
WizardInputElement,
1717
} from '../foundation.js';
18-
import { updateNamingAction } from './foundation/actions.js';
18+
import { replaceNamingAction } from './foundation/actions.js';
1919

2020
const types: Partial<Record<string, string>> = {
2121
// standard
@@ -313,7 +313,7 @@ export function editConductingEquipmentWizard(element: Element): Wizard {
313313
primary: {
314314
icon: 'edit',
315315
label: get('save'),
316-
action: updateNamingAction(element),
316+
action: replaceNamingAction(element),
317317
},
318318
content: renderConductingEquipmentWizard(
319319
element.getAttribute('name'),

src/wizards/foundation/actions.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
import { get } from "lit-translate";
1111
import { updateReferences } from "./references.js";
1212

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

31+
export function replaceNamingAttributeWithReferencesAction(
32+
element: Element,
33+
messageTitleKey: string
34+
): WizardActor {
35+
return (inputs: WizardInputElement[]): EditorAction[] => {
36+
const newName = getValue(inputs.find(i => i.label === 'name')!)!;
37+
const oldName = element.getAttribute('name');
38+
const newDesc = getValue(inputs.find(i => i.label === 'desc')!);
39+
40+
if (
41+
newName === oldName &&
42+
newDesc === element.getAttribute('desc')
43+
) {
44+
return [];
45+
}
46+
47+
const newElement = cloneElement(element, { name: newName, desc: newDesc });
48+
49+
const complexAction: ComplexAction = {
50+
actions: [],
51+
title: get(messageTitleKey, {name: newName}),
52+
};
53+
complexAction.actions.push({ old: { element }, new: { element: newElement } });
54+
complexAction.actions.push(...updateReferences(element, oldName, newName));
55+
return complexAction.actions.length ? [complexAction] : [];
56+
};
57+
}
58+
3159
export function updateNamingAttributeWithReferencesAction(
3260
element: Element,
3361
messageTitleKey: string

src/wizards/foundation/references.ts

Lines changed: 136 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,132 @@
1-
import {isPublic, SimpleAction} from "../../foundation.js";
1+
import {
2+
isPublic,
3+
Replace
4+
} from "../../foundation.js";
25

3-
const referenceInfoTags = ['IED', 'Substation'] as const;
6+
const referenceInfoTags = ['IED', 'Substation', 'VoltageLevel', 'Bay'] as const;
47
type ReferencesInfoTag = typeof referenceInfoTags[number];
58

9+
type FilterFunction = (element: Element, attributeName: string | null, oldName: string | null) => string;
10+
611
/*
712
* For every supported tag a list of information about which elements to search for and which attribute value
813
* to replace with the new value typed in the screen by the user. This is used to update references to a name
914
* of an element by other elements.
10-
* If the attribute is null the text content of the found element will be replaced.
15+
* If the attributeName is null the text content of the found element will be replaced.
1116
*/
1217
const referenceInfos: Record<
1318
ReferencesInfoTag,
1419
{
15-
elementQuery: string;
16-
attribute: string | null;
20+
attributeName: string | null;
21+
filter: FilterFunction;
1722
}[]
1823
> = {
1924
IED:
2025
[{
21-
elementQuery: `Association`,
22-
attribute: 'iedName'
26+
attributeName: 'iedName',
27+
filter: simpleAttributeFilter(`Association`)
2328
}, {
24-
elementQuery: `ClientLN`,
25-
attribute: 'iedName'
29+
attributeName: 'iedName',
30+
filter: simpleAttributeFilter(`ClientLN`)
2631
}, {
27-
elementQuery: `ConnectedAP`,
28-
attribute: 'iedName'
32+
attributeName: 'iedName',
33+
filter: simpleAttributeFilter(`ConnectedAP`)
2934
}, {
30-
elementQuery: `ExtRef`,
31-
attribute: 'iedName'
35+
attributeName: 'iedName',
36+
filter: simpleAttributeFilter(`ExtRef`)
3237
}, {
33-
elementQuery: `KDC`,
34-
attribute: 'iedName'
38+
attributeName: 'iedName',
39+
filter: simpleAttributeFilter(`KDC`)
3540
}, {
36-
elementQuery: `LNode`,
37-
attribute: 'iedName'
41+
attributeName: 'iedName',
42+
filter: simpleAttributeFilter(`LNode`)
3843
}, {
39-
elementQuery: `GSEControl > IEDName`,
40-
attribute: null
44+
attributeName: null,
45+
filter: simpleTextContentFilter(`GSEControl > IEDName`)
4146
}, {
42-
elementQuery: `SampledValueControl > IEDName`,
43-
attribute: null
47+
attributeName: null,
48+
filter: simpleTextContentFilter(`SampledValueControl > IEDName`)
4449
}],
4550
Substation:
4651
[{
47-
elementQuery: `Terminal`,
48-
attribute: 'substationName'
49-
}]
52+
attributeName: 'substationName',
53+
filter: simpleAttributeFilter(`Terminal`)
54+
}],
55+
VoltageLevel:
56+
[{
57+
attributeName: 'voltageLevelName',
58+
filter: attributeFilterWithParentNameAttribute(`Terminal`,
59+
{'Substation': 'substationName'})
60+
}],
61+
Bay:
62+
[{
63+
attributeName: 'bayName',
64+
filter: attributeFilterWithParentNameAttribute(`Terminal`,
65+
{'Substation': 'substationName', 'VoltageLevel': 'voltageLevelName'})
66+
}],
5067
}
5168

69+
/**
70+
* Simple function to create a filter to find Elements where the value of an attribute equals the old name.
71+
*
72+
* @param tagName - The tagName of the elements to search for.
73+
*/
74+
function simpleAttributeFilter(tagName: string) {
75+
return function filter(element: Element, attributeName: string | null, oldName: string | null): string {
76+
return `${tagName}[${attributeName}="${oldName}"]`;
77+
}
78+
}
79+
80+
/**
81+
* Simple function to search for Elements for which the text content may contain the old name.
82+
* Because the text content of an element can't be search for in a CSS Selector this is done afterwards.
83+
*
84+
* @param elementQuery - The CSS Query to search for the Elements.
85+
*/
86+
function simpleTextContentFilter(elementQuery: string) {
87+
return function filter(): string {
88+
return `${elementQuery}`;
89+
}
90+
}
91+
92+
/**
93+
* More complex function to search for elements for which the value of an attribute needs to be updated.
94+
* To find the correct element the name of a parent element also needs to be included in the search.
95+
*
96+
* For instance when the name of a Bay is updated only the terminals need to be updated where of course
97+
* the old name of the bay is the value of the attribute 'bayName', but also the voltage level and substation
98+
* name need to be included, because the name of the bay is only unique within the voltage level.
99+
* The query will then become
100+
* `Terminal[substationName="<substationName>"][voltageLevelName="<voltageLevelName>"][bayName="<oldName>"]`
101+
*
102+
* @param tagName - The tagName of the elements to search for.
103+
* @param parentInfo - The records of parent to search for, the key is the tagName of the parent, the value
104+
* is the name of the attribuet to use in the query.
105+
*/
106+
function attributeFilterWithParentNameAttribute(tagName: string, parentInfo: Record<string, string>) {
107+
return function filter(element: Element, attributeName: string | null, oldName: string | null): string {
108+
return `${tagName}${Object.entries(parentInfo)
109+
.map(([parentTag, parentAttribute]) => {
110+
const parentElement = element.closest(parentTag);
111+
if (parentElement && parentElement.hasAttribute('name')) {
112+
const name = parentElement.getAttribute('name');
113+
return `[${parentAttribute}="${name}"]`;
114+
}
115+
return null;
116+
}).join('') // Join the strings to 1 string without a separator.
117+
}[${attributeName}="${oldName}"]`;
118+
}
119+
}
120+
121+
/**
122+
* Clone an element with the attribute name passed and process the new value. If the new value
123+
* is null the attribute will be removed otherwise the value of the attribute is updated.
124+
*
125+
* @param element - The element to clone.
126+
* @param attributeName - The name of the attribute to copy.
127+
* @param value - The value to set on the cloned element or if null remove the attribute.
128+
* @returns Returns the cloned element.
129+
*/
52130
function cloneElement(element: Element, attributeName: string, value: string | null): Element {
53131
const newElement = <Element>element.cloneNode(false);
54132
if (value === null) {
@@ -59,14 +137,33 @@ function cloneElement(element: Element, attributeName: string, value: string | n
59137
return newElement;
60138
}
61139

140+
/**
141+
* Clone an element and set the value as text content on the cloned element.
142+
*
143+
* @param element - The element to clone.
144+
* @param value - The value to set.
145+
* @returns Returns the cloned element.
146+
*/
62147
function cloneElementAndTextContent(element: Element, value: string | null): Element {
63148
const newElement = <Element>element.cloneNode(false);
64149
newElement.textContent = value;
65150
return newElement;
66151
}
67152

68-
export function updateReferences(element: Element, oldValue: string | null, newValue: string): SimpleAction[] {
69-
if (oldValue === newValue) {
153+
/**
154+
* Function to create Replace actions to update reference which point to the name of the element being updated.
155+
* For instance the IED Name is used in other SCL Elements as attribute 'iedName' to reference the IED.
156+
* These attribute values need to be updated if the name of the IED changes.
157+
*
158+
* An empty array will be returned if the old and new value are the same or no references need to be updated.
159+
*
160+
* @param element - The element for which the name is updated.
161+
* @param oldName - The old name of the element.
162+
* @param newName - The new name of the element.
163+
* @returns Returns a list of Replace Actions that can be added to a Complex Action or returned directly for execution.
164+
*/
165+
export function updateReferences(element: Element, oldName: string | null, newName: string): Replace[] {
166+
if (oldName === newName) {
70167
return [];
71168
}
72169

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

78-
const actions: SimpleAction[] = [];
175+
const actions: Replace[] = [];
79176
referenceInfo.forEach(info => {
80-
if (info.attribute !== null) {
81-
Array.from(element.ownerDocument.querySelectorAll(`${info.elementQuery}[${info.attribute}="${oldValue}"]`))
177+
// Depending on if an attribute value needs to be updated or the text content of an element
178+
// different scenarios need to be executed.
179+
if (info.attributeName) {
180+
const filter = info.filter(element, info.attributeName, oldName);
181+
Array.from(element.ownerDocument.querySelectorAll(`${filter}`))
82182
.filter(isPublic)
83183
.forEach(element => {
84-
const newElement = cloneElement(element, info.attribute!, newValue);
184+
const newElement = cloneElement(element, info.attributeName!, newName);
85185
actions.push({old: {element}, new: {element: newElement}});
86186
})
87187
} else {
88-
Array.from(element.ownerDocument.querySelectorAll(`${info.elementQuery}`))
89-
.filter(element => element.textContent === oldValue)
188+
// If the text content needs to be updated, filter on the text content can't be done in a CSS Selector.
189+
// So we query all elements the may need to be updated and filter them afterwards.
190+
const filter = info.filter(element, info.attributeName, oldName);
191+
Array.from(element.ownerDocument.querySelectorAll(`${filter}`))
192+
.filter(element => element.textContent === oldName)
90193
.filter(isPublic)
91194
.forEach(element => {
92-
const newElement = cloneElementAndTextContent(element, newValue);
195+
const newElement = cloneElementAndTextContent(element, newName);
93196
actions.push({old: {element}, new: {element: newElement}});
94197
})
95198
}

src/wizards/powertransformer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
WizardInputElement,
1212
} from '../foundation.js';
1313

14-
import { updateNamingAction } from "./foundation/actions.js";
14+
import { replaceNamingAction } from "./foundation/actions.js";
1515

1616
const defaultPowerTransformerType = 'PTR';
1717

@@ -103,7 +103,7 @@ export function editPowerTransformerWizard(element: Element): Wizard {
103103
primary: {
104104
icon: 'edit',
105105
label: get('save'),
106-
action: updateNamingAction(element),
106+
action: replaceNamingAction(element),
107107
},
108108
content: renderPowerTransformerWizard(
109109
element.getAttribute('name'),

0 commit comments

Comments
 (0)