Skip to content

Commit 659aa8e

Browse files
Rob TjalmaDennis Labordus
andauthored
feat(settings): load nsdoc to local storage (openscd#502)
* Implemented adding document to localStorage * Refactor * Fix unit tests * Adding unit tests * Refactor * Refactor translation * Fixing snapshot test * Review comments * Added version text for NSDocs * Review comments * Merge with stash * feat(wizards/settings): Added Nsdoc plugin + Show LN names (openscd#516) * Added first version nsdoc plugin * Added plugin * Refactoring * Refactoring with Jakob * Added LN name * Removed multiple usages of DOMParser * Added refactored nsdoc foundation * Added tests * Changed name of testfile to lowercase * Fixed double names * review comments * Review comment Co-authored-by: Dennis Labordus <[email protected]>
1 parent 469aa38 commit 659aa8e

File tree

20 files changed

+847
-13
lines changed

20 files changed

+847
-13
lines changed

src/Plugging.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { TextField } from '@material/mwc-textfield';
2626
import { ifImplemented, LitElementConstructor, Mixin } from './foundation.js';
2727
import { EditingElement } from './Editing.js';
2828
import { officialPlugins } from '../public/js/plugins.js';
29+
import { initializeNsdoc } from './foundation/nsdoc.js';
2930

3031
type PluginKind = 'editor' | 'menu' | 'validator';
3132
const menuPosition = ['top', 'middle', 'bottom'] as const;
@@ -208,6 +209,7 @@ export function Plugging<TBase extends new (...args: any[]) => EditingElement>(
208209
.docName=${this.docName}
209210
.docId=${this.docId}
210211
.pluginId=${plugin.src}
212+
.nsdoc=${await initializeNsdoc()}
211213
></${loadedPlugins.get(plugin.src)}>`;
212214
},
213215
};

src/Setting.ts

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,42 @@
11
import { html, property, query, TemplateResult } from 'lit-element';
2-
import { registerTranslateConfig, translate, use } from 'lit-translate';
2+
import { get, registerTranslateConfig, translate, use } from 'lit-translate';
33

44
import '@material/mwc-button';
55
import '@material/mwc-dialog';
66
import '@material/mwc-formfield';
77
import '@material/mwc-list/mwc-list-item';
88
import '@material/mwc-select';
99
import '@material/mwc-switch';
10+
1011
import { Dialog } from '@material/mwc-dialog';
1112
import { Select } from '@material/mwc-select';
1213
import { Switch } from '@material/mwc-switch';
1314

14-
import { ifImplemented, LitElementConstructor, Mixin } from './foundation.js';
15+
import { ifImplemented, LitElementConstructor, Mixin, newLogEvent } from './foundation.js';
1516
import { Language, languages, loader } from './translations/loader.js';
17+
18+
import './WizardDivider.js';
1619
import { WizardDialog } from './wizard-dialog.js';
1720

1821
export type Settings = {
1922
language: Language;
2023
theme: 'light' | 'dark';
2124
mode: 'safe' | 'pro';
2225
showieds: 'on' | 'off';
26+
'IEC 61850-7-2': string | undefined;
27+
'IEC 61850-7-3': string | undefined;
28+
'IEC 61850-7-4': string | undefined;
29+
'IEC 61850-8-1': string | undefined;
2330
};
2431
export const defaults: Settings = {
2532
language: 'en',
2633
theme: 'light',
2734
mode: 'safe',
2835
showieds: 'off',
36+
'IEC 61850-7-2': undefined,
37+
'IEC 61850-7-3': undefined,
38+
'IEC 61850-7-4': undefined,
39+
'IEC 61850-8-1': undefined
2940
};
3041

3142
/** Mixin that saves [[`Settings`]] to `localStorage`, reflecting them in the
@@ -42,6 +53,10 @@ export function Setting<TBase extends LitElementConstructor>(Base: TBase) {
4253
theme: this.getSetting('theme'),
4354
mode: this.getSetting('mode'),
4455
showieds: this.getSetting('showieds'),
56+
'IEC 61850-7-2': this.getSetting('IEC 61850-7-2'),
57+
'IEC 61850-7-3': this.getSetting('IEC 61850-7-3'),
58+
'IEC 61850-7-4': this.getSetting('IEC 61850-7-4'),
59+
'IEC 61850-8-1': this.getSetting('IEC 61850-8-1')
4560
};
4661
}
4762

@@ -56,11 +71,15 @@ export function Setting<TBase extends LitElementConstructor>(Base: TBase) {
5671
@query('#showieds')
5772
showiedsUI!: Switch;
5873

74+
@query('#nsdoc-file')
75+
private nsdocFileUI!: HTMLInputElement;
76+
5977
private getSetting<T extends keyof Settings>(setting: T): Settings[T] {
6078
return (
6179
<Settings[T] | null>localStorage.getItem(setting) ?? defaults[setting]
6280
);
6381
}
82+
6483
/** Update the `value` of `setting`, storing to `localStorage`. */
6584
setSetting<T extends keyof Settings>(setting: T, value: Settings[T]): void {
6685
localStorage.setItem(setting, <string>(<unknown>value));
@@ -70,6 +89,15 @@ export function Setting<TBase extends LitElementConstructor>(Base: TBase) {
7089
this.requestUpdate();
7190
}
7291

92+
/** Remove the `setting` in `localStorage`. */
93+
removeSetting<T extends keyof Settings>(setting: T): void {
94+
localStorage.removeItem(setting);
95+
this.shadowRoot
96+
?.querySelector<WizardDialog>('wizard-dialog')
97+
?.requestUpdate();
98+
this.requestUpdate();
99+
}
100+
73101
private onClosing(ae: CustomEvent<{ action: string } | null>): void {
74102
if (ae.detail?.action === 'reset') {
75103
Object.keys(this.settings).forEach(item =>
@@ -90,6 +118,78 @@ export function Setting<TBase extends LitElementConstructor>(Base: TBase) {
90118
if (changedProperties.has('settings')) use(this.settings.language);
91119
}
92120

121+
private renderFileSelect(): TemplateResult {
122+
return html `
123+
<input id="nsdoc-file" accept=".nsdoc" type="file" hidden required multiple
124+
@change=${(evt: Event) => this.loadNsdocFile(evt)}}>
125+
<mwc-button label="${translate('settings.selectFileButton')}"
126+
id="selectFileButton"
127+
@click=${() => {
128+
const input = <HTMLInputElement | null>this.shadowRoot!.querySelector("#nsdoc-file");
129+
input?.click();
130+
}}>
131+
</mwc-button>
132+
`;
133+
}
134+
135+
private async loadNsdocFile(evt: Event): Promise<void> {
136+
const files = Array.from(
137+
(<HTMLInputElement | null>evt.target)?.files ?? []
138+
);
139+
140+
if (files.length == 0) return;
141+
files.forEach(async file => {
142+
const text = await file.text();
143+
const id = this.parseToXmlObject(text).querySelector('NSDoc')?.getAttribute('id');
144+
if (!id) {
145+
document
146+
.querySelector('open-scd')!
147+
.dispatchEvent(
148+
newLogEvent({ kind: 'error', title: get('settings.invalidFileNoIdFound') })
149+
);
150+
return;
151+
}
152+
153+
this.setSetting(id as keyof Settings, text);
154+
})
155+
156+
this.nsdocFileUI.value = '';
157+
this.requestUpdate();
158+
}
159+
160+
/**
161+
* Render one .nsdoc item in the Settings wizard
162+
* @param key - The key of the nsdoc file in the settings.
163+
* @returns a .nsdoc item for the Settings wizard
164+
*/
165+
private renderNsdocItem<T extends keyof Settings>(key: T): TemplateResult {
166+
const nsdSetting = this.settings[key];
167+
let nsdVersion: string | undefined | null;
168+
let nsdRevision: string | undefined | null;
169+
let nsdRelease: string | undefined | null;
170+
171+
if (nsdSetting) {
172+
const nsdoc = this.parseToXmlObject(nsdSetting)!.querySelector('NSDoc');
173+
nsdVersion = nsdoc?.getAttribute('version');
174+
nsdRevision = nsdoc?.getAttribute('revision');
175+
nsdRelease = nsdoc?.getAttribute('release');
176+
}
177+
178+
return html`<mwc-list-item id=${key} graphic="avatar" hasMeta twoline .disabled=${!nsdSetting}>
179+
<span>${key}</span>
180+
${nsdSetting ? html`<span slot="secondary">${nsdVersion}${nsdRevision}${nsdRelease}</span>` :
181+
html``}
182+
${nsdSetting ? html`<mwc-icon slot="graphic" style="color:green;">done</mwc-icon>` :
183+
html`<mwc-icon slot="graphic" style="color:red;">close</mwc-icon>`}
184+
${nsdSetting ? html`<mwc-icon id="deleteNsdocItem" slot="meta" @click=${() => {this.removeSetting(key)}}>delete</mwc-icon>` :
185+
html``}
186+
</mwc-list-item>`;
187+
}
188+
189+
private parseToXmlObject(text: string): XMLDocument {
190+
return new DOMParser().parseFromString(text, 'application/xml');
191+
}
192+
93193
constructor(...params: any[]) {
94194
super(...params);
95195

@@ -140,6 +240,17 @@ export function Setting<TBase extends LitElementConstructor>(Base: TBase) {
140240
></mwc-switch>
141241
</mwc-formfield>
142242
</form>
243+
<wizard-divider></wizard-divider>
244+
<section>
245+
<h3>${translate('settings.loadNsdTranslations')}</h3>
246+
${this.renderFileSelect()}
247+
</section>
248+
<mwc-list id="nsdocList">
249+
${this.renderNsdocItem('IEC 61850-7-2')}
250+
${this.renderNsdocItem('IEC 61850-7-3')}
251+
${this.renderNsdocItem('IEC 61850-7-4')}
252+
${this.renderNsdocItem('IEC 61850-8-1')}
253+
</mwc-list>
143254
<mwc-button slot="secondaryAction" dialogAction="close">
144255
${translate('cancel')}
145256
</mwc-button>

src/WizardDivider.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import {css, customElement, html, LitElement, TemplateResult} from "lit-element";
2+
3+
@customElement('wizard-divider')
4+
export class WizardDividerElement extends LitElement {
5+
render(): TemplateResult {
6+
return html `
7+
<div role="separator"></div>
8+
`
9+
}
10+
11+
static styles = css`
12+
div {
13+
height: 0px;
14+
margin: 10px 0px 10px 0px;
15+
border-top: none;
16+
border-right: none;
17+
border-left: none;
18+
border-image: initial;
19+
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
20+
}
21+
`
22+
}

src/editors/IED.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import './ied/ied-container.js'
1010
import { translate } from 'lit-translate';
1111
import { SingleSelectedEvent } from '@material/mwc-list/mwc-list-foundation';
1212
import { compareNames, getDescriptionAttribute, getNameAttribute } from '../foundation.js';
13+
import { Nsdoc } from '../foundation/nsdoc.js';
1314

1415
/*
1516
* We need a variable outside the plugin to save the selected IED, because the Plugin is created
@@ -31,6 +32,10 @@ export default class IedPlugin extends LitElement {
3132
@property()
3233
doc!: XMLDocument;
3334

35+
/** All the nsdoc files that are being uploaded via the settings. */
36+
@property()
37+
nsdoc!: Nsdoc;
38+
3439
private get alphabeticOrderedIeds() : Element[] {
3540
return (this.doc)
3641
? Array.from(this.doc.querySelectorAll(':root > IED'))
@@ -81,7 +86,10 @@ export default class IedPlugin extends LitElement {
8186
</mwc-list-item>`
8287
)}
8388
</mwc-select>
84-
<ied-container .element=${selectedIedElement}></ied-container>
89+
<ied-container
90+
.element=${selectedIedElement}
91+
.nsdoc=${this.nsdoc}
92+
></ied-container>
8593
</section>`;
8694
}
8795
return html `

