diff --git a/src/editors/templates/datype-wizards.ts b/src/editors/templates/datype-wizards.ts index 56297cfb92..c26e1fa3cf 100644 --- a/src/editors/templates/datype-wizards.ts +++ b/src/editors/templates/datype-wizards.ts @@ -12,6 +12,7 @@ import { SingleSelectedEvent } from '@material/mwc-list/mwc-list-foundation'; import '../../wizard-textfield.js'; import { + cloneElement, Create, createElement, EditorAction, @@ -21,6 +22,7 @@ import { newSubWizardEvent, newWizardEvent, patterns, + Replace, selector, Wizard, WizardActor, @@ -31,9 +33,42 @@ import { addReferencedDataTypes, allDataTypeSelector, unifyCreateActionArray, - updateIDNamingAction, } from './foundation.js'; +function updateDATpyeAction(element: Element): WizardActor { + return (inputs: WizardInput[]): EditorAction[] => { + const id = getValue(inputs.find(i => i.label === 'id')!)!; + const desc = getValue(inputs.find(i => i.label === 'desc')!); + + if ( + id === element.getAttribute('id') && + desc === element.getAttribute('desc') + ) + return []; + + const newElement = cloneElement(element, { id, desc }); + + const actions: Replace[] = []; + actions.push({ old: { element }, new: { element: newElement } }); + + const oldId = element.getAttribute('id')!; + Array.from( + element.ownerDocument.querySelectorAll( + `DOType > DA[type="${oldId}"], DAType > BDA[type="${oldId}"]` + ) + ).forEach(oldDa => { + const newDa = oldDa.cloneNode(false); + newDa.setAttribute('type', id); + + actions.push({ old: { element: oldDa }, new: { element: newDa } }); + }); + + return [ + { title: get('datype.action.edit', { oldId, newId: id }), actions }, + ]; + }; +} + export function editDaTypeWizard( dATypeIdentity: string, doc: XMLDocument @@ -51,7 +86,7 @@ export function editDaTypeWizard( primary: { icon: '', label: get('save'), - action: updateIDNamingAction(datype), + action: updateDATpyeAction(datype), }, content: [ html` DO[type="${oldId}"], DOType > SDO[type="${oldId}"]` + ) + ).forEach(oldDo => { + const newDo = oldDo.cloneNode(false); + newDo.setAttribute('type', id); + + actions.push({ old: { element: oldDo }, new: { element: newDo } }); + }); + + return [ + { title: get('dotype.action.edit', { oldId, newId: id }), actions }, + ]; }; } diff --git a/src/editors/templates/enumtype-wizard.ts b/src/editors/templates/enumtype-wizard.ts index 8146cce622..7f055b9e1b 100644 --- a/src/editors/templates/enumtype-wizard.ts +++ b/src/editors/templates/enumtype-wizard.ts @@ -22,17 +22,13 @@ import { newSubWizardEvent, newWizardEvent, patterns, + Replace, selector, Wizard, WizardActor, WizardInput, } from '../../foundation.js'; -import { - CreateOptions, - updateIDNamingAction, - UpdateOptions, - WizardOptions, -} from './foundation.js'; +import { CreateOptions, UpdateOptions, WizardOptions } from './foundation.js'; function nextOrd(parent: Element): string { const maxOrd = Math.max( @@ -258,6 +254,38 @@ export function createEnumTypeWizard( ]; } +function updateEnumTpyeAction(element: Element): WizardActor { + return (inputs: WizardInput[]): EditorAction[] => { + const id = getValue(inputs.find(i => i.label === 'id')!)!; + const desc = getValue(inputs.find(i => i.label === 'desc')!); + + if ( + id === element.getAttribute('id') && + desc === element.getAttribute('desc') + ) + return []; + + const newElement = cloneElement(element, { id, desc }); + + const actions: Replace[] = []; + actions.push({ old: { element }, new: { element: newElement } }); + + const oldId = element.getAttribute('id')!; + Array.from( + element.ownerDocument.querySelectorAll( + `DOType > DA[type="${oldId}"], DAType > BDA[type="${oldId}"]` + ) + ).forEach(oldDa => { + const newDa = oldDa.cloneNode(false); + newDa.setAttribute('type', id); + + actions.push({ old: { element: oldDa }, new: { element: newDa } }); + }); + + return [{ title: get('enum.action.edit', { oldId, newId: id }), actions }]; + }; +} + export function eNumTypeEditWizard( eNumTypeIdentity: string, doc: XMLDocument @@ -272,7 +300,7 @@ export function eNumTypeEditWizard( primary: { icon: '', label: get('save'), - action: updateIDNamingAction(enumtype), + action: updateEnumTpyeAction(enumtype), }, content: [ html`options).parent !== undefined; } -export function updateIDNamingAction(element: Element): WizardActor { - return (inputs: WizardInput[]): EditorAction[] => { - const id = getValue(inputs.find(i => i.label === 'id')!)!; - const desc = getValue(inputs.find(i => i.label === 'desc')!); - - if ( - id === element.getAttribute('id') && - desc === element.getAttribute('desc') - ) - return []; - - const newElement = cloneElement(element, { id, desc }); - - return [{ old: { element }, new: { element: newElement } }]; - }; -} - function containsCreateAction(actions: Create[], newAction: Create): boolean { return !actions.some( action => diff --git a/src/editors/templates/lnodetype-wizard.ts b/src/editors/templates/lnodetype-wizard.ts index b0d5a10b72..61ba9bc451 100644 --- a/src/editors/templates/lnodetype-wizard.ts +++ b/src/editors/templates/lnodetype-wizard.ts @@ -26,6 +26,7 @@ import { newSubWizardEvent, newWizardEvent, patterns, + Replace, selector, Wizard, WizardActor, @@ -530,7 +531,24 @@ function updateLNodeTypeAction(element: Element): WizardActor { const newElement = cloneElement(element, { id, desc, lnClass }); - return [{ old: { element }, new: { element: newElement } }]; + const actions: Replace[] = []; + actions.push({ old: { element }, new: { element: newElement } }); + + const oldId = element.getAttribute('id')!; + Array.from( + element.ownerDocument.querySelectorAll( + `LN0[lnType="${oldId}"], LN[lnType="${oldId}"]` + ) + ).forEach(oldAnyLn => { + const newAnyLn = oldAnyLn.cloneNode(false); + newAnyLn.setAttribute('lnType', id); + + actions.push({ old: { element: oldAnyLn }, new: { element: newAnyLn } }); + }); + + return [ + { title: get('lnodetype.action.edit', { oldId, newId: id }), actions }, + ]; }; } diff --git a/src/translations/de.ts b/src/translations/de.ts index 72549aacea..f5f15e407c 100644 --- a/src/translations/de.ts +++ b/src/translations/de.ts @@ -303,6 +303,9 @@ export const de: Translations = { edit: 'EnumType bearbeiten', }, }, + action: { + edit: 'DAType ID "{{oldId}}" und deren DA-Referenzen geändert zu {{newId}} ', + }, }, datype: { wizard: { @@ -311,6 +314,9 @@ export const de: Translations = { edit: 'DAType bearbeiten', }, }, + action: { + edit: 'EnumType ID "{{oldId}}" und deren DA-Referenzen geändert zu {{newId}} ', + }, }, bda: { wizard: { @@ -352,6 +358,9 @@ export const de: Translations = { }, enums: 'Standard Enumerations', }, + action: { + edit: 'DOType ID "{{oldId}}" und deren DO-Referenzen geändert zu {{newId}} ', + }, }, lnodetype: { wizard: { @@ -361,6 +370,9 @@ export const de: Translations = { select: 'Data Objects auswählen', }, }, + action: { + edit: 'LNodeType ID "{{oldId}}" und deren LN-Referenzen geändert zu {{newId}} ', + }, autoimport: 'Vordefinierte OpenSCD LN Klasse verwenden', missinglnclass: 'Vordefinierte LN Klasse fehlt', }, diff --git a/src/translations/en.ts b/src/translations/en.ts index 2daf0055c4..9100bbe0a5 100644 --- a/src/translations/en.ts +++ b/src/translations/en.ts @@ -299,6 +299,9 @@ export const en = { edit: 'Edit EnumType', }, }, + action: { + edit: 'Change EnumType ID "{{oldId}}" and its DA references to {{newId}} ', + }, }, datype: { wizard: { @@ -307,6 +310,9 @@ export const en = { edit: 'Edit DAType', }, }, + action: { + edit: 'Change DAType ID "{{oldId}}" and its DA references to {{newId}} ', + }, }, bda: { wizard: { @@ -348,6 +354,9 @@ export const en = { }, enums: 'Default enumerations', }, + action: { + edit: 'Change DOType ID "{{oldId}}" and its DO references to {{newId}} ', + }, }, lnodetype: { wizard: { @@ -357,6 +366,9 @@ export const en = { select: 'Select Data Objects', }, }, + action: { + edit: 'Change LNodeType ID "{{oldId}}" and its LN references to {{newId}} ', + }, autoimport: 'Use LN class from OpenSCD template', missinglnclass: 'Missing pre-defined LN class', }, diff --git a/test/unit/editors/templates/datype.test.ts b/test/unit/editors/templates/datype.test.ts new file mode 100644 index 0000000000..e6baa9b70b --- /dev/null +++ b/test/unit/editors/templates/datype.test.ts @@ -0,0 +1,103 @@ +import { expect, fixture, html } from '@open-wc/testing'; +import { SinonSpy, spy } from 'sinon'; + +import '../../../mock-wizard.js'; +import { MockWizard } from '../../../mock-wizard.js'; + +import { + ComplexAction, + identity, + isSimple, + Replace, + WizardInput, +} from '../../../../src/foundation.js'; +import { editDaTypeWizard } from '../../../../src/editors/templates/datype-wizards.js'; + +describe('wizards for DAType element', () => { + let doc: XMLDocument; + let element: MockWizard; + let inputs: WizardInput[]; + let input: WizardInput | undefined; + + let primaryAction: HTMLElement; + + let actionEvent: SinonSpy; + + beforeEach(async () => { + element = await fixture(html``); + + actionEvent = spy(); + window.addEventListener('editor-action', actionEvent); + }); + + describe('include an edit wizard that', () => { + beforeEach(async () => { + doc = await fetch('/test/testfiles/valid2003.scd') + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); + + const wizard = editDaTypeWizard( + identity(doc.querySelector('DAType')), + doc + )!; + element.workflow.push(() => wizard); + await element.requestUpdate(); + + inputs = Array.from(element.wizardUI.inputs); + + primaryAction = ( + element.wizardUI.dialog?.querySelector( + 'mwc-button[slot="primaryAction"]' + ) + ); + }); + + describe('allows to edit id attribute', () => { + beforeEach(() => { + input = inputs.find(input => input.label === 'id'); + }); + + it('as wizard input', () => expect(input).to.exist); + + it('triggers a complex action', () => { + input!.value = 'someTestId'; + primaryAction.click(); + expect(actionEvent).to.be.calledOnce; + + const action = actionEvent.args[0][0].detail.action; + expect(action).to.not.satisfy(isSimple); + }); + + it('that edits the id attribute of DAType', () => { + input!.value = 'someTestId'; + primaryAction.click(); + + const complexAction = ( + actionEvent.args[0][0].detail.action + ); + const actions = complexAction.actions; + expect(actions[0].new.element).to.have.attribute('id', 'someTestId'); + }); + + it('that edits all referenced lnType attribute as well', () => { + const oldId = input?.value; + const numReferences = doc.querySelectorAll( + `DOType > DA[type="${oldId}"], DAType > BDA[type="${oldId}"]` + ).length; + + input!.value = 'someTestId'; + primaryAction.click(); + + const complexAction = ( + actionEvent.args[0][0].detail.action + ); + const actions = complexAction.actions; + expect(actions).to.have.lengthOf(numReferences + 1); + + actions.shift(); //the first updates the DAType itself and has no 'id' + for (const action of actions) + expect(action.new.element).to.have.attribute('type', 'someTestId'); + }); + }); + }); +}); diff --git a/test/unit/editors/templates/dotype.test.ts b/test/unit/editors/templates/dotype.test.ts new file mode 100644 index 0000000000..60adf03402 --- /dev/null +++ b/test/unit/editors/templates/dotype.test.ts @@ -0,0 +1,102 @@ +import { expect, fixture, html } from '@open-wc/testing'; +import { SinonSpy, spy } from 'sinon'; + +import '../../../mock-wizard.js'; +import { MockWizard } from '../../../mock-wizard.js'; + +import { + ComplexAction, + identity, + isSimple, + Replace, + WizardInput, +} from '../../../../src/foundation.js'; +import { dOTypeWizard } from '../../../../src/editors/templates/dotype-wizards.js'; + +describe('wizards for DOType element', () => { + let doc: XMLDocument; + let element: MockWizard; + let inputs: WizardInput[]; + let input: WizardInput | undefined; + + let primaryAction: HTMLElement; + + let actionEvent: SinonSpy; + + beforeEach(async () => { + element = await fixture(html``); + + actionEvent = spy(); + window.addEventListener('editor-action', actionEvent); + }); + + describe('include an edit wizard that', () => { + beforeEach(async () => { + doc = await fetch('/test/testfiles/valid2003.scd') + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); + + const wizard = dOTypeWizard( + identity(doc.querySelector('DOType')), + doc + )!; + element.workflow.push(() => wizard); + await element.requestUpdate(); + + inputs = Array.from(element.wizardUI.inputs); + + primaryAction = ( + element.wizardUI.dialog?.querySelector( + 'mwc-button[slot="primaryAction"]' + ) + ); + }); + + describe('allows to edit id attribute', () => { + beforeEach(() => { + input = inputs.find(input => input.label === 'id'); + }); + + it('as wizard input', () => expect(input).to.exist); + + it('triggers a complex action', () => { + input!.value = 'someTestId'; + primaryAction.click(); + expect(actionEvent).to.be.calledOnce; + + const action = actionEvent.args[0][0].detail.action; + expect(action).to.not.satisfy(isSimple); + }); + + it('that edits the id attribute of DOType', () => { + input!.value = 'someTestId'; + primaryAction.click(); + + const complexAction = ( + actionEvent.args[0][0].detail.action + ); + const actions = complexAction.actions; + expect(actions[0].new.element).to.have.attribute('id', 'someTestId'); + }); + + it('that edits all referenced lnType attribute as well', () => { + const oldId = input?.value; + const numReferences = doc.querySelectorAll( + `LNodeType > DO[type="${oldId}"], DOType > SDO[type="${oldId}"]` + ).length; + + input!.value = 'someTestId'; + primaryAction.click(); + + const complexAction = ( + actionEvent.args[0][0].detail.action + ); + const actions = complexAction.actions; + expect(actions).to.have.lengthOf(numReferences + 1); + actions.shift(); //the first updates the DOType itself and has no 'id' + for (const action of actions) + expect(action.new.element).to.have.attribute('type', 'someTestId'); + }); + }); + }); +}); diff --git a/test/unit/editors/templates/enumtype.test.ts b/test/unit/editors/templates/enumtype.test.ts new file mode 100644 index 0000000000..f0655b89e7 --- /dev/null +++ b/test/unit/editors/templates/enumtype.test.ts @@ -0,0 +1,103 @@ +import { expect, fixture, html } from '@open-wc/testing'; +import { SinonSpy, spy } from 'sinon'; + +import '../../../mock-wizard.js'; +import { MockWizard } from '../../../mock-wizard.js'; + +import { + ComplexAction, + identity, + isSimple, + Replace, + WizardInput, +} from '../../../../src/foundation.js'; +import { eNumTypeEditWizard } from '../../../../src/editors/templates/enumtype-wizard.js'; + +describe('wizards for EnumType element', () => { + let doc: XMLDocument; + let element: MockWizard; + let inputs: WizardInput[]; + let input: WizardInput | undefined; + + let primaryAction: HTMLElement; + + let actionEvent: SinonSpy; + + beforeEach(async () => { + element = await fixture(html``); + + actionEvent = spy(); + window.addEventListener('editor-action', actionEvent); + }); + + describe('include an edit wizard that', () => { + beforeEach(async () => { + doc = await fetch('/test/testfiles/valid2003.scd') + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); + + const wizard = eNumTypeEditWizard( + identity(doc.querySelector('EnumType')), + doc + )!; + element.workflow.push(() => wizard); + await element.requestUpdate(); + + inputs = Array.from(element.wizardUI.inputs); + + primaryAction = ( + element.wizardUI.dialog?.querySelector( + 'mwc-button[slot="primaryAction"]' + ) + ); + }); + + describe('allows to edit id attribute', () => { + beforeEach(() => { + input = inputs.find(input => input.label === 'id'); + }); + + it('as wizard input', () => expect(input).to.exist); + + it('triggers a complex action', () => { + input!.value = 'someTestId'; + primaryAction.click(); + expect(actionEvent).to.be.calledOnce; + + const action = actionEvent.args[0][0].detail.action; + expect(action).to.not.satisfy(isSimple); + }); + + it('that edits the id attribute of EnumType', () => { + input!.value = 'someTestId'; + primaryAction.click(); + + const complexAction = ( + actionEvent.args[0][0].detail.action + ); + const actions = complexAction.actions; + expect(actions[0].new.element).to.have.attribute('id', 'someTestId'); + }); + + it('that edits all referenced lnType attribute as well', () => { + const oldId = input?.value; + const numReferences = doc.querySelectorAll( + `DOType > DA[type="${oldId}"], DAType > BDA[type="${oldId}"]` + ).length; + + input!.value = 'someTestId'; + primaryAction.click(); + + const complexAction = ( + actionEvent.args[0][0].detail.action + ); + const actions = complexAction.actions; + expect(actions).to.have.lengthOf(numReferences + 1); + + actions.shift(); //the first updates the EnumType itself and has no 'id' + for (const action of actions) + expect(action.new.element).to.have.attribute('type', 'someTestId'); + }); + }); + }); +}); diff --git a/test/unit/editors/templates/lnodetype-wizard.test.ts b/test/unit/editors/templates/lnodetype-wizard.test.ts index 9fa020ff0e..dd68c455eb 100644 --- a/test/unit/editors/templates/lnodetype-wizard.test.ts +++ b/test/unit/editors/templates/lnodetype-wizard.test.ts @@ -1,10 +1,17 @@ import { expect, fixture, html } from '@open-wc/testing'; +import { SinonSpy, spy } from 'sinon'; import fc from 'fast-check'; import '../../../mock-wizard.js'; import { MockWizard } from '../../../mock-wizard.js'; -import { identity, WizardInput } from '../../../../src/foundation.js'; +import { + ComplexAction, + identity, + isSimple, + Replace, + WizardInput, +} from '../../../../src/foundation.js'; import { lNodeTypeWizard } from '../../../../src/editors/templates/lnodetype-wizard.js'; import { regExp, regexString } from '../../../foundation.js'; @@ -14,8 +21,15 @@ describe('wizards for LNodeType element', () => { let inputs: WizardInput[]; let input: WizardInput | undefined; + let primaryAction: HTMLElement; + + let actionEvent: SinonSpy; + beforeEach(async () => { element = await fixture(html``); + + actionEvent = spy(); + window.addEventListener('editor-action', actionEvent); }); describe('include an edit wizard that', () => { beforeEach(async () => { @@ -31,7 +45,14 @@ describe('wizards for LNodeType element', () => { await element.requestUpdate(); inputs = Array.from(element.wizardUI.inputs); + + primaryAction = ( + element.wizardUI.dialog?.querySelector( + 'mwc-button[slot="primaryAction"]' + ) + ); }); + describe('allows to edit lnClass attribute', () => { beforeEach(() => { input = inputs.find(input => input.label === 'lnClass'); @@ -57,5 +78,52 @@ describe('wizards for LNodeType element', () => { expect(input!.checkValidity()).to.be.true; }); }); + + describe('allows to edit id attribute', () => { + beforeEach(() => { + input = inputs.find(input => input.label === 'id'); + }); + + it('as wizard input', () => expect(input).to.exist); + + it('triggers a complex action', () => { + input!.value = 'someTestId'; + primaryAction.click(); + expect(actionEvent).to.be.calledOnce; + + const action = actionEvent.args[0][0].detail.action; + expect(action).to.not.satisfy(isSimple); + }); + + it('that edits the id attribute of LNodeType', () => { + input!.value = 'someTestId'; + primaryAction.click(); + + const complexAction = ( + actionEvent.args[0][0].detail.action + ); + const actions = complexAction.actions; + expect(actions[0].new.element).to.have.attribute('id', 'someTestId'); + }); + + it('that edits all referenced lnType attribute as well', () => { + const oldId = input?.value; + const numReferences = doc.querySelectorAll( + `LN0[lnType="${oldId}"], LN[lnType="${oldId}"]` + ).length; + + input!.value = 'someTestId'; + primaryAction.click(); + + const complexAction = ( + actionEvent.args[0][0].detail.action + ); + const actions = complexAction.actions; + expect(actions).to.have.lengthOf(numReferences + 1); + actions.shift(); //the first updates the LNodeType itself and has no 'id' + for (const action of actions) + expect(action.new.element).to.have.attribute('lnType', 'someTestId'); + }); + }); }); });