diff --git a/components/add-data-object-dialog.spec.ts b/components/add-data-object-dialog.spec.ts
index 7a691f5..7f9aa23 100644
--- a/components/add-data-object-dialog.spec.ts
+++ b/components/add-data-object-dialog.spec.ts
@@ -44,9 +44,10 @@ describe('AddDataObjectDialog', () => {
it('renders dialog', () => {
expect(dialog.createDOdialog.open).to.be.false;
dialog.show();
- expect(dialog.shadowRoot?.querySelector('md-dialog')).to.exist;
- expect(dialog.shadowRoot?.querySelector('md-filled-select')).to.exist;
- expect(dialog.shadowRoot?.querySelector('md-outlined-text-field')).to.exist;
+ expect(dialog.shadowRoot?.querySelector('oscd-dialog')).to.exist;
+ expect(dialog.shadowRoot?.querySelector('oscd-filled-select')).to.exist;
+ expect(dialog.shadowRoot?.querySelector('oscd-outlined-text-field')).to
+ .exist;
cdClasses.forEach(cdClass => {
expect(dialog.shadowRoot?.textContent).to.include(cdClass);
});
diff --git a/components/add-data-object-dialog.ts b/components/add-data-object-dialog.ts
index 6e1a3bf..f5adc37 100644
--- a/components/add-data-object-dialog.ts
+++ b/components/add-data-object-dialog.ts
@@ -3,12 +3,11 @@ import { LitElement, html, css } from 'lit';
import { property, state, query } from 'lit/decorators.js';
import { Tree, TreeNode } from '@openenergytools/tree-grid';
import { ScopedElementsMixin } from '@open-wc/scoped-elements/lit-element.js';
-import { MdDialog } from '@scopedelement/material-web/dialog/MdDialog.js';
-import { MdOutlinedTextField } from '@scopedelement/material-web/textfield/MdOutlinedTextField.js';
-import { MdFilledSelect } from '@scopedelement/material-web/select/MdOutlineSelect.js';
-import { MdSelectOption } from '@scopedelement/material-web/select/MdSelectOption.js';
-import { MdOutlinedButton } from '@scopedelement/material-web/button/outlined-button.js';
-import { MdTextButton } from '@scopedelement/material-web/button/text-button.js';
+import { OscdDialog } from '@omicronenergy/oscd-ui/dialog/OscdDialog.js';
+import { OscdOutlinedTextField } from '@omicronenergy/oscd-ui/textfield/OscdOutlinedTextField.js';
+import { OscdFilledSelect } from '@omicronenergy/oscd-ui/select/OscdFilledSelect.js';
+import { OscdSelectOption } from '@omicronenergy/oscd-ui/select/OscdSelectOption.js';
+import { OscdTextButton } from '@omicronenergy/oscd-ui/button/OscdTextButton.js';
import { debounce } from '../utils/debounce.js';
// eslint-disable-next-line no-shadow
@@ -23,16 +22,15 @@ const firstTextBlockRegExp = /[A-Za-z]+/;
export class AddDataObjectDialog extends ScopedElementsMixin(LitElement) {
static scopedElements = {
- 'md-outlined-button': MdOutlinedButton,
- 'md-dialog': MdDialog,
- 'md-outlined-text-field': MdOutlinedTextField,
- 'md-text-button': MdTextButton,
- 'md-select-option': MdSelectOption,
- 'md-filled-select': MdFilledSelect,
+ 'oscd-dialog': OscdDialog,
+ 'oscd-outlined-text-field': OscdOutlinedTextField,
+ 'oscd-text-button': OscdTextButton,
+ 'oscd-select-option': OscdSelectOption,
+ 'oscd-filled-select': OscdFilledSelect,
};
static styles = css`
- md-dialog {
+ oscd-dialog {
min-width: 350px;
}
.dialog-content {
@@ -41,11 +39,11 @@ export class AddDataObjectDialog extends ScopedElementsMixin(LitElement) {
gap: 16px;
margin-top: 8px;
}
- md-filled-select,
- md-outlined-text-field {
+ oscd-filled-select,
+ oscd-outlined-text-field {
width: 100%;
}
- md-text-button {
+ oscd-text-button {
text-transform: uppercase;
}
`;
@@ -69,17 +67,17 @@ export class AddDataObjectDialog extends ScopedElementsMixin(LitElement) {
@state()
errorText = '';
- @query('md-dialog')
- createDOdialog!: MdDialog;
+ @query('oscd-dialog')
+ createDOdialog!: OscdDialog;
@query('#cdc-type')
- cdcType!: MdFilledSelect;
+ cdcType!: OscdFilledSelect;
@query('#do-name')
- doName!: MdOutlinedTextField;
+ doName!: OscdOutlinedTextField;
@query('#namespace')
- namespace!: MdOutlinedTextField;
+ namespace!: OscdOutlinedTextField;
private namespaceDefaultValue = 'User-Defined';
@@ -251,7 +249,7 @@ export class AddDataObjectDialog extends ScopedElementsMixin(LitElement) {
/* eslint-disable class-methods-use-this */
private resetErrorText(e: Event): void {
- const target = e.target as MdOutlinedTextField;
+ const target = e.target as OscdOutlinedTextField;
if (target.errorText && target.checkValidity()) {
target.errorText = '';
target.error = false;
@@ -260,7 +258,7 @@ export class AddDataObjectDialog extends ScopedElementsMixin(LitElement) {
render() {
return html`
-
+
Add Data Object
- CloseClose
- AddAdd
-
+
`;
}
}
diff --git a/components/delete-lnodetype-dialog.spec.ts b/components/delete-lnodetype-dialog.spec.ts
index 18039a6..824f803 100644
--- a/components/delete-lnodetype-dialog.spec.ts
+++ b/components/delete-lnodetype-dialog.spec.ts
@@ -40,8 +40,9 @@ describe('DeleteDialog', () => {
await deleteDialog.updateComplete;
await deleteDialog.dialog.updateComplete;
- const buttons =
- deleteDialog.shadowRoot?.querySelectorAll('md-outlined-button');
+ const buttons = deleteDialog.shadowRoot?.querySelectorAll(
+ 'oscd-outlined-button'
+ );
const deleteButton = Array.from(buttons || []).find(
btn => btn.textContent?.trim() === 'Delete'
) as HTMLElement;
@@ -75,8 +76,9 @@ describe('DeleteDialog', () => {
deleteDialog.show();
await deleteDialog.updateComplete;
- const buttons =
- deleteDialog.shadowRoot?.querySelectorAll('md-outlined-button');
+ const buttons = deleteDialog.shadowRoot?.querySelectorAll(
+ 'oscd-outlined-button'
+ );
const cancelButton = Array.from(buttons || []).find(
btn => btn.textContent?.trim() === 'Cancel'
) as HTMLElement;
diff --git a/components/delete-lnodetype-dialog.ts b/components/delete-lnodetype-dialog.ts
index 62ab811..3ee7a90 100644
--- a/components/delete-lnodetype-dialog.ts
+++ b/components/delete-lnodetype-dialog.ts
@@ -2,13 +2,13 @@
import { ScopedElementsMixin } from '@open-wc/scoped-elements/lit-element.js';
import { LitElement, html, css } from 'lit';
import { query, property } from 'lit/decorators.js';
-import { MdDialog } from '@scopedelement/material-web/dialog/dialog.js';
-import { MdOutlinedButton } from '@scopedelement/material-web/button/MdOutlinedButton.js';
+import { OscdDialog } from '@omicronenergy/oscd-ui/dialog/OscdDialog.js';
+import { OscdOutlinedButton } from '@omicronenergy/oscd-ui/button/OscdOutlinedButton.js';
export class DeleteDialog extends ScopedElementsMixin(LitElement) {
static scopedElements = {
- 'md-dialog': MdDialog,
- 'md-outlined-button': MdOutlinedButton,
+ 'oscd-dialog': OscdDialog,
+ 'oscd-outlined-button': OscdOutlinedButton,
};
@property()
@@ -17,8 +17,8 @@ export class DeleteDialog extends ScopedElementsMixin(LitElement) {
@property()
lnodeTypeId: string = '';
- @query('md-dialog')
- dialog!: MdDialog;
+ @query('oscd-dialog')
+ dialog!: OscdDialog;
get open() {
return this.dialog?.open ?? false;
@@ -43,23 +43,25 @@ export class DeleteDialog extends ScopedElementsMixin(LitElement) {
render() {
return html`
-
+
Confirm delete
Are you sure you want to delete Logical Node Type ${this.lnodeTypeId}?
This action may have severe consequences.
- CancelCancel
- DeleteDelete
-
+
`;
}
diff --git a/components/lnodetype-sidebar.ts b/components/lnodetype-sidebar.ts
index 7e499fb..b204e6c 100644
--- a/components/lnodetype-sidebar.ts
+++ b/components/lnodetype-sidebar.ts
@@ -3,17 +3,17 @@ import { LitElement, html, css } from 'lit';
import { property, state } from 'lit/decorators.js';
import { ScopedElementsMixin } from '@open-wc/scoped-elements/lit-element.js';
-import { MdOutlinedButton } from '@scopedelement/material-web/button/outlined-button.js';
-import { MdOutlinedTextField } from '@scopedelement/material-web/textfield/MdOutlinedTextField.js';
-import { MdList } from '@scopedelement/material-web/list/MdList.js';
-import { MdListItem } from '@scopedelement/material-web/list/MdListItem.js';
+import { OscdFilledButton } from '@omicronenergy/oscd-ui/button/OscdFilledButton.js';
+import { OscdOutlinedTextField } from '@omicronenergy/oscd-ui/textfield/OscdOutlinedTextField.js';
+import { OscdList } from '@omicronenergy/oscd-ui/list/OscdList.js';
+import { OscdListItem } from '@omicronenergy/oscd-ui/list/OscdListItem.js';
export class LNodeTypeSidebar extends ScopedElementsMixin(LitElement) {
static scopedElements = {
- 'md-outlined-button': MdOutlinedButton,
- 'md-outlined-textfield': MdOutlinedTextField,
- 'md-list': MdList,
- 'md-list-item': MdListItem,
+ 'oscd-filled-button': OscdFilledButton,
+ 'oscd-outlined-textfield': OscdOutlinedTextField,
+ 'oscd-list': OscdList,
+ 'oscd-list-item': OscdListItem,
};
@property({ type: Array })
@@ -88,13 +88,13 @@ export class LNodeTypeSidebar extends ScopedElementsMixin(LitElement) {
render() {
return html`
`;
}
updated(changedProperties: Map) {
super.updated?.(changedProperties);
- // Scroll md-list to top when lNodeTypes changes
+ // Scroll oscd-list to top when lNodeTypes changes
if (changedProperties.has('lNodeTypes')) {
- const mdList = this.renderRoot.querySelector('md-list');
- if (mdList) mdList.scrollTop = 0;
+ const oscdList = this.renderRoot.querySelector('oscd-list');
+ if (oscdList) oscdList.scrollTop = 0;
}
}
@@ -142,35 +142,35 @@ export class LNodeTypeSidebar extends ScopedElementsMixin(LitElement) {
min-height: 0;
padding: 1rem;
overflow: hidden;
- background-color: #fcf6e5;
+ background-color: var(--oscd-base3);
}
- md-list {
+ oscd-list {
min-height: 0;
max-height: calc(100vh - var(--header-height) - 1rem - 134px);
overflow-y: auto;
scrollbar-width: thin;
padding: 0;
}
- md-list::-webkit-scrollbar {
+ oscd-list::-webkit-scrollbar {
width: 8px;
}
- md-list::-webkit-scrollbar-thumb {
+ oscd-list::-webkit-scrollbar-thumb {
border-radius: 4px;
}
- md-list::-webkit-scrollbar-track {
+ oscd-list::-webkit-scrollbar-track {
background: transparent;
}
- md-list-item {
+ oscd-list-item {
box-sizing: border-box;
}
- md-list-item[selected] {
+ oscd-list-item[selected] {
background: var(--md-sys-color-primary);
}
- md-list-item[selected] span[slot='headline'],
- md-list-item[selected] span[slot='supporting-text'] {
+ oscd-list-item[selected] span[slot='headline'],
+ oscd-list-item[selected] span[slot='supporting-text'] {
color: var(--md-sys-color-on-primary, #ffffff);
}
- md-outlined-textfield {
+ oscd-outlined-textfield {
width: 100%;
}
.actions {
diff --git a/components/settings-dialog.spec.ts b/components/settings-dialog.spec.ts
index a30a814..b404251 100644
--- a/components/settings-dialog.spec.ts
+++ b/components/settings-dialog.spec.ts
@@ -7,7 +7,7 @@ describe('SettingsDialog', () => {
let element: SettingsDialog;
const getRadio = (value: 'update' | 'swap') =>
- element.shadowRoot!.querySelector(`md-radio[value="${value}"]`) as any;
+ element.shadowRoot!.querySelector(`oscd-radio[value="${value}"]`) as any;
beforeEach(async () => {
localStorage.clear();
@@ -53,7 +53,7 @@ describe('SettingsDialog', () => {
await element.updateComplete;
const confirmButton = element.shadowRoot!.querySelector(
- 'md-text-button:last-child'
+ 'oscd-text-button:last-child'
) as any;
confirmButton.click();
@@ -68,7 +68,7 @@ describe('SettingsDialog', () => {
await element.updateComplete;
const cancelButton = element.shadowRoot!.querySelector(
- 'md-text-button:first-child'
+ 'oscd-text-button:first-child'
) as any;
cancelButton.click();
diff --git a/components/settings-dialog.ts b/components/settings-dialog.ts
index a53a047..fa57567 100644
--- a/components/settings-dialog.ts
+++ b/components/settings-dialog.ts
@@ -2,9 +2,9 @@
import { ScopedElementsMixin } from '@open-wc/scoped-elements/lit-element.js';
import { LitElement, html, css } from 'lit';
import { query, state } from 'lit/decorators.js';
-import { MdDialog } from '@scopedelement/material-web/dialog/dialog.js';
-import { MdTextButton } from '@scopedelement/material-web/button/text-button.js';
-import { MdRadio } from '@scopedelement/material-web/radio/radio.js';
+import { OscdDialog } from '@omicronenergy/oscd-ui/dialog/OscdDialog.js';
+import { OscdTextButton } from '@omicronenergy/oscd-ui/button/OscdTextButton.js';
+import { OscdRadio } from '@omicronenergy/oscd-ui/radio/OscdRadio.js';
import { TEMPLATE_UPDATE_SETTING_STORAGE_KEY } from '../foundation/constants.js';
// eslint-disable-next-line no-shadow
@@ -15,13 +15,13 @@ export enum UpdateSetting {
export class SettingsDialog extends ScopedElementsMixin(LitElement) {
static scopedElements = {
- 'md-dialog': MdDialog,
- 'md-text-button': MdTextButton,
- 'md-radio': MdRadio,
+ 'oscd-dialog': OscdDialog,
+ 'oscd-text-button': OscdTextButton,
+ 'oscd-radio': OscdRadio,
};
- @query('md-dialog')
- dialog!: MdDialog;
+ @query('oscd-dialog')
+ dialog!: OscdDialog;
@state()
private updateSetting: UpdateSetting = UpdateSetting.Update;
@@ -63,7 +63,7 @@ export class SettingsDialog extends ScopedElementsMixin(LitElement) {
}
private handleRadioChange(event: Event) {
- const target = event.target as MdRadio;
+ const target = event.target as OscdRadio;
if (target.checked) {
this.updateSetting = target.value as UpdateSetting;
}
@@ -81,44 +81,44 @@ export class SettingsDialog extends ScopedElementsMixin(LitElement) {
render() {
return html`
- this.dialog?.close()}>
+ this.dialog?.close()}>
LNodeType update behaviour
-
+
Cancel
-
-
+
+
Save
-
+
-
+
`;
}
static styles = css`
- md-dialog {
+ oscd-dialog {
--md-dialog-container-max-width: 400px;
}
@@ -147,7 +147,7 @@ export class SettingsDialog extends ScopedElementsMixin(LitElement) {
flex: 1;
}
- md-text-button {
+ oscd-text-button {
text-transform: uppercase;
}
`;
diff --git a/foundation/utils.spec.ts b/foundation/utils.spec.ts
index 638051d..4a69f68 100644
--- a/foundation/utils.spec.ts
+++ b/foundation/utils.spec.ts
@@ -1,6 +1,7 @@
/* eslint-disable no-unused-expressions */
import { expect } from '@open-wc/testing';
-import { removeDOsNotInSelection } from './utils.js';
+import { removeDOsNotInSelection, computeOrphanedRemoves } from './utils.js';
+import { nsdSpeced } from '../scl-template-update.testfiles.js';
describe('foundation/utils', () => {
describe('removeDOsNotInSelection', () => {
@@ -85,4 +86,83 @@ describe('foundation/utils', () => {
expect(result.tagName).to.equal('LNodeType');
});
});
+
+ describe('computeOrphanedRemoves', () => {
+ const mmxuId = 'MMXU$oscd$_c53e78191fabefa3';
+ let dataTypeTemplates: Element;
+
+ beforeEach(() => {
+ dataTypeTemplates = new DOMParser()
+ .parseFromString(nsdSpeced, 'application/xml')
+ .querySelector('DataTypeTemplates')!;
+ });
+
+ function makeNewMmxu(doNames: string[]): Element {
+ const doElements = doNames
+ .map(name => {
+ const existing = dataTypeTemplates.querySelector(
+ `LNodeType[id="${mmxuId}"] > DO[name="${name}"]`
+ );
+ return existing ? existing.outerHTML : '';
+ })
+ .join('');
+ return new DOMParser().parseFromString(
+ `${doElements}`,
+ 'application/xml'
+ ).documentElement;
+ }
+
+ it('returns the old LNodeType as the first remove', () => {
+ const result = computeOrphanedRemoves(dataTypeTemplates, mmxuId, [
+ makeNewMmxu(['Beh']),
+ ]);
+
+ expect(result).to.have.length.greaterThan(0);
+ expect((result[0] as any).node as Element).to.equal(
+ dataTypeTemplates.querySelector(`LNodeType[id="${mmxuId}"]`)
+ );
+ });
+
+ it('cascades orphans when A DO is removed: removes A DOType and all its exclusive descendants', () => {
+ const result = computeOrphanedRemoves(dataTypeTemplates, mmxuId, [
+ makeNewMmxu(['Beh']),
+ ]);
+
+ const orphanedIds = result
+ .slice(1) // skip old LNodeType remove
+ .map((edit: any) => edit.node.getAttribute('id'))
+ .filter(id => id !== null);
+ expect(orphanedIds).to.include('A$oscd$_41824603f63b26ac');
+ expect(orphanedIds).to.include('phsA$oscd$_995aad120f12c815');
+ expect(orphanedIds).to.include('cVal$oscd$_80272042468595d1');
+ expect(orphanedIds).to.include('units$oscd$_3f2e10def85bfeac');
+ expect(orphanedIds).to.include('mag$oscd$_ed49c2f7a55ad05a');
+ expect(orphanedIds).to.include('SIUnit$oscd$_39f6ca400c633081');
+ });
+
+ it('does not orphan the Beh DOType which is shared with LLN0', () => {
+ const result = computeOrphanedRemoves(dataTypeTemplates, mmxuId, [
+ makeNewMmxu(['Beh']),
+ ]);
+
+ const orphanedIds = result
+ .slice(1) // skip old LNodeType remove
+ .map((edit: any) => edit.node.getAttribute('id'))
+ .filter(id => id !== null);
+
+ expect(orphanedIds).to.not.include('Beh$oscd$_c6ed035c8137b35a');
+ expect(orphanedIds).to.not.include('stVal$oscd$_48ba16345b8e7f5b');
+ });
+
+ it('returns only the old LNodeType when the new version keeps all DOs', () => {
+ const result = computeOrphanedRemoves(dataTypeTemplates, mmxuId, [
+ makeNewMmxu(['Beh', 'A']),
+ ]);
+
+ expect(result).to.have.lengthOf(1);
+ expect((result[0] as any).node as Element).to.equal(
+ dataTypeTemplates.querySelector(`LNodeType[id="${mmxuId}"]`)
+ );
+ });
+ });
});
diff --git a/foundation/utils.ts b/foundation/utils.ts
index 1c01287..bffef73 100644
--- a/foundation/utils.ts
+++ b/foundation/utils.ts
@@ -1,4 +1,5 @@
import { Tree, TreeSelection } from '@openenergytools/tree-grid';
+import { EditV2 } from '@openscd/oscd-api';
export function getLNodeTypes(doc: XMLDocument | undefined): Element[] {
return Array.from(
@@ -69,3 +70,64 @@ export function removeDOsNotInSelection(
return clonedLNodeType;
}
+
+/**
+ * Builds the Remove edits needed when an LNodeType is replaced.
+ * Clones the DataTypeTemplates once, adds the incoming new nodes, removes
+ * the old LNodeType, then cascades to find which pre-existing sub-types are
+ * now unreferenced. Returns the old LNodeType remove plus all orphan removes.
+ * @param dataTypeTemplates The live DataTypeTemplates element to base the remove calculations on
+ * @param oldLNodeTypeId The ID of the LNodeType being replaced
+ * @param newNodes The new nodes being added (cloned before passing in)
+ * @returns An array of EditV2 objects representing the nodes to remove
+ */
+export function computeOrphanedRemoves(
+ dataTypeTemplates: Element,
+ oldLNodeTypeId: string,
+ newNodes: Element[]
+): EditV2[] {
+ const subTypes = Array.from(
+ dataTypeTemplates.querySelectorAll(
+ ':scope > DOType, :scope > DAType, :scope > EnumType'
+ )
+ );
+ const subTypeIds = subTypes
+ .map(el => el.getAttribute('id'))
+ .filter(id => id !== null);
+ const preEditIds = new Set(subTypeIds);
+
+ const dttClone = dataTypeTemplates.cloneNode(true) as Element;
+ newNodes.forEach(n => dttClone.appendChild(n.cloneNode(true)));
+ dttClone
+ .querySelector(`:scope > LNodeType[id="${oldLNodeTypeId}"]`)
+ ?.remove();
+
+ const remainingIds = new Set(preEditIds);
+ const orphanIds: string[] = [];
+ let changed = true;
+
+ while (changed) {
+ changed = false;
+ for (const id of remainingIds) {
+ if (!dttClone.querySelector(`:scope *[type="${id}"]`)) {
+ orphanIds.push(id);
+ remainingIds.delete(id);
+ dttClone.querySelector(`:scope > *[id="${id}"]`)?.remove();
+ changed = true;
+ }
+ }
+ }
+
+ const results: EditV2[] = [];
+ const oldLN = dataTypeTemplates.querySelector(
+ `:scope > LNodeType[id="${oldLNodeTypeId}"]`
+ );
+ if (oldLN) results.push({ node: oldLN as Node });
+ orphanIds.forEach(id => {
+ const node = dataTypeTemplates.querySelector(
+ `:scope > *[id="${id}"]`
+ ) as Node | null;
+ if (node) results.push({ node });
+ });
+ return results;
+}
diff --git a/index.html b/index.html
index b6c02b0..eb87359 100644
--- a/index.html
+++ b/index.html
@@ -14,14 +14,40 @@
/>
-
+