diff --git a/components/delete-lnodetype-dialog.spec.ts b/components/delete-lnodetype-dialog.spec.ts new file mode 100644 index 0000000..18039a6 --- /dev/null +++ b/components/delete-lnodetype-dialog.spec.ts @@ -0,0 +1,89 @@ +/* eslint-disable no-unused-expressions */ +import { fixture, expect, html } from '@open-wc/testing'; +import { SinonSpy, spy } from 'sinon'; +import { DeleteDialog } from './delete-lnodetype-dialog.js'; + +customElements.define('delete-dialog', DeleteDialog); + +describe('DeleteDialog', () => { + let deleteDialog: DeleteDialog; + let confirmSpy: SinonSpy; + + beforeEach(async () => { + confirmSpy = spy(); + + deleteDialog = await fixture( + html`` + ); + }); + + afterEach(async () => { + deleteDialog.remove(); + }); + + it('displays the correct LNodeType ID in the message', () => { + deleteDialog.show(); + expect(deleteDialog.shadowRoot?.textContent).to.include( + 'MMXU$oscd$_c53e78191fabefa3' + ); + expect(deleteDialog.shadowRoot?.textContent).to.include('Confirm delete'); + expect(deleteDialog.shadowRoot?.textContent).to.include( + 'This action may have severe consequences' + ); + }); + + it('should call onConfirm and close when Delete button is clicked', async () => { + deleteDialog.show(); + await deleteDialog.updateComplete; + await deleteDialog.dialog.updateComplete; + + const buttons = + deleteDialog.shadowRoot?.querySelectorAll('md-outlined-button'); + const deleteButton = Array.from(buttons || []).find( + btn => btn.textContent?.trim() === 'Delete' + ) as HTMLElement; + + expect(deleteButton).to.exist; + deleteButton.click(); + await deleteDialog.updateComplete; + await deleteDialog.dialog.updateComplete; + + expect(confirmSpy.callCount).to.equal(1); + }); + + it('should update displayed ID when lnodeTypeId property changes', async () => { + deleteDialog.show(); + await deleteDialog.updateComplete; + expect(deleteDialog.shadowRoot?.textContent).to.include( + 'MMXU$oscd$_c53e78191fabefa3' + ); + + deleteDialog.lnodeTypeId = 'LLN0$oscd$_85c7ffbe25d80e63'; + await deleteDialog.updateComplete; + expect(deleteDialog.shadowRoot?.textContent).to.include( + 'LLN0$oscd$_85c7ffbe25d80e63' + ); + expect(deleteDialog.shadowRoot?.textContent).to.not.include( + 'MMXU$oscd$_c53e78191fabefa3' + ); + }); + + it('should not call onConfirm when dialog is cancelled', async () => { + deleteDialog.show(); + await deleteDialog.updateComplete; + + const buttons = + deleteDialog.shadowRoot?.querySelectorAll('md-outlined-button'); + const cancelButton = Array.from(buttons || []).find( + btn => btn.textContent?.trim() === 'Cancel' + ) as HTMLElement; + + cancelButton.click(); + await deleteDialog.updateComplete; + + expect(confirmSpy.callCount).to.equal(0); + }); +}); diff --git a/components/delete-lnodetype-dialog.ts b/components/delete-lnodetype-dialog.ts new file mode 100644 index 0000000..62ab811 --- /dev/null +++ b/components/delete-lnodetype-dialog.ts @@ -0,0 +1,104 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +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'; + +export class DeleteDialog extends ScopedElementsMixin(LitElement) { + static scopedElements = { + 'md-dialog': MdDialog, + 'md-outlined-button': MdOutlinedButton, + }; + + @property() + onConfirm!: () => void; + + @property() + lnodeTypeId: string = ''; + + @query('md-dialog') + dialog!: MdDialog; + + get open() { + return this.dialog?.open ?? false; + } + + show() { + this.dialog?.show(); + } + + close() { + this.dialog?.close(); + } + + private handleCancel() { + this.close(); + } + + private handleConfirm() { + this.onConfirm(); + this.close(); + } + + render() { + return html` + +
Confirm delete
+
+ Are you sure you want to delete Logical Node Type ${this.lnodeTypeId}? + This action may have severe consequences. +
+
+ Cancel + Delete +
+
+ `; + } + + static styles = css` + * { + --md-sys-color-primary: var(--oscd-primary); + --md-sys-color-secondary: var(--oscd-secondary); + --md-sys-typescale-body-large-font: var(--oscd-theme-text-font); + --md-outlined-text-field-input-text-color: var(--oscd-base01); + + --md-sys-color-surface: var(--oscd-base3); + --md-sys-color-on-surface: var(--oscd-base00); + --md-sys-color-on-primary: var(--oscd-base2); + --md-sys-color-on-surface-variant: var(--oscd-base00); + --md-menu-container-color: var(--oscd-base3); + font-family: var(--oscd-theme-text-font); + --md-sys-color-surface-container-highest: var(--oscd-base2); + --md-dialog-container-color: var(--oscd-base3); + font-family: var(--oscd-theme-text-font, 'Roboto'); + } + + md-outlined-button { + text-transform: uppercase; + } + + .delete-content { + display: flex; + flex-direction: column; + gap: 12px; + } + + .button.close { + --md-outlined-button-label-text-color: var(--oscd-accent-red); + --md-outlined-button-hover-label-text-color: var(--oscd-accent-red); + } + + .button.delete { + --md-outlined-button-label-text-color: var(--oscd-accent-red); + --md-outlined-button-hover-label-text-color: var(--oscd-accent-red); + } + `; +} diff --git a/foundation/utils.spec.ts b/foundation/utils.spec.ts new file mode 100644 index 0000000..638051d --- /dev/null +++ b/foundation/utils.spec.ts @@ -0,0 +1,88 @@ +/* eslint-disable no-unused-expressions */ +import { expect } from '@open-wc/testing'; +import { removeDOsNotInSelection } from './utils.js'; + +describe('foundation/utils', () => { + describe('removeDOsNotInSelection', () => { + let lNodeType: Element; + + beforeEach(() => { + const doc = new DOMParser().parseFromString( + ` + + + + `, + 'application/xml' + ); + lNodeType = doc.documentElement; + }); + + it('removes DOs not in selection', () => { + const selection = { + Beh: {}, + PhV: {}, + }; + + const result = removeDOsNotInSelection(lNodeType, selection); + + const remainingDOs = Array.from(result.querySelectorAll('DO')).map( + doElement => doElement.getAttribute('name') + ); + + expect(remainingDOs).to.have.lengthOf(2); + expect(remainingDOs).to.include('Beh'); + expect(remainingDOs).to.include('PhV'); + expect(remainingDOs).to.not.include('A'); + }); + + it('returns a clone without modifying the original', () => { + const selection = { + Beh: {}, + }; + + const originalDOCount = lNodeType.querySelectorAll('DO').length; + const result = removeDOsNotInSelection(lNodeType, selection); + + // Original should be unchanged + expect(lNodeType.querySelectorAll('DO')).to.have.lengthOf( + originalDOCount + ); + + // Result should have fewer DOs + expect(result.querySelectorAll('DO')).to.have.lengthOf(1); + }); + + it('removes all DOs when selection is empty', () => { + const selection = {}; + + const result = removeDOsNotInSelection(lNodeType, selection); + + expect(result.querySelectorAll('DO')).to.have.lengthOf(0); + }); + + it('keeps all DOs when all are in selection', () => { + const selection = { + Beh: {}, + A: {}, + PhV: {}, + }; + + const result = removeDOsNotInSelection(lNodeType, selection); + + expect(result.querySelectorAll('DO')).to.have.lengthOf(3); + }); + + it('preserves LNodeType attributes', () => { + const selection = { + Beh: {}, + }; + + const result = removeDOsNotInSelection(lNodeType, selection); + + expect(result.getAttribute('id')).to.equal('TestLNodeType'); + expect(result.getAttribute('lnClass')).to.equal('MMXU'); + expect(result.tagName).to.equal('LNodeType'); + }); + }); +}); diff --git a/foundation/utils.ts b/foundation/utils.ts index dcf6bbe..1c01287 100644 --- a/foundation/utils.ts +++ b/foundation/utils.ts @@ -39,3 +39,33 @@ export function filterSelection( return filteredTree; } + +/** + * Creates a clone of an LNodeType with only the DOs that are present in the selection. + * DOs not included in the selection are removed from the cloned element. + * @param lNodeType - The LNodeType element to filter + * @param selection - The tree selection containing the DO names to keep + * @returns A cloned LNodeType element containing only the selected DOs + */ +export function removeDOsNotInSelection( + lNodeType: Element, + selection: TreeSelection +): Element { + const clonedLNodeType = lNodeType.cloneNode(true) as Element; + + const dosToRemove: Element[] = []; + Array.from(clonedLNodeType.querySelectorAll(':scope > DO')).forEach( + doElement => { + const doName = doElement.getAttribute('name'); + if (doName && !selection[doName]) { + dosToRemove.push(doElement); + } + } + ); + + dosToRemove.forEach(doElement => { + clonedLNodeType.removeChild(doElement); + }); + + return clonedLNodeType; +} diff --git a/package-lock.json b/package-lock.json index 54341c7..dc61bb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "@openscd/oscd-template-generator", + "name": "@com-pas/scl-template-update", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@openscd/oscd-template-generator", + "name": "@com-pas/scl-template-update", "version": "0.0.0", "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 9c8bf86..4886eeb 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { - "name": "@openscd/oscd-template-generator", - "description": "OpenSCD plugin for creating new DataTypeTemplates", + "name": "@com-pas/scl-template-update", + "description": "OpenSCD plugin for updating DataTypeTemplates", "license": "Apache-2.0", - "author": "oscd-template-generator", + "author": "CoMPAS", "version": "0.0.0", "scripts": { "prepare": "npm run build", diff --git a/scl-template-update.spec.ts b/scl-template-update.spec.ts index ac579b4..e77848f 100644 --- a/scl-template-update.spec.ts +++ b/scl-template-update.spec.ts @@ -286,6 +286,148 @@ describe('NsdTemplateUpdater', () => { await element.updateComplete; expect(listener).to.have.been.called; }); + + it('does not delete LNodeType when clicking update twice with same selection', async () => { + localStorage.removeItem('template-update-setting'); + + const event = { + detail: { id: 'MMXU$oscd$_c53e78191fabefa3' }, + } as CustomEvent; + element.onLNodeTypeSelect(event); + await new Promise(res => { + setTimeout(res, 0); + }); + + element.treeUI.selection = mmxuSelection; + await element.updateComplete; + + // First update + (element.shadowRoot?.querySelector('md-fab') as HTMLElement).click(); + await element.updateComplete; + expect(listener).to.have.been.calledOnce; + + // Second update with same selection should not delete the LNodeType + listener.resetHistory(); + (element.shadowRoot?.querySelector('md-fab') as HTMLElement).click(); + await element.updateComplete; + + // Verify LNodeType still exists in document + const lNodeType = element.doc?.querySelector( + 'LNodeType[id="MMXU$oscd$_c53e78191fabefa3"]' + ); + expect(lNodeType).to.exist; + }).timeout(5000); + + it('successfully removes data objects from LNodeType', async () => { + localStorage.removeItem('template-update-setting'); + + const event = { + detail: { id: 'MMXU$oscd$_c53e78191fabefa3' }, + } as CustomEvent; + element.onLNodeTypeSelect(event); + await new Promise(res => { + setTimeout(res, 0); + }); + + // Remove the 'A' data object by not including it in selection + const selectionWithoutA = { + Beh: { + q: {}, + stVal: { + blocked: {}, + on: {}, + 'test/blocked': {}, + }, + t: {}, + }, + }; + + element.treeUI.selection = selectionWithoutA; + await element.updateComplete; + + (element.shadowRoot?.querySelector('md-fab') as HTMLElement).click(); + await element.updateComplete; + + expect(listener).to.have.been.called; + const updateEdits = listener.args[0][0].detail.edit; + expect(updateEdits).to.have.length.greaterThan(0); + + // Verify the LNodeType was updated (not deleted) + const lNodeType = element.doc?.querySelector( + 'LNodeType[id="MMXU$oscd$_c53e78191fabefa3"]' + ); + expect(lNodeType).to.exist; + + // Verify 'A' DO was removed + const doA = lNodeType?.querySelector('DO[name="A"]'); + expect(doA).to.not.exist; + + // Verify 'Beh' DO still exists + const doBeh = lNodeType?.querySelector('DO[name="Beh"]'); + expect(doBeh).to.exist; + }).timeout(5000); + + it('updates description when making selection changes', async () => { + localStorage.removeItem('template-update-setting'); + + const event = { + detail: { id: 'MMXU$oscd$_c53e78191fabefa3' }, + } as CustomEvent; + element.onLNodeTypeSelect(event); + await new Promise(res => { + setTimeout(res, 0); + }); + + // Change both selection and description + element.treeUI.selection = mmxuSelection; + element.lnodeTypeDesc.value = 'Updated with new DOs'; + await element.updateComplete; + + (element.shadowRoot?.querySelector('md-fab') as HTMLElement).click(); + await element.updateComplete; + + expect(listener).to.have.been.called; + + // Verify the description was updated + const lNodeType = element.doc?.querySelector( + 'LNodeType[id="MMXU$oscd$_c53e78191fabefa3"]' + ); + expect(lNodeType?.getAttribute('desc')).to.equal('Updated with new DOs'); + }).timeout(5000); + + it('clears description when set to empty string', async () => { + localStorage.removeItem('template-update-setting'); + + const event = { + detail: { id: 'MMXU$oscd$_c53e78191fabefa3' }, + } as CustomEvent; + element.onLNodeTypeSelect(event); + await new Promise(res => { + setTimeout(res, 0); + }); + + // First add a description + element.lnodeTypeDesc.value = 'Test Description'; + await element.updateComplete; + (element.shadowRoot?.querySelector('md-fab') as HTMLElement).click(); + await element.updateComplete; + + listener.resetHistory(); + + // Now clear the description + element.lnodeTypeDesc.value = ''; + await element.updateComplete; + (element.shadowRoot?.querySelector('md-fab') as HTMLElement).click(); + await element.updateComplete; + + expect(listener).to.have.been.called; + + // Verify the description attribute was removed + const lNodeType = element.doc?.querySelector( + 'LNodeType[id="MMXU$oscd$_c53e78191fabefa3"]' + ); + expect(lNodeType?.hasAttribute('desc')).to.be.false; + }).timeout(5000); }); describe('given a document with unsupported CDC', () => { diff --git a/scl-template-update.ts b/scl-template-update.ts index 925613c..808e278 100644 --- a/scl-template-update.ts +++ b/scl-template-update.ts @@ -4,7 +4,7 @@ import { state, query, property } from 'lit/decorators.js'; import { ScopedElementsMixin } from '@open-wc/scoped-elements/lit-element.js'; -import { newEditEvent } from '@openenergytools/open-scd-core'; +import { newEditEvent, Edit } from '@openenergytools/open-scd-core'; import { insertSelectedLNodeType, @@ -30,6 +30,7 @@ import { MdIconButton } from '@scopedelement/material-web/iconbutton/MdIconButto import { CdcChildren } from '@openscd/scl-lib/dist/tDataTypeTemplates/nsdToJson.js'; import { AddDataObjectDialog } from './components/add-data-object-dialog.js'; +import { DeleteDialog } from './components/delete-lnodetype-dialog.js'; import { LNodeTypeSidebar } from './components/lnodetype-sidebar.js'; import { SettingsDialog, UpdateSetting } from './components/settings-dialog.js'; import { @@ -42,6 +43,7 @@ import { getSelectedLNodeType, isLNodeTypeReferenced, filterSelection, + removeDOsNotInSelection, } from './foundation/utils.js'; export default class NsdTemplateUpdated extends ScopedElementsMixin( @@ -60,6 +62,7 @@ export default class NsdTemplateUpdated extends ScopedElementsMixin( 'md-outlined-text-field': MdOutlinedTextField, 'md-icon-button': MdIconButton, 'add-data-object-dialog': AddDataObjectDialog, + 'delete-dialog': DeleteDialog, 'lnodetype-sidebar': LNodeTypeSidebar, 'settings-dialog': SettingsDialog, }; @@ -67,6 +70,9 @@ export default class NsdTemplateUpdated extends ScopedElementsMixin( @property() doc?: XMLDocument; + @property({ type: Number }) + editCount = -1; + @query('tree-grid') treeUI!: TreeGrid; @@ -79,6 +85,9 @@ export default class NsdTemplateUpdated extends ScopedElementsMixin( @query('#dialog-choice') choiceDialog?: MdDialog; + @query('delete-dialog') + deleteDialog!: DeleteDialog; + @query('add-data-object-dialog') addDataObjectDialog!: HTMLElement & { show: () => void; @@ -115,12 +124,49 @@ export default class NsdTemplateUpdated extends ScopedElementsMixin( @state() disableAddDataObjectButton = true; + @state() + lNodeTypeDescription = ''; + updated(changedProperties: Map) { super.updated?.(changedProperties); if (changedProperties.has('doc')) { this.resetUI(true); this.lNodeTypes = getLNodeTypes(this.doc); } + + if (changedProperties.has('editCount') && this.editCount >= 0) { + this.lNodeTypes = getLNodeTypes(this.doc); + this.refreshSelectedLNodeType(); + } + } + + private refreshSelectedLNodeType(): void { + if (!this.selectedLNodeType) return; + + const selectedId = this.selectedLNodeType.getAttribute('id'); + const updatedLNodeType = getSelectedLNodeType(this.doc!, selectedId!); + + if (!updatedLNodeType) return; + + this.selectedLNodeType = updatedLNodeType; + this.lNodeTypeDescription = updatedLNodeType.getAttribute('desc') ?? ''; + + // Rebuild the tree to show the updated structure after undo/redo + const selectedLNodeTypeClass = updatedLNodeType.getAttribute('lnClass'); + if (selectedLNodeTypeClass) { + const { tree } = buildLNodeTree( + selectedLNodeTypeClass, + updatedLNodeType, + this.doc! + ); + if (tree) { + this.lNodeTypeSelection = lNodeTypeToSelection(updatedLNodeType); + this.nsdSelection = this.lNodeTypeSelection; + this.treeUI.tree = tree; + this.treeUI.selection = this.lNodeTypeSelection; + this.treeUI.requestUpdate(); + } + } } private resetUI(full: boolean = false): void { @@ -130,6 +176,7 @@ export default class NsdTemplateUpdated extends ScopedElementsMixin( this.nsdSelection = undefined; this.lNodeTypeUI?.reset(); this.disableAddDataObjectButton = true; + this.lNodeTypeDescription = ''; } if (this.treeUI) { this.treeUI.tree = {}; @@ -155,6 +202,22 @@ export default class NsdTemplateUpdated extends ScopedElementsMixin( this.choiceDialog?.close(); } + // eslint-disable-next-line class-methods-use-this + private applyDescriptionUpdate( + newLNodeType: Element, + desc: string, + currentLNodeType: Element + ): void { + const currentDesc = currentLNodeType.getAttribute('desc') ?? ''; + if (desc !== currentDesc) { + if (desc) { + newLNodeType.setAttribute('desc', desc); + } else { + newLNodeType.removeAttribute('desc'); + } + } + } + private async saveTemplates() { if (!this.doc || !this.nsdSelection) return; const updateSetting = @@ -165,42 +228,51 @@ export default class NsdTemplateUpdated extends ScopedElementsMixin( const lnID = this.selectedLNodeType!.getAttribute('id')!; const desc = this.lnodeTypeDesc.value; - const inserts = insertSelectedLNodeType(this.doc, this.nsdSelection, { - class: lnClass, - ...(!!desc && { desc }), - data: this.treeUI.tree as LNodeDescription, - }); + const currentLNodeType = getSelectedLNodeType(this.doc, lnID); + if (!currentLNodeType) return; - if (inserts.length === 0) { - const currentDesc = this.selectedLNodeType?.getAttribute('desc') ?? ''; - if (this.selectedLNodeType && currentDesc !== desc) { + const currentDocumentSelection = lNodeTypeToSelection(currentLNodeType); + + const selectionsMatch = + JSON.stringify(this.nsdSelection) === + JSON.stringify(currentDocumentSelection); + const currentDesc = currentLNodeType.getAttribute('desc') ?? ''; + const descChanged = currentDesc !== desc; + + if (selectionsMatch) { + if (this.selectedLNodeType && descChanged) { this.updateLNodeTypeDescription(desc); this.lNodeTypes = getLNodeTypes(this.doc); + this.showSuccessFeedback(lnID); } return; } - if (updateSetting === UpdateSetting.Update) { - const newLNodeType = inserts.find( - insert => (insert.node as Element).tagName === 'LNodeType' - )?.node as Element; - - if (newLNodeType) { - newLNodeType.setAttribute('id', lnID); + const inserts = insertSelectedLNodeType(this.doc, this.nsdSelection, { + class: lnClass, + ...(!!desc && { desc }), + data: this.treeUI.tree as LNodeDescription, + }); - const updateEdits = updateLNodeType(newLNodeType, this.doc); + if (updateSetting === UpdateSetting.Update) { + const allEdits = this.buildUpdateEdits( + inserts, + currentLNodeType, + lnID, + desc + ); - if (updateEdits.length > 0) { - this.dispatchEvent( - newEditEvent(updateEdits, { - title: `Update ${lnID}`, - }) - ); - } + if (allEdits.length > 0) { + this.dispatchEvent( + newEditEvent(allEdits, { + title: `Update ${lnID}`, + }) + ); } - this.fabLabel = `${lnID} updated!`; + this.showSuccessFeedback(lnID, 'update'); } else { + // Swap mode: Insert new, then remove old with squash this.dispatchEvent(newEditEvent(inserts)); await this.updateComplete; @@ -216,33 +288,93 @@ export default class NsdTemplateUpdated extends ScopedElementsMixin( insert => (insert.node as Element).tagName === 'LNodeType' )?.node as Element; - if (updatedLNodeType) { - const updatedLNodeTypeID = updatedLNodeType.getAttribute('id'); - this.selectedLNodeType = updatedLNodeType; - await this.updateComplete; - - if (this.lNodeTypeUI && updatedLNodeType) { - this.lNodeTypeUI.value = updatedLNodeType.getAttribute('id') ?? ''; - } - - this.fabLabel = `${updatedLNodeTypeID} swapped!`; + if (updatedLNodeType && this.lNodeTypeUI) { + this.lNodeTypeUI.value = updatedLNodeType.getAttribute('id') ?? ''; } + + const updatedID = updatedLNodeType?.getAttribute('id') ?? lnID; + this.showSuccessFeedback(updatedID, 'swap'); } await this.updateComplete; this.lNodeTypes = getLNodeTypes(this.doc); + } + private showSuccessFeedback( + lnID: string, + mode: 'update' | 'swap' = 'update' + ): void { + this.fabLabel = mode === 'swap' ? `${lnID} swapped!` : `${lnID} updated!`; setTimeout(() => { this.fabLabel = 'Update Logical Node Type'; }, 5000); } + private buildUpdateEdits( + inserts: Edit[], + currentLNodeType: Element, + lnID: string, + desc: string + ): Edit[] { + const lNodeTypeInsert = inserts.find( + insert => + 'node' in insert && (insert.node as Element).tagName === 'LNodeType' + ); + + if ( + lNodeTypeInsert && + 'node' in lNodeTypeInsert && + 'parent' in lNodeTypeInsert && + 'reference' in lNodeTypeInsert + ) { + const newLNodeType = (lNodeTypeInsert.node as Element).cloneNode( + true + ) as Element; + + newLNodeType.setAttribute('id', lnID); + this.applyDescriptionUpdate(newLNodeType, desc, currentLNodeType); + + const supportingTypes = inserts.filter( + insert => insert !== lNodeTypeInsert + ); + const removeOld = removeDataType( + { node: currentLNodeType }, + { force: true } + ); + + return [ + ...supportingTypes, + { + parent: lNodeTypeInsert.parent, + node: newLNodeType, + reference: lNodeTypeInsert.reference, + }, + ...removeOld, + ]; + } + + if (inserts.length === 0) { + const newLNodeType = removeDOsNotInSelection( + currentLNodeType, + this.nsdSelection! + ); + + newLNodeType.setAttribute('id', lnID); + this.applyDescriptionUpdate(newLNodeType, desc, currentLNodeType); + + return updateLNodeType(newLNodeType, this.doc!); + } + + return inserts; + } + private updateLNodeTypeDescription(desc: string): void { + this.lNodeTypeDescription = desc; this.dispatchEvent( newEditEvent([ { element: this.selectedLNodeType!, - attributes: { desc }, + attributes: { desc: desc || null }, attributesNS: {}, }, ]) @@ -254,17 +386,35 @@ export default class NsdTemplateUpdated extends ScopedElementsMixin( this.saveTemplates(); } + private confirmDelete(): void { + if (!this.doc || !this.selectedLNodeType) return; + + const lnID = this.selectedLNodeType.getAttribute('id'); + const remove = removeDataType( + { node: this.selectedLNodeType }, + { force: true } + ); + + this.dispatchEvent(newEditEvent(remove, { title: `Delete ${lnID}` })); + + this.resetUI(true); + this.lNodeTypes = getLNodeTypes(this.doc); + } + private handleUpdateTemplate(): void { if (!this.doc || !this.selectedLNodeType) return; - this.nsdSelection = filterSelection( + const newNsdSelection = filterSelection( this.treeUI.tree, this.treeUI.selection ); + if (JSON.stringify(newNsdSelection) !== JSON.stringify(this.nsdSelection)) { + this.nsdSelection = newNsdSelection; + } + if ( - JSON.stringify(this.treeUI.selection) !== - JSON.stringify(this.nsdSelection) + JSON.stringify(this.treeUI.selection) !== JSON.stringify(newNsdSelection) ) { this.choiceDialog?.show(); return; @@ -278,6 +428,8 @@ export default class NsdTemplateUpdated extends ScopedElementsMixin( this.disableAddDataObjectButton = true; this.loading = true; this.selectedLNodeType = getSelectedLNodeType(this.doc!, id); + this.lNodeTypeDescription = + this.selectedLNodeType?.getAttribute('desc') ?? ''; // Let the browser render the loader before heavy work await new Promise(resolve => { setTimeout(resolve, 0); @@ -440,11 +592,19 @@ export default class NsdTemplateUpdated extends ScopedElementsMixin( add Add Data Object + this.deleteDialog.show()} + class="button-delete" + > + delete + Delete LNode Type + ${this.loading ? html`` @@ -467,6 +627,10 @@ export default class NsdTemplateUpdated extends ScopedElementsMixin( > ${this.renderFab()} ${this.renderWarning()} ${this.renderChoice()} + this.confirmDelete()} + >