Skip to content

Commit 0099ede

Browse files
authored
feat: add LNodeType preview (#12)
1 parent 752b591 commit 0099ede

File tree

7 files changed

+255
-8
lines changed

7 files changed

+255
-8
lines changed

components/preview-dialog.spec.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { expect, fixture, html } from '@open-wc/testing';
2+
import { PreviewDialog } from './preview-dialog.js';
3+
4+
customElements.define('preview-dialog', PreviewDialog);
5+
6+
const tree = {
7+
DO1: {
8+
name: 'DO1',
9+
tagName: 'DataObject',
10+
type: 'SPS',
11+
descID: 'desc1',
12+
presCond: 'O',
13+
children: {
14+
DA1: {
15+
name: 'DA1',
16+
tagName: 'DataAttribute',
17+
type: 'BOOLEAN',
18+
descID: 'descDA1',
19+
presCond: 'M',
20+
children: {},
21+
},
22+
},
23+
},
24+
DO2: {
25+
name: 'DO2',
26+
tagName: 'DataObject',
27+
type: 'INS',
28+
descID: 'desc2',
29+
presCond: 'O',
30+
children: {},
31+
},
32+
};
33+
34+
describe('PreviewDialog', () => {
35+
let element: PreviewDialog;
36+
37+
beforeEach(async () => {
38+
element = await fixture(html`<preview-dialog></preview-dialog>`);
39+
});
40+
41+
it('renders the selected LNodeType', async () => {
42+
element.lNodeType = 'LPHD';
43+
element.tree = tree;
44+
element.selection = {
45+
DO1: { DA1: {} },
46+
DO2: {},
47+
};
48+
expect(element.xmlContent).to.include('<LNodeType');
49+
expect(element.xmlContent).to.include('LPHD');
50+
expect(element.xmlContent).to.include('DO1');
51+
expect(element.xmlContent).to.include('DA1');
52+
expect(element.xmlContent).to.include('DO2');
53+
});
54+
55+
it('shows a message if no data is selected', async () => {
56+
element.lNodeType = '';
57+
element.tree = undefined;
58+
element.selection = {};
59+
await element.updateComplete;
60+
expect(element.xmlContent).to.include('No data selected for preview');
61+
});
62+
});

components/preview-dialog.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/* eslint-disable @typescript-eslint/no-unused-vars */
2+
import { ScopedElementsMixin } from '@open-wc/scoped-elements/lit-element.js';
3+
import { LitElement, html, css } from 'lit';
4+
import { query, property } from 'lit/decorators.js';
5+
import { MdDialog } from '@scopedelement/material-web/dialog/dialog.js';
6+
import { MdTextButton } from '@scopedelement/material-web/button/text-button.js';
7+
import AceEditor from 'ace-custom-element';
8+
import {
9+
insertSelectedLNodeType,
10+
LNodeDescription,
11+
} from '@openenergytools/scl-lib';
12+
import { TreeSelection } from '@openenergytools/tree-grid';
13+
import { createBaseSCLDoc, serializeAndFormat } from '../foundation.js';
14+
15+
const aceTheme = `solarized_${localStorage.getItem('theme') || 'light'}`;
16+
17+
export class PreviewDialog extends ScopedElementsMixin(LitElement) {
18+
static scopedElements = {
19+
'md-dialog': MdDialog,
20+
'md-text-button': MdTextButton,
21+
'ace-editor': AceEditor,
22+
};
23+
24+
@query('md-dialog')
25+
dialog!: MdDialog;
26+
27+
@query('ace-editor')
28+
aceEditor!: AceEditor;
29+
30+
@property({ type: Object })
31+
selection: TreeSelection = {};
32+
33+
@property({ type: Object })
34+
tree: LNodeDescription | undefined = undefined;
35+
36+
@property({ type: String })
37+
lNodeType: string = '';
38+
39+
get xmlContent(): string {
40+
if (!this.selection || !this.tree || !this.lNodeType) {
41+
return '<!-- No data selected for preview -->';
42+
}
43+
try {
44+
const doc = createBaseSCLDoc();
45+
const inserts = insertSelectedLNodeType(doc, this.selection, {
46+
class: this.lNodeType,
47+
data: this.tree,
48+
});
49+
inserts.forEach(insert => {
50+
if (insert.parent && insert.node) {
51+
insert.parent.appendChild(insert.node);
52+
}
53+
});
54+
return serializeAndFormat(doc);
55+
} catch (error) {
56+
return `<!-- Error generating preview: ${error} -->`;
57+
}
58+
}
59+
60+
show() {
61+
this.dialog?.show();
62+
}
63+
64+
render() {
65+
return html`
66+
<md-dialog @closed=${() => this.dialog?.close()}>
67+
<div slot="headline">Preview LNodeType</div>
68+
<div slot="content">
69+
<ace-editor
70+
mode="ace/mode/xml"
71+
theme=${`ace/theme/${aceTheme}`}
72+
wrap
73+
style="width: 80vw;"
74+
.value=${this.xmlContent}
75+
readonly
76+
></ace-editor>
77+
</div>
78+
<div slot="actions">
79+
<md-text-button @click=${() => this.dialog.close()} type="button"
80+
>Close</md-text-button
81+
>
82+
</div>
83+
</md-dialog>
84+
`;
85+
}
86+
87+
static styles = css`
88+
md-dialog {
89+
--md-dialog-container-max-width: 90vw;
90+
max-width: 90vw;
91+
max-height: 100vh;
92+
}
93+
[slot='content'] {
94+
padding: 12px;
95+
}
96+
ace-editor {
97+
height: calc(100vh - 240px);
98+
box-sizing: border-box;
99+
}
100+
md-text-button {
101+
text-transform: uppercase;
102+
}
103+
`;
104+
}

foundation.spec.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { expect } from '@open-wc/testing';
2-
import { getSelectionByPath, processEnums } from './foundation.js';
2+
import {
3+
getSelectionByPath,
4+
processEnums,
5+
serializeAndFormat,
6+
} from './foundation.js';
37

48
describe('foundation.js', () => {
59
describe('getSelectionByPath', () => {
@@ -90,4 +94,30 @@ describe('foundation.js', () => {
9094
expect(result.parent.child).to.have.property('deepEnum');
9195
});
9296
});
97+
98+
describe('serializeAndFormat', () => {
99+
const doc = new DOMParser().parseFromString(
100+
`<?xml version="1.0" encoding="UTF-8"?>
101+
<SCL xmlns="http://www.iec.ch/61850/2003/SCL" version="2007" revision="B" release="5">
102+
<Header id="LNodeTypePreview"/>
103+
<DataTypeTemplates>
104+
<LNodeType lnClass="LPHD" id="LPHD$oscd$_f79cbe3f4e9088ea"/>
105+
</DataTypeTemplates>
106+
</SCL>`,
107+
'application/xml'
108+
);
109+
110+
it('should serialize and format an XML document', () => {
111+
const result = serializeAndFormat(doc);
112+
expect(result).to.match(/^<\?xml version="1.0" encoding="UTF-8"\?>/);
113+
expect(result).to.match(/<SCL[\s\S]*>/);
114+
expect(result).to.match(/^\t<Header id="LNodeTypePreview"\/>/m);
115+
expect(result).to.match(/^\t<DataTypeTemplates>/m);
116+
expect(result).to.match(
117+
/^\t\t<LNodeType lnClass="LPHD" id="LPHD\$oscd\$_f79cbe3f4e9088ea"\/>/m
118+
);
119+
expect(result).to.match(/^\t<\/DataTypeTemplates>/m);
120+
expect(result).to.match(/^<\/SCL>$/m);
121+
});
122+
});
93123
});

foundation.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,31 @@ export function processEnums(
106106

107107
return newSel;
108108
}
109+
110+
function formatXml(xml: string): string {
111+
let formatted = '';
112+
let indent = '';
113+
const tab = '\t';
114+
xml.split(/>\s*</).forEach(node => {
115+
if (node.match(/^\/\w/)) indent = indent.substring(tab.length);
116+
formatted += `${indent}<${node}>\r\n`;
117+
if (node.match(/^<?\w[^>]*[^/]$/)) indent += tab;
118+
});
119+
return formatted.substring(1, formatted.length - 3);
120+
}
121+
122+
export function serializeAndFormat(doc: XMLDocument): string {
123+
const serializer = new XMLSerializer();
124+
const xmlString = serializer.serializeToString(doc);
125+
return formatXml(xmlString);
126+
}
127+
128+
export function createBaseSCLDoc(): XMLDocument {
129+
return new DOMParser().parseFromString(
130+
`<?xml version="1.0" encoding="UTF-8"?>
131+
<SCL xmlns="http://www.iec.ch/61850/2003/SCL" version="2007" revision="B" release="5">
132+
<Header id="LNodeTypePreview"/>
133+
</SCL>`,
134+
'application/xml'
135+
);
136+
}

oscd-template-generator.ts

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { CdcChildren } from '@openenergytools/scl-lib/dist/tDataTypeTemplates/ns
2727
import { Snackbar } from './components/snackbar.js';
2828
import { CreateDataObjectDialog } from './components/create-do-dialog.js';
2929
import { DescriptionDialog } from './components/description-dialog.js';
30+
import { PreviewDialog } from './components/preview-dialog.js';
3031

3132
import { cdClasses, lnClass74 } from './constants.js';
3233
import { NodeData, getSelectionByPath, processEnums } from './foundation.js';
@@ -49,6 +50,7 @@ export default class TemplateGenerator extends ScopedElementsMixin(LitElement) {
4950
'oscd-snackbar': Snackbar,
5051
'create-data-object-dialog': CreateDataObjectDialog,
5152
'description-dialog': DescriptionDialog,
53+
'preview-dialog': PreviewDialog,
5254
};
5355

5456
@property({ attribute: false })
@@ -66,6 +68,9 @@ export default class TemplateGenerator extends ScopedElementsMixin(LitElement) {
6668
@query('description-dialog')
6769
descriptionDialog!: DescriptionDialog;
6870

71+
@query('preview-dialog')
72+
previewDialog!: PreviewDialog;
73+
6974
@state()
7075
get selection(): TreeSelection {
7176
if (!this.treeUI) return {};
@@ -225,6 +230,11 @@ export default class TemplateGenerator extends ScopedElementsMixin(LitElement) {
225230
}, 0);
226231
}
227232

233+
private showPreview(): void {
234+
this.previewDialog.selection = this.treeUI.selection;
235+
this.previewDialog.show();
236+
}
237+
228238
private updateSelectionAtPath(
229239
selection: TreeSelection,
230240
path: string[],
@@ -298,12 +308,17 @@ export default class TemplateGenerator extends ScopedElementsMixin(LitElement) {
298308
<tree-grid @node-selected=${this.handleNodeSelected}></tree-grid>
299309
</div>
300310
${this.doc
301-
? html`<md-fab
302-
label="${this.addedLNode || 'Add Type'}"
303-
@click=${() => this.descriptionDialog.show()}
304-
>
305-
<md-icon slot="icon">${this.addedLNode ? 'done' : 'add'}</md-icon>
306-
</md-fab>`
311+
? html`<div class="fab-wrapper">
312+
<md-fab @click=${() => this.showPreview()} title="Preview">
313+
<md-icon slot="icon">preview</md-icon>
314+
</md-fab>
315+
<md-fab
316+
label="${this.addedLNode || 'Add Type'}"
317+
@click=${() => this.descriptionDialog.show()}
318+
>
319+
<md-icon slot="icon">${this.addedLNode ? 'done' : 'add'}</md-icon>
320+
</md-fab>
321+
</div>`
307322
: html``}
308323
<create-data-object-dialog
309324
.cdClasses=${cdClasses}
@@ -314,6 +329,10 @@ export default class TemplateGenerator extends ScopedElementsMixin(LitElement) {
314329
.onConfirm=${(description: string) => this.saveTemplates(description)}
315330
.onCancel=${() => this.descriptionDialog.close()}
316331
></description-dialog>
332+
<preview-dialog
333+
.tree=${this.treeUI?.tree}
334+
.lNodeType=${this.lNodeType}
335+
></preview-dialog>
317336
<oscd-snackbar
318337
.message=${this.snackbarMessage}
319338
.type=${this.snackbarType}
@@ -355,10 +374,12 @@ export default class TemplateGenerator extends ScopedElementsMixin(LitElement) {
355374
font-family: var(--oscd-theme-icon-font, 'Material Symbols Outlined');
356375
}
357376
358-
md-fab {
377+
.fab-wrapper {
359378
position: fixed;
360379
bottom: 32px;
361380
right: 32px;
381+
display: flex;
382+
gap: 16px;
362383
}
363384
364385
.container {

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"@openenergytools/scl-lib": "1.6.0",
2424
"@openenergytools/tree-grid": "1.1.0",
2525
"@scopedelement/material-web": "^3.6.2",
26+
"ace-custom-element": "^1.6.5",
2627
"lit": "^3.0.0"
2728
},
2829
"devDependencies": {

0 commit comments

Comments
 (0)