Skip to content

Commit 1c457cf

Browse files
authored
feat: Add oscd api with plugin state (openscd#1696)
1 parent f5f3fc4 commit 1c457cf

File tree

7 files changed

+198
-48
lines changed

7 files changed

+198
-48
lines changed

docs/core-api/oscd-api.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# OSCD API
2+
3+
Open scd passes an API object as the property `oscdApi` to every plugin. At the moment the API only includes the plugin state API. Here is an example usage in a Lit based plugin.
4+
5+
```
6+
import { OscdApi } from '@openscd/core';
7+
8+
class SomePlugin extends LitElement {
9+
10+
@property()
11+
oscdApi: OscdApi | null = null;
12+
13+
connectedCallback() {
14+
const pluginState = this.oscdApi?.pluginState.getState();
15+
16+
...
17+
}
18+
19+
disconnectedCallback() {
20+
this.oscdApi?.pluginState.setState(someStateObject);
21+
}
22+
}
23+
```
24+
25+
⚠️ Be aware that not every open scd distribution provides this API, so your plugin should have a null check if you want it to be compatible with other distributions.
26+
27+
## Plugin state API
28+
29+
The plugin state API stores an arbitrary object as your plugin's state in memory. Be aware that this state is only persisted during the open scd distribution's runtime and will not be stored in local storage for example.
30+
31+
```
32+
interface PluginStateApi {
33+
setState(state: PluginState | null): void;
34+
35+
getState(): PluginState | null;
36+
37+
updateState(partialState: Partial<PluginState>): void
38+
}
39+
```
40+

packages/core/api/api.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { PluginStateApi } from './plugin-state-api.js';
2+
3+
export class OscdApi {
4+
public pluginState: PluginStateApi;
5+
6+
constructor(pluginTag: string) {
7+
this.pluginState = new PluginStateApi(pluginTag);
8+
}
9+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
type PluginState = {
2+
[key: string]: unknown
3+
}
4+
5+
export class PluginStateApi {
6+
private static state: { [tag: string]: PluginState | null } = {};
7+
private pluginTag: string;
8+
9+
constructor(tag: string) {
10+
this.pluginTag = tag;
11+
}
12+
13+
public setState(state: PluginState | null): void {
14+
this.setPluginState(state);
15+
}
16+
17+
public getState(): PluginState | null {
18+
return this.getPluginState();
19+
}
20+
21+
public updateState(partialState: Partial<PluginState>): void {
22+
const pluginState = this.getPluginState();
23+
const patchedState = {
24+
...pluginState,
25+
...partialState
26+
};
27+
this.setPluginState(patchedState);
28+
}
29+
30+
private setPluginState(state: PluginState | null): void {
31+
PluginStateApi.state[this.pluginTag] = state;
32+
}
33+
34+
private getPluginState(): PluginState | null {
35+
return PluginStateApi.state[this.pluginTag] ?? null;
36+
}
37+
}

packages/core/foundation.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,5 @@ export function crossProduct<T>(...arrays: T[][]): T[][] {
6666
[[]]
6767
);
6868
}
69+
70+
export { OscdApi } from './api/api.js';

packages/openscd/src/open-scd.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import type {
4545
Plugin as CorePlugin,
4646
EditCompletedEvent,
4747
} from '@openscd/core';
48+
import { OscdApi } from '@openscd/core';
4849

4950
import { HistoryState, historyStateEvent } from './addons/History.js';
5051

@@ -431,6 +432,7 @@ export class OpenSCD extends LitElement {
431432
.nsdoc=${this.nsdoc}
432433
.docs=${this.docs}
433434
.locale=${this.locale}
435+
.oscdApi=${new OscdApi(tag)}
434436
class="${classMap({
435437
plugin: true,
436438
menu: plugin.kind === 'menu',

packages/plugins/src/editors/IED.ts

Lines changed: 80 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -26,26 +26,30 @@ import {
2626
} from '@openscd/open-scd/src/foundation.js';
2727
import { Nsdoc } from '@openscd/open-scd/src/foundation/nsdoc.js';
2828
import { getIcon } from '@openscd/open-scd/src/icons/icons.js';
29+
import { OscdApi } from '@openscd/core';
2930

3031
/** An editor [[`plugin`]] for editing the `IED` section. */
3132
export default class IedPlugin extends LitElement {
3233
/** The document being edited as provided to plugins by [[`OpenSCD`]]. */
3334
@property()
3435
doc!: XMLDocument;
35-
36+
3637
@property({ type: Number })
3738
editCount = -1;
38-
39+
3940
/** All the nsdoc files that are being uploaded via the settings. */
4041
@property()
4142
nsdoc!: Nsdoc;
42-
43+
44+
@property()
45+
oscdApi: OscdApi | null = null;
46+
4347
@state()
4448
selectedIEDs: string[] = [];
45-
49+
4650
@state()
4751
selectedLNClasses: string[] = [];
48-
52+
4953
@state()
5054
private get iedList(): Element[] {
5155
return this.doc
@@ -61,26 +65,26 @@ export default class IedPlugin extends LitElement {
6165
const uniqueLNClassList: string[] = [];
6266
if (currentIed) {
6367
return Array.from(currentIed.querySelectorAll('LN0, LN'))
64-
.filter(element => element.hasAttribute('lnClass'))
65-
.filter(element => {
66-
const lnClass = element.getAttribute('lnClass') ?? '';
67-
if (uniqueLNClassList.includes(lnClass)) {
68-
return false;
69-
}
70-
uniqueLNClassList.push(lnClass);
71-
return true;
72-
})
73-
.sort((a, b) => {
74-
const aLnClass = a.getAttribute('lnClass') ?? '';
75-
const bLnClass = b.getAttribute('lnClass') ?? '';
76-
77-
return aLnClass.localeCompare(bLnClass);
78-
})
79-
.map(element => {
80-
const lnClass = element.getAttribute('lnClass');
81-
const label = this.nsdoc.getDataDescription(element).label;
82-
return [lnClass, label];
83-
}) as string[][];
68+
.filter(element => element.hasAttribute('lnClass'))
69+
.filter(element => {
70+
const lnClass = element.getAttribute('lnClass') ?? '';
71+
if (uniqueLNClassList.includes(lnClass)) {
72+
return false;
73+
}
74+
uniqueLNClassList.push(lnClass);
75+
return true;
76+
})
77+
.sort((a, b) => {
78+
const aLnClass = a.getAttribute('lnClass') ?? '';
79+
const bLnClass = b.getAttribute('lnClass') ?? '';
80+
81+
return aLnClass.localeCompare(bLnClass);
82+
})
83+
.map(element => {
84+
const lnClass = element.getAttribute('lnClass');
85+
const label = this.nsdoc.getDataDescription(element).label;
86+
return [lnClass, label];
87+
}) as string[][];
8488
}
8589
return [];
8690
}
@@ -101,6 +105,16 @@ export default class IedPlugin extends LitElement {
101105

102106
lNClassListOpenedOnce = false;
103107

108+
connectedCallback(): void {
109+
super.connectedCallback();
110+
this.loadPluginState();
111+
}
112+
113+
disconnectedCallback(): void {
114+
super.disconnectedCallback();
115+
this.storePluginState();
116+
}
117+
104118
protected updated(_changedProperties: PropertyValues): void {
105119
super.updated(_changedProperties);
106120

@@ -132,6 +146,23 @@ export default class IedPlugin extends LitElement {
132146
}
133147
}
134148

149+
private loadPluginState(): void {
150+
const stateApi = this.oscdApi?.pluginState;
151+
const selectedIEDs: string[] | null = (stateApi?.getState()?.selectedIEDs as string[]) ?? null;
152+
153+
if (selectedIEDs) {
154+
this.onSelectionChange(selectedIEDs);
155+
}
156+
}
157+
158+
private storePluginState(): void {
159+
const stateApi = this.oscdApi?.pluginState;
160+
161+
if (stateApi) {
162+
stateApi.setState({ selectedIEDs: this.selectedIEDs });
163+
}
164+
}
165+
135166
private calcSelectedLNClasses(): string[] {
136167
const somethingSelected = this.selectedLNClasses.length > 0;
137168
const lnClasses = this.lnClassList.map(lnClassInfo => lnClassInfo[0]);
@@ -147,6 +178,29 @@ export default class IedPlugin extends LitElement {
147178
return selectedLNClasses;
148179
}
149180

181+
private onSelectionChange(selectedIeds: string[]): void {
182+
const equalArrays = <T>(first: T[], second: T[]): boolean => {
183+
return (
184+
first.length === second.length &&
185+
first.every((val, index) => val === second[index])
186+
);
187+
};
188+
189+
const selectionChanged = !equalArrays(
190+
this.selectedIEDs,
191+
selectedIeds
192+
);
193+
194+
if (!selectionChanged) {
195+
return;
196+
}
197+
198+
this.lNClassListOpenedOnce = false;
199+
this.selectedIEDs = selectedIeds;
200+
this.selectedLNClasses = [];
201+
this.requestUpdate('selectedIed');
202+
}
203+
150204
render(): TemplateResult {
151205
const iedList = this.iedList;
152206
if (iedList.length > 0) {
@@ -158,28 +212,7 @@ export default class IedPlugin extends LitElement {
158212
id="iedFilter"
159213
icon="developer_board"
160214
.header=${get('iededitor.iedSelector')}
161-
@selected-items-changed="${(e: SelectedItemsChangedEvent) => {
162-
const equalArrays = <T>(first: T[], second: T[]): boolean => {
163-
return (
164-
first.length === second.length &&
165-
first.every((val, index) => val === second[index])
166-
);
167-
};
168-
169-
const selectionChanged = !equalArrays(
170-
this.selectedIEDs,
171-
e.detail.selectedItems
172-
);
173-
174-
if (!selectionChanged) {
175-
return;
176-
}
177-
178-
this.lNClassListOpenedOnce = false;
179-
this.selectedIEDs = e.detail.selectedItems;
180-
this.selectedLNClasses = [];
181-
this.requestUpdate('selectedIed');
182-
}}"
215+
@selected-items-changed="${(e: SelectedItemsChangedEvent) => this.onSelectionChange(e.detail.selectedItems)}"
183216
>
184217
${iedList.map(ied => {
185218
const name = getNameAttribute(ied);

packages/plugins/test/integration/editors/IED.test.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import { LNContainer } from '../../../src/editors/ied/ln-container.js';
1717
import { DOContainer } from '../../../src/editors/ied/do-container.js';
1818
import { DAContainer } from '../../../src/editors/ied/da-container.js';
1919
import { MockOpenSCD } from '@openscd/open-scd/test/mock-open-scd.js';
20+
import { OscdApi } from '@openscd/core';
21+
import { PluginStateApi } from '../../../../core/dist/api/plugin-state-api.js';
2022

2123
describe('IED Plugin', () => {
2224
if (customElements.get('ied-plugin') === undefined)
@@ -42,6 +44,7 @@ describe('IED Plugin', () => {
4244

4345
describe('with a doc loaded', () => {
4446
let doc: XMLDocument;
47+
let oscdApi: OscdApi;
4548

4649
describe('containing no IEDs', () => {
4750
beforeEach(async () => {
@@ -106,9 +109,11 @@ describe('IED Plugin', () => {
106109
.then(response => response.text())
107110
.then(str => new DOMParser().parseFromString(str, 'application/xml'));
108111
nsdoc = await initializeNsdoc();
112+
oscdApi = new OscdApi('IED');
113+
oscdApi.pluginState.setState(null);
109114
parent = await fixture(
110115
html`<mock-open-scd
111-
><ied-plugin .doc="${doc}" .nsdoc="${nsdoc}"></ied-plugin
116+
><ied-plugin .doc="${doc}" .nsdoc="${nsdoc}" .oscdApi=${oscdApi}></ied-plugin
112117
></mock-open-scd>`
113118
);
114119
element = parent.getActivePlugin();
@@ -370,6 +375,28 @@ describe('IED Plugin', () => {
370375
primaryButton.click();
371376
await element.updateComplete;
372377
}
378+
379+
describe('load and store selected IEDs', () => {
380+
it('should store selected IEDs on disconnected', async () => {
381+
await selectIed('IED3');
382+
element.disconnectedCallback();
383+
384+
const api = new OscdApi('IED');
385+
expect(api.pluginState.getState()).to.deep.equal({ selectedIEDs: ['IED3'] });
386+
});
387+
});
388+
389+
describe('with stored plugin state', () => {
390+
beforeEach(() => {
391+
oscdApi.pluginState.setState({ selectedIEDs: ['IED3'] });
392+
});
393+
394+
it('should load previously saved IED', () => {
395+
element.connectedCallback();
396+
397+
expect(element.selectedIEDs).to.deep.equal(['IED3']);
398+
});
399+
});
373400
});
374401
});
375402

0 commit comments

Comments
 (0)