src/editors/ied/access-point-container.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,17 @@ import '../../action-pane.js';
1111
import './server-container.js'
1212
import { nothing } from 'lit-html';
1313
import { getDescriptionAttribute, getNameAttribute } from '../../foundation.js';
14+
import { Nsdoc } from '../../foundation/nsdoc.js';
1415

1516
/** [[`IED`]] plugin subeditor for editing `AccessPoint` element. */
1617
@customElement('access-point-container')
1718
export class AccessPointContainer extends LitElement {
1819
@property({ attribute: false })
1920
element!: Element;
2021

22+
@property()
23+
nsdoc!: Nsdoc;
24+
2125
private header(): TemplateResult {
2226
const name = getNameAttribute(this.element);
2327
const desc = getDescriptionAttribute(this.element);
@@ -30,6 +34,7 @@ export class AccessPointContainer extends LitElement {
3034
${Array.from(this.element.querySelectorAll(':scope > Server')).map(
3135
server => html`<server-container
3236
.element=${server}
37+
.nsdoc=${this.nsdoc}
3338
></server-container>`)}
3439
</action-pane>`;
3540
}

src/editors/ied/ied-container.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ import { translate } from "lit-translate";
1111

1212
import {wizards} from "../../wizards/wizard-library.js";
1313
import '../../action-pane.js';
14-
import {getDescriptionAttribute, getNameAttribute, newWizardEvent} from '../../foundation.js';
1514
import './access-point-container.js';
15+
import { Nsdoc } from '../../foundation/nsdoc.js';
16+
import { getDescriptionAttribute, getNameAttribute, newWizardEvent } from '../../foundation.js';
1617

1718
/** [[`IED`]] plugin subeditor for editing `IED` element. */
1819
@customElement('ied-container')
@@ -21,6 +22,9 @@ export class IedContainer extends LitElement {
2122
@property({ attribute: false })
2223
element!: Element;
2324

25+
@property()
26+
nsdoc!: Nsdoc;
27+
2428
private openEditWizard(): void {
2529
const wizard = wizards['IED'].edit(this.element);
2630
if (wizard) this.dispatchEvent(newWizardEvent(wizard));
@@ -45,6 +49,7 @@ export class IedContainer extends LitElement {
4549
${Array.from(this.element.querySelectorAll(':scope > AccessPoint')).map(
4650
ap => html`<access-point-container
4751
.element=${ap}
52+
.nsdoc=${this.nsdoc}
4853
></access-point-container>`)}
4954
</action-pane>`;
5055
}

src/editors/ied/ldevice-container.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,16 @@ import { nothing } from 'lit-html';
1414
import { getDescriptionAttribute, getInstanceAttribute, getNameAttribute } from '../../foundation.js';
1515
import { IconButtonToggle } from '@material/mwc-icon-button-toggle';
1616
import { translate } from 'lit-translate';
17+
import { Nsdoc } from '../../foundation/nsdoc.js';
1718

1819
/** [[`IED`]] plugin subeditor for editing `LDevice` element. */
1920
@customElement('ldevice-container')
2021
export class LDeviceContainer extends LitElement {
2122
@property({ attribute: false })
2223
element!: Element;
24+
25+
@property()
26+
nsdoc!: Nsdoc;
2327

2428
@query('#toggleButton') toggleButton!: IconButtonToggle | undefined;
2529

@@ -48,8 +52,9 @@ export class LDeviceContainer extends LitElement {
4852
></mwc-icon-button-toggle>
4953
</abbr>` : nothing}
5054
<div id="lnContainer">
51-
${this.toggleButton?.on ? lnElements.map(server => html`<ln-container
52-
.element=${server}
55+
${this.toggleButton?.on ? lnElements.map(ln => html`<ln-container
56+
.element=${ln}
57+
.nsdoc=${this.nsdoc}
5358
></ln-container>
5459
`) : nothing}
5560
</div>

src/editors/ied/ln-container.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,28 @@ import './do-container.js';
1414
import { getInstanceAttribute, getNameAttribute } from '../../foundation.js';
1515
import { translate } from 'lit-translate';
1616
import { IconButtonToggle } from '@material/mwc-icon-button-toggle';
17+
import { until } from 'lit-html/directives/until';
18+
import { Nsdoc } from '../../foundation/nsdoc.js';
1719

1820
/** [[`IED`]] plugin subeditor for editing `LN` and `LN0` element. */
1921
@customElement('ln-container')
2022
export class LNContainer extends LitElement {
2123
@property({ attribute: false })
2224
element!: Element;
25+
26+
@property()
27+
nsdoc!: Nsdoc;
2328

2429
@query('#toggleButton') toggleButton!: IconButtonToggle | undefined;
2530

26-
private header(): TemplateResult {
31+
private async header(): Promise<TemplateResult> {
2732
const prefix = this.element.getAttribute('prefix');
28-
const lnClass = this.element.getAttribute('lnClass');
2933
const inst = getInstanceAttribute(this.element);
3034

35+
const data = this.nsdoc.getDataDescription(this.element);
36+
3137
return html`${prefix != null ? html`${prefix} &mdash; ` : nothing}
32-
${lnClass}
38+
${data.label}
3339
${inst ? html` &mdash; ${inst}` : nothing}`;
3440
}
3541

@@ -59,7 +65,7 @@ export class LNContainer extends LitElement {
5965
render(): TemplateResult {
6066
const doElements = this.getDOElements();
6167

62-
return html`<action-pane .label="${this.header()}">
68+
return html`<action-pane .label="${until(this.header())}">
6369
${doElements.length > 0 ? html`<abbr slot="action" title="${translate('iededitor.toggleChildElements')}">
6470
<mwc-icon-button-toggle
6571
id="toggleButton"

0 commit comments

Comments
 (0)