Skip to content

Commit 5eb10e9

Browse files
authored
Merge pull request #8 from com-pas/feature/add-dataset-select-ldevice
feat: add LDevice select on create DataSet (#7)
2 parents 8bc2ae8 + 1d639ca commit 5eb10e9

2 files changed

Lines changed: 203 additions & 20 deletions

File tree

editors/dataset/data-set-editor.spec.ts

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,91 @@ const doc = new DOMParser().parseFromString(dataSetDoc, 'application/xml');
2121

2222
describe('DataSet editor component', () => {
2323
let editEvent: SinonSpy;
24+
let editor: DataSetEditor;
2425

2526
beforeEach(async () => {
26-
await fixture(html`<data-set-editor .doc="${doc}"></data-set-editor>`);
27+
editor = await fixture(
28+
html`<data-set-editor .doc="${doc}"></data-set-editor>`
29+
);
2730

2831
editEvent = spy();
2932
window.addEventListener('oscd-edit-v2', editEvent);
3033
});
3134

32-
it('allows to add a new empty DataSet element', async () => {
33-
await sendMouse({ type: 'click', position: [760, 100] });
35+
it('allows to add a new empty DataSet element directly if only one LDevice is available', async () => {
36+
// Prepare document to have IED with only one LDevice
37+
const copyDoc = doc.cloneNode(true) as XMLDocument;
38+
const ied = copyDoc.querySelector('IED[name="IED"]')!;
39+
40+
const lDevices = ied.querySelectorAll(
41+
':scope > AccessPoint > Server > LDevice'
42+
);
43+
lDevices.forEach(ld => {
44+
if (ld.getAttribute('inst') !== 'ldInst1') {
45+
ld.remove();
46+
}
47+
});
48+
49+
editor = await fixture(
50+
html`<data-set-editor .doc="${copyDoc}"></data-set-editor>`
51+
);
52+
53+
// should add new DataSet element to the only available LDevice without opening the LDevice select dialog
54+
const addDataSetListItem = editor.shadowRoot
55+
?.querySelector('action-list')
56+
?.shadowRoot?.querySelector('md-list:nth-child(2)')
57+
?.querySelector('md-list-item') as HTMLElement;
58+
expect(addDataSetListItem).to.exist;
59+
addDataSetListItem.click();
60+
61+
await editor.updateComplete;
62+
await Promise.resolve();
63+
64+
expect(editEvent).to.have.been.calledOnce;
65+
66+
const insert = editEvent.args[0][0].detail.edit;
67+
68+
expect(insert).to.satisfy(isInsert);
69+
expect(insert.parent.tagName).to.equal('LN0');
70+
expect(insert.node.tagName).to.equal('DataSet');
71+
expect(insert.node.getAttribute('name')).to.equal('newDataSet_001');
72+
expect(insert.node.children.length).to.equal(0);
73+
});
74+
75+
it('allows to add a new empty DataSet element to a selected LDevice if multiple LDevices are available', async () => {
76+
// should open LDevice select dialog
77+
const addDataSetListItem = editor.shadowRoot
78+
?.querySelector('action-list')
79+
?.shadowRoot?.querySelector('md-list:nth-child(2)')
80+
?.querySelector('md-list-item') as HTMLElement;
81+
expect(addDataSetListItem).to.exist;
82+
addDataSetListItem.click();
83+
84+
await editor.updateComplete;
85+
await Promise.resolve();
86+
87+
// get the dialog
88+
const dialog = editor.lDeviceSelectDialog;
89+
90+
// get the second lDevice radio button
91+
const radio = dialog
92+
.querySelector('md-list')
93+
?.querySelector('md-list-item:nth-child(2)')!
94+
.querySelector('md-radio') as HTMLElement;
95+
96+
// select second lDevice as target
97+
radio.click();
98+
await editor.updateComplete;
99+
await Promise.resolve();
100+
101+
// click the "Select" button
102+
const selectBtn = dialog.querySelector(
103+
'md-text-button.do.picker.save'
104+
) as HTMLElement;
105+
selectBtn.click();
106+
107+
await editor.updateComplete;
108+
await Promise.resolve();
34109

35110
expect(editEvent).to.have.been.calledOnce;
36111

@@ -61,6 +136,7 @@ describe('DataSet editor component', () => {
61136

62137
const actionList = (el as DataSetEditor).selectionList;
63138
expect(actionList).to.exist;
139+
actionList.items;
64140
expect(actionList.searchValue).to.equal('IED1');
65141
});
66142
});

editors/dataset/data-set-editor.ts

Lines changed: 124 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,26 @@ import {
1818
removeDataSet,
1919
} from '@openenergytools/scl-lib';
2020

21-
import { DataSetElementEditor } from './data-set-element-editor.js';
22-
21+
import { MdDialog } from '@scopedelement/material-web/dialog/MdDialog.js';
22+
import { MdIcon } from '@scopedelement/material-web/icon/MdIcon.js';
23+
import { MdTextButton } from '@scopedelement/material-web/button/MdTextButton.js';
24+
import { MdRadio } from '@scopedelement/material-web/radio/radio.js';
25+
import { MdList } from '@scopedelement/material-web/list/MdList.js';
26+
import { MdListItem } from '@scopedelement/material-web/list/MdListItem.js';
2327
import { pathIdentity, styles } from '../../foundation.js';
28+
import { DataSetElementEditor } from './data-set-element-editor.js';
2429

2530
export class DataSetEditor extends ScopedElementsMixin(LitElement) {
2631
static scopedElements = {
2732
'action-list': ActionList,
33+
'md-text-button': MdTextButton,
2834
'data-set-element-editor': DataSetElementEditor,
2935
'md-outlined-button': MdOutlinedButton,
36+
'md-dialog': MdDialog,
37+
'md-icon': MdIcon,
38+
'md-radio': MdRadio,
39+
'md-list': MdList,
40+
'md-list-item': MdListItem,
3041
};
3142

3243
/** The document being edited as provided to plugins by [[`OpenSCD`]]. */
@@ -42,13 +53,24 @@ export class DataSetEditor extends ScopedElementsMixin(LitElement) {
4253
@state()
4354
selectedDataSet?: Element;
4455

56+
@state()
57+
lDevices: Element[] = []; // lDevices of the currently selected IED, used for the LDevice select dialog
58+
59+
@state()
60+
selectedLDevice: Element | null = null;
61+
62+
@state()
63+
selectedIed: Element | null = null;
64+
4565
@query('.selectionlist') selectionList!: ActionList;
4666

4767
@query('.change.scl.element') selectDataSetButton!: MdOutlinedButton;
4868

4969
@query('data-set-element-editor')
5070
dataSetElementEditor!: DataSetElementEditor;
5171

72+
@query('#ldevice-select') lDeviceSelectDialog!: MdDialog;
73+
5274
/** Resets selected DataSet, if not existing in new doc
5375
update(props: Map<string | number | symbol, unknown>): void {
5476
if (props.has('doc') && this.selectedDataSet) {
@@ -101,28 +123,30 @@ export class DataSetEditor extends ScopedElementsMixin(LitElement) {
101123
{
102124
icon: 'playlist_add',
103125
callback: () => {
104-
const insertDataSet = createDataSet(ied);
105-
if (insertDataSet) {
106-
this.dispatchEvent(
107-
newEditEvent(insertDataSet, { title: `Create New DataSet` })
108-
);
109-
} else {
126+
this.selectedIed = ied;
127+
const lDevices = ied.querySelectorAll(
128+
':scope > AccessPoint > Server > LDevice'
129+
);
130+
if (lDevices.length === 0) {
131+
// LDevice does not exist, cannot create DataSet
110132
const iedName = ied.getAttribute('name');
111-
let reason: string;
112-
const anyLn = ied.querySelector('LN0, LN');
113-
if (!anyLn) {
114-
reason = 'it has no LN0 or LN element';
115-
} else {
116-
reason = 'an unknown validation error occurred';
117-
}
118-
133+
const reason = 'it has no LDevice element';
119134
this.dispatchEvent(
120135
newLogEvent({
121136
title: 'Could not create DataSet',
122137
message: `The DataSet could not be created in IED '${iedName}' because ${reason}.`,
123138
kind: 'warning',
124139
})
125140
);
141+
} else if (lDevices.length === 1) {
142+
// only one LDevice, create DataSet directly
143+
const selectedLDevice = lDevices[0];
144+
this.createDataSet(ied, selectedLDevice);
145+
} else {
146+
// multiple LDevices, show select dialog
147+
this.selectedLDevice = null;
148+
this.lDevices = Array.from(lDevices);
149+
this.lDeviceSelectDialog?.show();
126150
}
127151
},
128152
},
@@ -170,6 +194,36 @@ export class DataSetEditor extends ScopedElementsMixin(LitElement) {
170194
></action-list>`;
171195
}
172196

197+
private createDataSet(ied: Element, targetLDevice: Element): void {
198+
if (!targetLDevice) return;
199+
const ln0 = targetLDevice.querySelector(':scope > LN0');
200+
if (!ln0) return;
201+
202+
const insertDataSet = createDataSet(ln0);
203+
if (insertDataSet) {
204+
this.dispatchEvent(
205+
newEditEvent(insertDataSet, { title: `Create New DataSet` })
206+
);
207+
} else {
208+
const iedName = ied.getAttribute('name');
209+
let reason: string;
210+
const anyLn = ied.querySelector('LN0, LN');
211+
if (!anyLn) {
212+
reason = 'it has no LN0 or LN element';
213+
} else {
214+
reason = 'an unknown validation error occurred';
215+
}
216+
217+
this.dispatchEvent(
218+
newLogEvent({
219+
title: 'Could not create DataSet',
220+
message: `The DataSet could not be created in IED '${iedName}' because ${reason}.`,
221+
kind: 'warning',
222+
})
223+
);
224+
}
225+
}
226+
173227
private renderToggleButton(): TemplateResult {
174228
return html`<md-outlined-button
175229
class="change scl element"
@@ -181,12 +235,65 @@ export class DataSetEditor extends ScopedElementsMixin(LitElement) {
181235
>`;
182236
}
183237

238+
private renderLDeviceSelectDialog(): TemplateResult {
239+
return html` <md-dialog id="ldevice-select">
240+
<div slot="headline">Select LDevice</div>
241+
242+
<div slot="content">
243+
<p style="color: var(--mdc-theme-on-surface, #333); font-size: 0.95em;">
244+
Choose the LDevice to which the new DataSet will be added.
245+
</p>
246+
<form>
247+
<md-list role="radiogroup">
248+
${this.lDevices.map(
249+
(ld, i) => html`
250+
<md-list-item>
251+
<md-radio
252+
id="ldevice${i}"
253+
name="ldevice"
254+
value="${ld.getAttribute('inst')}"
255+
?checked=${this.selectedLDevice?.getAttribute('inst') ===
256+
ld.getAttribute('inst')}
257+
@change=${() => this.selectLDevice(ld)}
258+
></md-radio>
259+
<label for="ldevice${i}">${ld.getAttribute('inst')}</label>
260+
</md-list-item>
261+
`
262+
)}
263+
</md-list>
264+
</form>
265+
</div>
266+
267+
<div slot="actions">
268+
<md-text-button @click=${() => this.lDeviceSelectDialog?.close()}
269+
>Close</md-text-button
270+
>
271+
<md-text-button
272+
class="do picker save"
273+
?disabled=${!this.selectedLDevice}
274+
@click=${() => this.handleLDeviceSelect()}
275+
>Select<md-icon slot="icon">check</md-icon></md-text-button
276+
>
277+
</div>
278+
</md-dialog>`;
279+
}
280+
281+
private selectLDevice(ld: Element | null): void {
282+
this.selectedLDevice = ld;
283+
}
284+
285+
private handleLDeviceSelect(): void {
286+
this.lDeviceSelectDialog?.close();
287+
if (!this.selectedLDevice || !this.selectedIed) return;
288+
this.createDataSet(this.selectedIed, this.selectedLDevice);
289+
}
290+
184291
render(): TemplateResult {
185292
if (!this.doc) return html`<div>No SCL loaded</div>`;
186293

187294
return html`${this.renderToggleButton()}
188295
<div class="section">
189-
${this.renderSelectionList()}${this.renderElementEditorContainer()}
296+
${this.renderSelectionList()}${this.renderElementEditorContainer()}${this.renderLDeviceSelectDialog()}
190297
</div>`;
191298
}
192299

0 commit comments

Comments
 (0)