Skip to content

Commit 6983caf

Browse files
authored
Merge pull request #1 from openscd/feat/copy-control-blocks
feat: Copy control blocks
2 parents 91ed07b + 1f1ed18 commit 6983caf

File tree

42 files changed

+1014
-93
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1014
-93
lines changed

editors/base-element-editor.ts

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,25 @@ import {
1414
identity,
1515
} from '@openenergytools/scl-lib';
1616
import '@openenergytools/filterable-lists/dist/action-list.js';
17+
import {
18+
isFCDACompatibleWithIED,
19+
queryLDevice,
20+
queryLN,
21+
} from '../foundation/utils/xml.js';
22+
23+
// eslint-disable-next-line no-shadow
24+
export enum ControlBlockCopyStatus {
25+
CanCopy = 'CanCopy',
26+
IEDStructureIncompatible = 'IEDStructureIncompatible',
27+
ControlBlockOrDataSetAlreadyExists = 'ControlBlockOrDataSetAlreadyExists',
28+
}
29+
30+
export interface ControlBlockCopyOption {
31+
ied: Element;
32+
control: Element;
33+
status: ControlBlockCopyStatus;
34+
selected: boolean;
35+
}
1736

1837
export class BaseElementEditor extends ScopedElementsMixin(LitElement) {
1938
/** The document being edited as provided to plugins by [[`OpenSCD`]]. */
@@ -30,12 +49,21 @@ export class BaseElementEditor extends ScopedElementsMixin(LitElement) {
3049
@state()
3150
selectedDataSet?: Element | null;
3251

52+
@state()
53+
controlBlockCopyOptions: ControlBlockCopyOption[] = [];
54+
3355
@query('.dialog.select') selectDataSetDialog!: MdDialog;
3456

57+
@query('.dialog.copy') copyControlBlockDialog!: MdDialog;
58+
3559
@query('.new.dataset') newDataSet!: MdIconButton;
3660

3761
@query('.change.dataset') changeDataSet!: MdIconButton;
3862

63+
get hasCopyControlSelected(): boolean {
64+
return this.controlBlockCopyOptions.some(o => o.selected);
65+
}
66+
3967
protected selectDataSet(dataSet: Element): void {
4068
const name = dataSet.getAttribute('name');
4169
if (!name || !this.selectCtrlBlock) return;
@@ -54,6 +82,130 @@ export class BaseElementEditor extends ScopedElementsMixin(LitElement) {
5482
this.selectDataSetDialog.close();
5583
}
5684

85+
protected queryIEDs(): Element[] {
86+
return Array.from(this.doc.querySelectorAll(':root > IED'));
87+
}
88+
89+
// eslint-disable-next-line class-methods-use-this
90+
protected queryLnForControl(ied: Element, control: Element): Element | null {
91+
const lDevice = control.closest('LDevice');
92+
const lnOrLn0 = control.closest('LN0, LN');
93+
94+
if (!lnOrLn0 || !lDevice) {
95+
throw new Error('ControlBlock must be a child of LN or LN0 and LDevice');
96+
}
97+
98+
const ldInst = lDevice.getAttribute('inst') ?? '';
99+
const lDeviceInIed = queryLDevice(ied, ldInst);
100+
101+
if (!lDeviceInIed) {
102+
return null;
103+
}
104+
105+
const lnClass = lnOrLn0.getAttribute('lnClass') ?? '';
106+
const inst = lnOrLn0.getAttribute('inst') ?? '';
107+
const prefix = lnOrLn0.getAttribute('prefix');
108+
109+
return queryLN(lDeviceInIed, lnClass, inst, prefix);
110+
}
111+
112+
// eslint-disable-next-line class-methods-use-this
113+
protected getDataSet(control: Element): Element | null {
114+
return (
115+
control.parentElement?.querySelector(
116+
`DataSet[name="${control.getAttribute('datSet')}"]`
117+
) ?? null
118+
);
119+
}
120+
121+
// eslint-disable-next-line class-methods-use-this
122+
protected getCopyControlBlockCopyStatus(
123+
controlBlock: Element,
124+
otherIED: Element
125+
): ControlBlockCopyStatus {
126+
const ln = this.queryLnForControl(otherIED, controlBlock);
127+
128+
if (!ln) {
129+
return ControlBlockCopyStatus.IEDStructureIncompatible;
130+
}
131+
132+
const controlInOtherIED = ln.querySelector(
133+
`${controlBlock.tagName}[name="${controlBlock.getAttribute('name')}"]`
134+
);
135+
const hasControl = Boolean(controlInOtherIED);
136+
137+
const dataSet = this.getDataSet(controlBlock);
138+
139+
if (!dataSet) {
140+
throw new Error('ControlBlock has no DataSet');
141+
}
142+
143+
const hasDataSet =
144+
dataSet !== null &&
145+
Boolean(
146+
ln.querySelector(`DataSet[name="${dataSet?.getAttribute('name')}"]`)
147+
);
148+
149+
if (hasDataSet || hasControl) {
150+
return ControlBlockCopyStatus.ControlBlockOrDataSetAlreadyExists;
151+
}
152+
153+
const fcdas = Array.from(dataSet.querySelectorAll('FCDA'));
154+
for (const fcda of fcdas) {
155+
const isCompatible = isFCDACompatibleWithIED(fcda, otherIED);
156+
157+
if (!isCompatible) {
158+
return ControlBlockCopyStatus.IEDStructureIncompatible;
159+
}
160+
}
161+
162+
return ControlBlockCopyStatus.CanCopy;
163+
}
164+
165+
protected copyControlBlock(): void {
166+
const selectedOptions = this.controlBlockCopyOptions.filter(
167+
o => o.selected
168+
);
169+
170+
if (selectedOptions.length === 0) {
171+
this.copyControlBlockDialog.close();
172+
return;
173+
}
174+
175+
const inserts = selectedOptions.flatMap(o => {
176+
const ln = this.queryLnForControl(o.ied, o.control);
177+
const dataSet = this.getDataSet(o.control);
178+
179+
if (!ln || !dataSet) {
180+
throw new Error('ControlBlock or DataSet not found');
181+
}
182+
183+
const controlCopy = o.control.cloneNode(true) as Element;
184+
const controlInsert = {
185+
parent: ln,
186+
node: controlCopy,
187+
reference: null,
188+
};
189+
190+
const dataSetCcopy = dataSet.cloneNode(true) as Element;
191+
const dataSetInsert = {
192+
parent: ln,
193+
node: dataSetCcopy,
194+
reference: null,
195+
};
196+
197+
return [controlInsert, dataSetInsert];
198+
});
199+
200+
this.dispatchEvent(
201+
newEditEvent(inserts, {
202+
title: `Copy control block to ${selectedOptions.length} IEDs`,
203+
})
204+
);
205+
206+
this.copyControlBlockDialog.close();
207+
}
208+
57209
private addNewDataSet(control: Element): void {
58210
const parent = control.parentElement;
59211
if (!parent) return;
@@ -79,6 +231,20 @@ export class BaseElementEditor extends ScopedElementsMixin(LitElement) {
79231
this.selectDataSetDialog.show();
80232
}
81233

234+
// eslint-disable-next-line class-methods-use-this
235+
private getCopyStatusText(status: ControlBlockCopyStatus): string {
236+
switch (status) {
237+
case ControlBlockCopyStatus.CanCopy:
238+
return 'Copy possible';
239+
case ControlBlockCopyStatus.IEDStructureIncompatible:
240+
return 'IED structure incompatible';
241+
case ControlBlockCopyStatus.ControlBlockOrDataSetAlreadyExists:
242+
return 'Control block or data set already exists';
243+
default:
244+
return '';
245+
}
246+
}
247+
82248
protected renderSelectDataSetDialog(): TemplateResult {
83249
const items = Array.from(
84250
this.selectCtrlBlock?.parentElement?.querySelectorAll(
@@ -97,6 +263,52 @@ export class BaseElementEditor extends ScopedElementsMixin(LitElement) {
97263
</md-dialog>`;
98264
}
99265

266+
protected renderCopyControlBlockDialog(): TemplateResult {
267+
return html`<md-dialog
268+
class="dialog copy"
269+
@close=${() => {
270+
this.controlBlockCopyOptions = [];
271+
}}
272+
>
273+
<div slot="content" class="copy-option-list">
274+
${this.controlBlockCopyOptions.map(
275+
option =>
276+
html` <label class="copy-optin-row">
277+
<div class="copy-option-description">
278+
<div class="copy-option-description-ied">
279+
${option.ied.getAttribute('name')}
280+
</div>
281+
<div class="copy-option-description-status">
282+
${this.getCopyStatusText(option.status)}
283+
</div>
284+
</div>
285+
<md-checkbox
286+
?checked=${option.selected}
287+
@change=${() => {
288+
// eslint-disable-next-line no-param-reassign
289+
option.selected = !option.selected;
290+
this.requestUpdate();
291+
}}
292+
?disabled=${option.status !== ControlBlockCopyStatus.CanCopy}
293+
>
294+
</md-checkbox>
295+
</label>`
296+
)}
297+
<div class="copy-button">
298+
<md-outlined-button
299+
@click=${() => this.copyControlBlockDialog.close()}
300+
>Close</md-outlined-button
301+
>
302+
<md-outlined-button
303+
@click=${this.copyControlBlock}
304+
?disabled=${!this.hasCopyControlSelected}
305+
>Copy</md-outlined-button
306+
>
307+
</div>
308+
</div>
309+
</md-dialog>`;
310+
}
311+
100312
protected renderDataSetElementContainer(): TemplateResult {
101313
return html`
102314
<div class="content dataSet">

editors/gsecontrol/gse-control-editor.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ describe('GSEControl editor component', () => {
4242
});
4343

4444
it('allows to insert new GSEControl element', async () => {
45-
await sendMouse({ type: 'click', position: [760, 100] });
45+
await sendMouse({ type: 'click', position: [688, 100] });
4646

4747
expect(editEvent).to.have.been.calledOnce;
4848
expect(editEvent.args[0][0].detail.edit[0]).to.satisfy(isInsert);

0 commit comments

Comments
 (0)