Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions components/delete-lnodetype-dialog.spec.ts
Original file line number Diff line number Diff line change
@@ -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`<delete-dialog
.lnodeTypeId=${'MMXU$oscd$_c53e78191fabefa3'}
.onConfirm=${confirmSpy}
></delete-dialog>`
);
});

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);
});
});
104 changes: 104 additions & 0 deletions components/delete-lnodetype-dialog.ts
Original file line number Diff line number Diff line change
@@ -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`
<md-dialog>
<div slot="headline">Confirm delete</div>
<div slot="content" class="delete-content">
Are you sure you want to delete Logical Node Type ${this.lnodeTypeId}?
This action may have severe consequences.
</div>
<div slot="actions">
<md-outlined-button class="button close" @click="${this.handleCancel}"
>Cancel</md-outlined-button
>
<md-outlined-button
class="button delete"
@click="${this.handleConfirm}"
>Delete</md-outlined-button
>
</div>
</md-dialog>
`;
}

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);
}
`;
}
88 changes: 88 additions & 0 deletions foundation/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -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(
`<LNodeType id="TestLNodeType" lnClass="MMXU">
<DO name="Beh" type="ENS_1"/>
<DO name="A" type="WYE_1"/>
<DO name="PhV" type="WYE_2"/>
</LNodeType>`,
'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');
});
});
});
30 changes: 30 additions & 0 deletions foundation/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Loading