From ccdfa482f4cb2ecd0d7da1b990762c419ef37a2b Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Fri, 4 Oct 2024 14:30:28 +0200 Subject: [PATCH 01/33] WIP: Add edit api v2 support --- packages/openscd/src/addons/Editor.ts | 138 +++++++++++++++++++++++ packages/openscd/src/plugins.ts | 7 ++ packages/plugins/src/editors/EditTest.ts | 100 ++++++++++++++++ 3 files changed, 245 insertions(+) create mode 100644 packages/plugins/src/editors/EditTest.ts diff --git a/packages/openscd/src/addons/Editor.ts b/packages/openscd/src/addons/Editor.ts index c1ab8901a9..5b2ff6dbb6 100644 --- a/packages/openscd/src/addons/Editor.ts +++ b/packages/openscd/src/addons/Editor.ts @@ -32,6 +32,22 @@ import { isUpdate, } from '@openscd/core/foundation/deprecated/editor.js'; +import { + AttributeValue as AttributeValueV2, + Edit as EditV2, + EditEvent as EditEventV2, + Insert as InsertV2, + isComplex as isComplexV2, + isInsert as isInsertV2, + isNamespaced as isNamespacedV2, + isRemove as isRemoveV2, + isUpdate as isUpdateV2, + LitElementConstructor, + OpenEvent as OpenEventV2, + Remove as RemoveV2, + Update as UpdateV2, +} from '@openscd/core'; + @customElement('oscd-editor') export class OscdEditor extends LitElement { /** The `XMLDocument` to be edited */ @@ -473,9 +489,131 @@ export class OscdEditor extends LitElement { this.host.addEventListener('editor-action', this.onAction.bind(this)); this.host.addEventListener('open-doc', this.onOpenDoc); this.host.addEventListener('oscd-open', this.handleOpenDoc); + + // TODO: Test v2 API + this.addEventListener('oscd-edit', event => this.handleEditEventV2(event)); } render(): TemplateResult { return html``; } + + // Test API v2 start + handleEditEventV2(event: EditEventV2) { + console.log('Edit V2', event); + const edit = event.detail.edit; + // this.history.splice(this.editCount); + // this.history.push({ undo: handleEdit(edit), redo: edit }); + handleEditV2(edit); + this.editCount += 1; + } + + /** Undo the last `n` [[Edit]]s committed */ + undo(n = 1) { + // if (!this.canUndo || n < 1) return; + // handleEdit(this.history[this.last!].undo); + this.editCount -= 1; + if (n > 1) this.undo(n - 1); + } + + /** Redo the last `n` [[Edit]]s that have been undone */ + redo(n = 1) { + // if (!this.canRedo || n < 1) return; + // handleEdit(this.history[this.editCount].redo); + this.editCount += 1; + if (n > 1) this.redo(n - 1); + } + + // Test API v2 end +} + +function handleEditV2(edit: EditV2): EditV2 { + if (isInsertV2(edit)) return handleInsertV2(edit); + if (isUpdateV2(edit)) return handleUpdateV2(edit); + if (isRemoveV2(edit)) return handleRemoveV2(edit); + if (isComplexV2(edit)) return edit.map(handleEditV2).reverse(); + return []; } + +function localAttributeName(attribute: string): string { + return attribute.includes(':') ? attribute.split(':', 2)[1] : attribute; +} + +function handleInsertV2({ + parent, + node, + reference, +}: InsertV2): InsertV2 | RemoveV2 | [] { + try { + const { parentNode, nextSibling } = node; + parent.insertBefore(node, reference); + if (parentNode) + return { + node, + parent: parentNode, + reference: nextSibling, + }; + return { node }; + } catch (e) { + // do nothing if insert doesn't work on these nodes + return []; + } +} + +function handleUpdateV2({ element, attributes }: UpdateV2): UpdateV2 { + const oldAttributes = { ...attributes }; + Object.entries(attributes) + .reverse() + .forEach(([name, value]) => { + let oldAttribute: AttributeValueV2; + if (isNamespacedV2(value!)) + oldAttribute = { + value: element.getAttributeNS( + value.namespaceURI, + localAttributeName(name) + ), + namespaceURI: value.namespaceURI, + }; + else + oldAttribute = element.getAttributeNode(name)?.namespaceURI + ? { + value: element.getAttribute(name), + namespaceURI: element.getAttributeNode(name)!.namespaceURI!, + } + : element.getAttribute(name); + oldAttributes[name] = oldAttribute; + }); + for (const entry of Object.entries(attributes)) { + try { + const [attribute, value] = entry as [string, AttributeValueV2]; + if (isNamespacedV2(value)) { + if (value.value === null) + element.removeAttributeNS( + value.namespaceURI, + localAttributeName(attribute) + ); + else element.setAttributeNS(value.namespaceURI, attribute, value.value); + } else if (value === null) element.removeAttribute(attribute); + else element.setAttribute(attribute, value); + } catch (e) { + // do nothing if update doesn't work on this attribute + delete oldAttributes[entry[0]]; + } + } + return { + element, + attributes: oldAttributes, + }; +} + +function handleRemoveV2({ node }: RemoveV2): InsertV2 | [] { + const { parentNode: parent, nextSibling: reference } = node; + node.parentNode?.removeChild(node); + if (parent) + return { + node, + parent, + reference, + }; + return []; +} \ No newline at end of file diff --git a/packages/openscd/src/plugins.ts b/packages/openscd/src/plugins.ts index 62f5ed915d..9f14f354f6 100644 --- a/packages/openscd/src/plugins.ts +++ b/packages/openscd/src/plugins.ts @@ -3,6 +3,13 @@ function generatePluginPath(plugin: string): string { } export const officialPlugins = [ + { + name: 'EditTest', + src: generatePluginPath('plugins/src/editors/EditTest.js'), + icon: 'developer_board', + default: true, + kind: 'editor', + }, { name: 'IED', src: generatePluginPath('plugins/src/editors/IED.js'), diff --git a/packages/plugins/src/editors/EditTest.ts b/packages/plugins/src/editors/EditTest.ts new file mode 100644 index 0000000000..229a65dd70 --- /dev/null +++ b/packages/plugins/src/editors/EditTest.ts @@ -0,0 +1,100 @@ +import { newEditEvent } from '@openscd/core'; +import { css, html, LitElement, property, TemplateResult } from 'lit-element'; + + +export default class EditTest extends LitElement { + /** The document being edited as provided to plugins by [[`OpenSCD`]]. */ + @property() + doc!: XMLDocument; + @property({ type: Number }) + editCount = -1; + + create(): void { + console.log('Create'); + + const substation = this.doc.querySelector('Substation'); + + const element = this.doc.createElement('VoltageLevel'); + element.setAttribute('name', 'Test_1'); + + const event = newEditEvent({ + parent: substation, + node: element + }) + this.dispatchEvent(event) + } + + remove(): void { + console.log('Remove'); + + const testVL = this.doc.querySelector('VoltageLevel[name="Test_1"]'); + + if (testVL) { + const event = newEditEvent({ + node: testVL + }); + this.dispatchEvent(event); + } + } + + edit(): void { + const testVL = this.doc.querySelector('VoltageLevel[name="Test_1"]'); + + if (testVL) { + const event = newEditEvent({ + element: testVL, + attributes: { + test: 'new attribute created' + } + }); + this.dispatchEvent(event); + } + } + + multiedit(): void { + const testVL = this.doc.querySelector('VoltageLevel[name="Test_1"]'); + + if (testVL) { + + const event = newEditEvent([ + { + element: testVL, + attributes: { + event1: 'new data 1' + } + }, + { + element: testVL, + attributes: { + event2: 'new data 2' + } + } + ]); + this.dispatchEvent(event); + } + } + + render(): TemplateResult { + return html` +
+

Edittest

+
+ this.create()}> + this.remove()}> + this.edit()}> + this.multiedit()}> +
+
+ `; + } + + static styles = css` + :host { + width: 100vw; + } + + .edit { + padding: 48px; + } + }`; +} \ No newline at end of file From 44255c36a7aa43afe21096f86ff01ca210afff5a Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Thu, 10 Oct 2024 14:30:31 +0200 Subject: [PATCH 02/33] feat: WIP Convert v1 to v2 events --- packages/core/foundation.ts | 3 + .../foundation/edit-v1-to-v2-converter.ts | 96 +++++++++++++++++++ packages/openscd/src/addons/Editor.ts | 21 +++- 3 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 packages/core/foundation/edit-v1-to-v2-converter.ts diff --git a/packages/core/foundation.ts b/packages/core/foundation.ts index 466136ed83..d06a67cfb4 100644 --- a/packages/core/foundation.ts +++ b/packages/core/foundation.ts @@ -23,6 +23,9 @@ export type { Update, Remove, } from './foundation/edit-event.js'; +export { + convertEditV1toV2 +} from './foundation/edit-v1-to-v2-converter.js'; export { cyrb64 } from './foundation/cyrb64.js'; diff --git a/packages/core/foundation/edit-v1-to-v2-converter.ts b/packages/core/foundation/edit-v1-to-v2-converter.ts new file mode 100644 index 0000000000..af2fb7aec8 --- /dev/null +++ b/packages/core/foundation/edit-v1-to-v2-converter.ts @@ -0,0 +1,96 @@ +import { Create, Delete, EditorAction, isCreate, isDelete, isMove, isReplace, isSimple, isUpdate, Move, Replace, SimpleAction, Update } from './deprecated/editor.js'; +import { Edit, Insert, Remove, Update as UpdateV2 } from './edit-event.js'; +import { getReference, SCLTag } from '@openscd/open-scd/src/foundation.js'; + + +export function convertEditV1toV2(action: EditorAction): Edit { + if (isSimple(action)) { + return convertSimpleAction(action); + } else { + return action.actions.map(convertSimpleAction); + } +} + +function convertSimpleAction(action: SimpleAction): Edit { + if (isCreate(action)) { + return convertCreate(action); + } else if (isDelete(action)) { + return convertDelete(action); + } else if (isUpdate(action)) { + return convertUpdate(action); + } else if (isMove(action)) { + return convertMove(action); + } else if (isReplace(action)) { + return convertReplace(action); + } + + throw new Error('Unknown action type'); +} + +function convertCreate(action: Create): Insert { + let reference: Node | null = null; + if ( + action.new.reference === undefined && + action.new.element instanceof Element && + action.new.parent instanceof Element + ) { + reference = getReference( + action.new.parent, + action.new.element.tagName + ); + } else { + reference = action.new.reference ?? null; + } + + return { + parent: action.new.parent, + node: action.new.element, + reference + }; +} + +function convertDelete(action: Delete): Remove { + return { + node: action.old.element + }; +} + +function convertUpdate(action: Update): UpdateV2 { + return { + element: action.element, + attributes: action.newAttributes + }; +} + +function convertMove(action: Move): Insert { + return { + parent: action.new.parent, + node: action.old.element, + reference: action.new.reference ?? null + } +} + +function convertReplace(action: Replace): Edit { + const oldChildren = action.old.element.children; + const newNode = action.new.element.cloneNode() as Element; + newNode.append(...Array.from(oldChildren)); + const parent = action.old.element.parentElement; + + if (!parent) { + throw new Error('Replace action called without parent in old element'); + } + + const reference = action.old.element.nextSibling; + + const remove: Remove = { node: action.old.element }; + const insert: Insert = { + parent, + node: newNode, + reference + }; + + return [ + remove, + insert + ]; +} diff --git a/packages/openscd/src/addons/Editor.ts b/packages/openscd/src/addons/Editor.ts index 5b2ff6dbb6..41b8685907 100644 --- a/packages/openscd/src/addons/Editor.ts +++ b/packages/openscd/src/addons/Editor.ts @@ -1,4 +1,4 @@ -import { OpenEvent, newEditCompletedEvent } from '@openscd/core'; +import { OpenEvent, newEditCompletedEvent, newEditEvent } from '@openscd/core'; import { property, LitElement, @@ -48,6 +48,8 @@ import { Update as UpdateV2, } from '@openscd/core'; +import { convertEditV1toV2 } from '@openscd/core'; + @customElement('oscd-editor') export class OscdEditor extends LitElement { /** The `XMLDocument` to be edited */ @@ -431,7 +433,18 @@ export class OscdEditor extends LitElement { else if (isUpdate(action)) this.logUpdate(action as Update); } - private async onAction(event: EditorActionEvent) { + private onAction(event: EditorActionEvent) { + console.log('old event', event); + + const editV2 = convertEditV1toV2(event.detail.action); + const initiator = event.detail.initiator; + + console.log('dispatching new event', editV2); + + this.host.dispatchEvent(newEditEvent(editV2, initiator)); + } + + private async onActionOld(event: EditorActionEvent) { if (isSimple(event.detail.action)) { if (this.onSimpleAction(event.detail.action)) this.logSimpleAction(event.detail.action); @@ -491,7 +504,7 @@ export class OscdEditor extends LitElement { this.host.addEventListener('oscd-open', this.handleOpenDoc); // TODO: Test v2 API - this.addEventListener('oscd-edit', event => this.handleEditEventV2(event)); + this.host.addEventListener('oscd-edit', event => this.handleEditEventV2(event)); } render(): TemplateResult { @@ -528,6 +541,8 @@ export class OscdEditor extends LitElement { } function handleEditV2(edit: EditV2): EditV2 { + console.log('Edit V2 event', edit); + if (isInsertV2(edit)) return handleInsertV2(edit); if (isUpdateV2(edit)) return handleUpdateV2(edit); if (isRemoveV2(edit)) return handleRemoveV2(edit); From e2060ccaed8adb37423188c7fa05887b43478adf Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Fri, 11 Oct 2024 16:06:51 +0200 Subject: [PATCH 03/33] feat: Dispatch edit completed --- packages/openscd/src/addons/Editor.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/openscd/src/addons/Editor.ts b/packages/openscd/src/addons/Editor.ts index 41b8685907..abb696a208 100644 --- a/packages/openscd/src/addons/Editor.ts +++ b/packages/openscd/src/addons/Editor.ts @@ -519,6 +519,10 @@ export class OscdEditor extends LitElement { // this.history.push({ undo: handleEdit(edit), redo: edit }); handleEditV2(edit); this.editCount += 1; + + this.dispatchEvent( + newEditCompletedEvent(event.detail.edit, event.detail.initiator) + ); } /** Undo the last `n` [[Edit]]s committed */ From 1cae3d5a22c06b05ffe5271310cb3ba49dd8ed05 Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Mon, 14 Oct 2024 14:11:35 +0200 Subject: [PATCH 04/33] feat: WIP Handle undo and redo events --- .../core/foundation/deprecated/history.ts | 5 ++-- packages/openscd/src/addons/Editor.ts | 24 +++++++++++++-- packages/openscd/src/addons/History.ts | 29 +++++++++++++++---- 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/packages/core/foundation/deprecated/history.ts b/packages/core/foundation/deprecated/history.ts index 2c128be2ef..d6c6d6c648 100644 --- a/packages/core/foundation/deprecated/history.ts +++ b/packages/core/foundation/deprecated/history.ts @@ -1,4 +1,4 @@ -import { EditorAction } from './editor'; +import { Edit } from '../edit-event.js'; type InfoEntryKind = 'info' | 'warning' | 'error'; @@ -12,7 +12,8 @@ export interface LogDetailBase { /** The [[`LogEntry`]] for a committed [[`EditorAction`]]. */ export interface CommitDetail extends LogDetailBase { kind: 'action'; - action: EditorAction; + redo: Edit; + undo: Edit; } /** A [[`LogEntry`]] for notifying the user. */ export interface InfoDetail extends LogDetailBase { diff --git a/packages/openscd/src/addons/Editor.ts b/packages/openscd/src/addons/Editor.ts index abb696a208..ca962d738b 100644 --- a/packages/openscd/src/addons/Editor.ts +++ b/packages/openscd/src/addons/Editor.ts @@ -159,6 +159,7 @@ export class OscdEditor extends LitElement { } private logCreate(action: Create) { + /* TODO: Remove const name = action.new.element instanceof Element ? action.new.element.tagName @@ -171,6 +172,7 @@ export class OscdEditor extends LitElement { action, }) ); + */ } private onDelete(action: Delete) { @@ -184,6 +186,7 @@ export class OscdEditor extends LitElement { } private logDelete(action: Delete) { + /* TODO: Remove const name = action.old.element instanceof Element ? action.old.element.tagName @@ -196,6 +199,7 @@ export class OscdEditor extends LitElement { action, }) ); + */ } private checkMoveValidity(move: Move): boolean { @@ -245,7 +249,7 @@ export class OscdEditor extends LitElement { } private logMove(action: Move) { - this.dispatchEvent( + /* TODO: Remove newLogEvent({ kind: 'action', title: get('editing.moved', { @@ -254,6 +258,7 @@ export class OscdEditor extends LitElement { action: action, }) ); + */ } private checkReplaceValidity(replace: Replace): boolean { @@ -329,6 +334,7 @@ export class OscdEditor extends LitElement { } private logUpdate(action: Replace | Update) { + /* TODO: Remove const name = isReplace(action) ? action.new.element.tagName : (action as Update).element.tagName; @@ -342,6 +348,7 @@ export class OscdEditor extends LitElement { action: action, }) ); + */ } private checkUpdateValidity(update: Update): boolean { @@ -444,6 +451,7 @@ export class OscdEditor extends LitElement { this.host.dispatchEvent(newEditEvent(editV2, initiator)); } + /* TODO: Remove private async onActionOld(event: EditorActionEvent) { if (isSimple(event.detail.action)) { if (this.onSimpleAction(event.detail.action)) @@ -469,6 +477,7 @@ export class OscdEditor extends LitElement { newEditCompletedEvent(event.detail.action, event.detail.initiator) ); } + */ /** * @@ -517,12 +526,23 @@ export class OscdEditor extends LitElement { const edit = event.detail.edit; // this.history.splice(this.editCount); // this.history.push({ undo: handleEdit(edit), redo: edit }); - handleEditV2(edit); + const undoEdit = handleEditV2(edit); this.editCount += 1; this.dispatchEvent( newEditCompletedEvent(event.detail.edit, event.detail.initiator) ); + + const isUserEvent = event.detail.initiator === 'user'; + + if (isUserEvent) { + this.dispatchEvent(newLogEvent({ + kind: 'action', + title: 'Edit V2', + redo: edit, + undo: undoEdit, + })); + } } /** Undo the last `n` [[Edit]]s committed */ diff --git a/packages/openscd/src/addons/History.ts b/packages/openscd/src/addons/History.ts index eac9e5b1ab..21160becee 100644 --- a/packages/openscd/src/addons/History.ts +++ b/packages/openscd/src/addons/History.ts @@ -43,6 +43,7 @@ import { import { getFilterIcon, iconColors } from '../icons/icons.js'; import { Plugin } from '../open-scd.js'; +import { newEditEvent } from '@openscd/core'; const icons = { info: 'info', @@ -197,18 +198,28 @@ export class OscdHistory extends LitElement { undo(): boolean { if (!this.canUndo) return false; - const invertedAction = invert( - (this.history[this.editCount]).action - ); - this.dispatchEvent(newActionEvent(invertedAction, 'undo')); + + console.log(this.history); + + const undoEdit = (this.history[this.editCount]).undo; + this.dispatchEvent(newEditEvent(undoEdit, 'undo')); this.editCount = this.previousAction; + + console.log('Undo - Setting edit count to ', this.editCount); + return true; } redo(): boolean { if (!this.canRedo) return false; - const nextAction = (this.history[this.nextAction]).action; - this.dispatchEvent(newActionEvent(nextAction, 'redo')); + + console.log(this.history); + + const redoEdit = (this.history[this.nextAction]).redo; + this.dispatchEvent(newEditEvent(redoEdit, 'redo')); this.editCount = this.nextAction; + + console.log('Redo - Setting edit count to ', this.editCount); + return true; } @@ -219,10 +230,16 @@ export class OscdHistory extends LitElement { }; if (entry.kind === 'action') { + console.log('Edit ', detail); + + if (this.nextAction !== -1) this.history.splice(this.nextAction); + this.editCount = this.history.length; + /* if (entry.action.derived) return; entry.action.derived = true; if (this.nextAction !== -1) this.history.splice(this.nextAction); this.editCount = this.history.length; + */ } this.history.push(entry); From a2f6552e35cdea477258d1e8c0205f15fce6d7e2 Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Thu, 17 Oct 2024 14:11:21 +0200 Subject: [PATCH 05/33] feat: Add log text and remove old code --- packages/openscd/src/addons/Editor.ts | 435 ++---------------------- packages/openscd/src/translations/de.ts | 1 + packages/openscd/src/translations/en.ts | 1 + 3 files changed, 24 insertions(+), 413 deletions(-) diff --git a/packages/openscd/src/addons/Editor.ts b/packages/openscd/src/addons/Editor.ts index ca962d738b..3baae24646 100644 --- a/packages/openscd/src/addons/Editor.ts +++ b/packages/openscd/src/addons/Editor.ts @@ -70,374 +70,26 @@ export class OscdEditor extends LitElement { }) editCount = -1; - private checkCreateValidity(create: Create): boolean { - if (create.checkValidity !== undefined) return create.checkValidity(); - - if ( - !(create.new.element instanceof Element) || - !(create.new.parent instanceof Element) - ) - return true; - - const invalidNaming = - create.new.element.hasAttribute('name') && - Array.from(create.new.parent.children).some( - elm => - elm.tagName === (create.new.element).tagName && - elm.getAttribute('name') === - (create.new.element).getAttribute('name') - ); - - if (invalidNaming) { - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.create', { - name: create.new.element.tagName, - }), - message: get('editing.error.nameClash', { - parent: - create.new.parent instanceof HTMLElement - ? create.new.parent.tagName - : 'Document', - child: create.new.element.tagName, - name: create.new.element.getAttribute('name')!, - }), - }) - ); - - return false; + private getLogText(edit: EditV2): { title: string, message?: string } { + if (isInsertV2(edit)) { + const name = edit.node instanceof Element ? + edit.node.tagName : + get('editing.node'); + return { title: get('editing.created', { name }) }; + } else if (isUpdateV2(edit)) { + const name = edit.element.tagName; + return { title: get('editing.updated', { name }) }; + } else if (isRemoveV2(edit)) { + const name = edit.node instanceof Element ? + edit.node.tagName : + get('editing.node'); + return { title: get('editing.deleted', { name }) }; + } else if (isComplexV2(edit)) { + const message = edit.map(this.getLogText).map(({ title }) => title).join(', '); + return { title: get('editing.complex'), message }; } - const invalidId = - create.new.element.hasAttribute('id') && - Array.from( - create.new.parent.ownerDocument.querySelectorAll( - 'LNodeType, DOType, DAType, EnumType' - ) - ).some( - elm => - elm.getAttribute('id') === - (create.new.element).getAttribute('id') - ); - - if (invalidId) { - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.create', { - name: create.new.element.tagName, - }), - message: get('editing.error.idClash', { - id: create.new.element.getAttribute('id')!, - }), - }) - ); - - return false; - } - - return true; - } - - private onCreate(action: Create) { - if (!this.checkCreateValidity(action)) return false; - - if ( - action.new.reference === undefined && - action.new.element instanceof Element && - action.new.parent instanceof Element - ) - action.new.reference = getReference( - action.new.parent, - action.new.element.tagName - ); - else action.new.reference = action.new.reference ?? null; - - action.new.parent.insertBefore(action.new.element, action.new.reference); - return true; - } - - private logCreate(action: Create) { - /* TODO: Remove - const name = - action.new.element instanceof Element - ? action.new.element.tagName - : get('editing.node'); - - this.dispatchEvent( - newLogEvent({ - kind: 'action', - title: get('editing.created', { name }), - action, - }) - ); - */ - } - - private onDelete(action: Delete) { - if (!action.old.reference) - action.old.reference = action.old.element.nextSibling; - - if (action.old.element.parentNode !== action.old.parent) return false; - - action.old.parent.removeChild(action.old.element); - return true; - } - - private logDelete(action: Delete) { - /* TODO: Remove - const name = - action.old.element instanceof Element - ? action.old.element.tagName - : get('editing.node'); - - this.dispatchEvent( - newLogEvent({ - kind: 'action', - title: get('editing.deleted', { name }), - action, - }) - ); - */ - } - - private checkMoveValidity(move: Move): boolean { - if (move.checkValidity !== undefined) return move.checkValidity(); - - const invalid = - move.old.element.hasAttribute('name') && - move.new.parent !== move.old.parent && - Array.from(move.new.parent.children).some( - elm => - elm.tagName === move.old.element.tagName && - elm.getAttribute('name') === move.old.element.getAttribute('name') - ); - - if (invalid) - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.move', { - name: move.old.element.tagName, - }), - message: get('editing.error.nameClash', { - parent: move.new.parent.tagName, - child: move.old.element.tagName, - name: move.old.element.getAttribute('name')!, - }), - }) - ); - - return !invalid; - } - - private onMove(action: Move) { - if (!this.checkMoveValidity(action)) return false; - - if (!action.old.reference) - action.old.reference = action.old.element.nextSibling; - - if (action.new.reference === undefined) - action.new.reference = getReference( - action.new.parent, - action.old.element.tagName - ); - - action.new.parent.insertBefore(action.old.element, action.new.reference); - return true; - } - - private logMove(action: Move) { - /* TODO: Remove - newLogEvent({ - kind: 'action', - title: get('editing.moved', { - name: action.old.element.tagName, - }), - action: action, - }) - ); - */ - } - - private checkReplaceValidity(replace: Replace): boolean { - if (replace.checkValidity !== undefined) return replace.checkValidity(); - - const invalidNaming = - replace.new.element.hasAttribute('name') && - replace.new.element.getAttribute('name') !== - replace.old.element.getAttribute('name') && - Array.from(replace.old.element.parentElement?.children ?? []).some( - elm => - elm.tagName === replace.new.element.tagName && - elm.getAttribute('name') === replace.new.element.getAttribute('name') - ); - - if (invalidNaming) { - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.update', { - name: replace.new.element.tagName, - }), - message: get('editing.error.nameClash', { - parent: replace.old.element.parentElement!.tagName, - child: replace.new.element.tagName, - name: replace.new.element.getAttribute('name')!, - }), - }) - ); - - return false; - } - - const invalidId = - replace.new.element.hasAttribute('id') && - replace.new.element.getAttribute('id') !== - replace.old.element.getAttribute('id') && - Array.from( - replace.new.element.ownerDocument.querySelectorAll( - 'LNodeType, DOType, DAType, EnumType' - ) - ).some( - elm => - elm.getAttribute('id') === - (replace.new.element).getAttribute('id') - ); - - if (invalidId) { - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.update', { - name: replace.new.element.tagName, - }), - message: get('editing.error.idClash', { - id: replace.new.element.getAttribute('id')!, - }), - }) - ); - - return false; - } - - return true; - } - - private onReplace(action: Replace) { - if (!this.checkReplaceValidity(action)) return false; - - action.new.element.append(...Array.from(action.old.element.children)); - action.old.element.replaceWith(action.new.element); - return true; - } - - private logUpdate(action: Replace | Update) { - /* TODO: Remove - const name = isReplace(action) - ? action.new.element.tagName - : (action as Update).element.tagName; - - this.dispatchEvent( - newLogEvent({ - kind: 'action', - title: get('editing.updated', { - name, - }), - action: action, - }) - ); - */ - } - - private checkUpdateValidity(update: Update): boolean { - if (update.checkValidity !== undefined) return update.checkValidity(); - - if (update.oldAttributes['name'] !== update.newAttributes['name']) { - const invalidNaming = Array.from( - update.element.parentElement?.children ?? [] - ).some( - elm => - elm.tagName === update.element.tagName && - elm.getAttribute('name') === update.newAttributes['name'] - ); - - if (invalidNaming) { - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.update', { - name: update.element.tagName, - }), - message: get('editing.error.nameClash', { - parent: update.element.parentElement!.tagName, - child: update.element.tagName, - name: update.newAttributes['name']!, - }), - }) - ); - - return false; - } - } - - const invalidId = - update.newAttributes['id'] && - Array.from( - update.element.ownerDocument.querySelectorAll( - 'LNodeType, DOType, DAType, EnumType' - ) - ).some(elm => elm.getAttribute('id') === update.newAttributes['id']); - - if (invalidId) { - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.update', { - name: update.element.tagName, - }), - message: get('editing.error.idClash', { - id: update.newAttributes['id']!, - }), - }) - ); - - return false; - } - - return true; - } - - private onUpdate(action: Update) { - if (!this.checkUpdateValidity(action)) return false; - - Array.from(action.element.attributes).forEach(attr => - action.element.removeAttributeNode(attr) - ); - - Object.entries(action.newAttributes).forEach(([key, value]) => { - if (value !== null && value !== undefined) - action.element.setAttribute(key, value); - }); - - return true; - } - - private onSimpleAction(action: SimpleAction) { - if (isMove(action)) return this.onMove(action as Move); - else if (isCreate(action)) return this.onCreate(action as Create); - else if (isDelete(action)) return this.onDelete(action as Delete); - else if (isReplace(action)) return this.onReplace(action as Replace); - else if (isUpdate(action)) return this.onUpdate(action as Update); - } - - private logSimpleAction(action: SimpleAction) { - if (isMove(action)) this.logMove(action as Move); - else if (isCreate(action)) this.logCreate(action as Create); - else if (isDelete(action)) this.logDelete(action as Delete); - else if (isReplace(action)) this.logUpdate(action as Replace); - else if (isUpdate(action)) this.logUpdate(action as Update); + return { title: '' }; } private onAction(event: EditorActionEvent) { @@ -451,34 +103,6 @@ export class OscdEditor extends LitElement { this.host.dispatchEvent(newEditEvent(editV2, initiator)); } - /* TODO: Remove - private async onActionOld(event: EditorActionEvent) { - if (isSimple(event.detail.action)) { - if (this.onSimpleAction(event.detail.action)) - this.logSimpleAction(event.detail.action); - } else if (event.detail.action.actions.length > 0) { - event.detail.action.actions.forEach(element => - this.onSimpleAction(element) - ); - this.dispatchEvent( - newLogEvent({ - kind: 'action', - title: event.detail.action.title, - action: event.detail.action, - }) - ); - } else return; - - if (!this.doc) return; - - await this.updateComplete; - this.dispatchEvent(newValidateEvent()); - this.dispatchEvent( - newEditCompletedEvent(event.detail.action, event.detail.initiator) - ); - } - */ - /** * * @deprecated [Move to handleOpenDoc instead] @@ -524,8 +148,6 @@ export class OscdEditor extends LitElement { handleEditEventV2(event: EditEventV2) { console.log('Edit V2', event); const edit = event.detail.edit; - // this.history.splice(this.editCount); - // this.history.push({ undo: handleEdit(edit), redo: edit }); const undoEdit = handleEditV2(edit); this.editCount += 1; @@ -536,31 +158,18 @@ export class OscdEditor extends LitElement { const isUserEvent = event.detail.initiator === 'user'; if (isUserEvent) { + const { title, message } = this.getLogText(edit); + this.dispatchEvent(newLogEvent({ kind: 'action', - title: 'Edit V2', + title, + message, redo: edit, undo: undoEdit, })); } } - /** Undo the last `n` [[Edit]]s committed */ - undo(n = 1) { - // if (!this.canUndo || n < 1) return; - // handleEdit(this.history[this.last!].undo); - this.editCount -= 1; - if (n > 1) this.undo(n - 1); - } - - /** Redo the last `n` [[Edit]]s that have been undone */ - redo(n = 1) { - // if (!this.canRedo || n < 1) return; - // handleEdit(this.history[this.editCount].redo); - this.editCount += 1; - if (n > 1) this.redo(n - 1); - } - // Test API v2 end } diff --git a/packages/openscd/src/translations/de.ts b/packages/openscd/src/translations/de.ts index 81d1e88891..67e314ede6 100644 --- a/packages/openscd/src/translations/de.ts +++ b/packages/openscd/src/translations/de.ts @@ -117,6 +117,7 @@ export const de: Translations = { moved: '{{ name }} verschoben', updated: '{{ name }} bearbeitet', import: '{{name}} importiert', + complex: 'Mehrere Elemente bearbeitet', error: { create: 'Konnte {{ name }} nicht hinzufügen', update: 'Konnte {{ name }} nicht bearbeiten', diff --git a/packages/openscd/src/translations/en.ts b/packages/openscd/src/translations/en.ts index 156be665bb..ca983fd40d 100644 --- a/packages/openscd/src/translations/en.ts +++ b/packages/openscd/src/translations/en.ts @@ -115,6 +115,7 @@ export const en = { moved: 'Moved {{ name }}', updated: 'Edited {{ name }}', import: 'Imported {{name}}', + complex: 'Multiple elements edited', error: { create: 'Could not add {{ name }}', update: 'Could not edit {{ name }}', From 5b8b62c854dc845977a053bace30f95c484b4c01 Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Thu, 17 Oct 2024 14:36:27 +0200 Subject: [PATCH 06/33] feat: Cleanup and fix complex event --- packages/openscd/src/addons/Editor.ts | 93 ++++++++++----------------- 1 file changed, 33 insertions(+), 60 deletions(-) diff --git a/packages/openscd/src/addons/Editor.ts b/packages/openscd/src/addons/Editor.ts index 3baae24646..bffbceb997 100644 --- a/packages/openscd/src/addons/Editor.ts +++ b/packages/openscd/src/addons/Editor.ts @@ -9,43 +9,27 @@ import { import { get } from 'lit-translate'; import { - Move, - Create, - Delete, EditorAction, EditorActionEvent, - SimpleAction, - Replace, - Update, } from '@openscd/core/foundation/deprecated/editor.js'; import { newLogEvent } from '@openscd/core/foundation/deprecated/history.js'; import { newValidateEvent } from '@openscd/core/foundation/deprecated/validation.js'; import { OpenDocEvent } from '@openscd/core/foundation/deprecated/open-event.js'; -import { getReference, SCLTag } from '../foundation.js'; -import { - isCreate, - isDelete, - isMove, - isSimple, - isReplace, - isUpdate, -} from '@openscd/core/foundation/deprecated/editor.js'; import { - AttributeValue as AttributeValueV2, - Edit as EditV2, - EditEvent as EditEventV2, - Insert as InsertV2, - isComplex as isComplexV2, - isInsert as isInsertV2, - isNamespaced as isNamespacedV2, - isRemove as isRemoveV2, - isUpdate as isUpdateV2, - LitElementConstructor, + AttributeValue, + Edit, + EditEvent, + Insert, + isComplex, + isInsert, + isNamespaced, + isRemove, + isUpdate, OpenEvent as OpenEventV2, - Remove as RemoveV2, - Update as UpdateV2, + Remove, + Update, } from '@openscd/core'; import { convertEditV1toV2 } from '@openscd/core'; @@ -70,22 +54,22 @@ export class OscdEditor extends LitElement { }) editCount = -1; - private getLogText(edit: EditV2): { title: string, message?: string } { - if (isInsertV2(edit)) { + private getLogText(edit: Edit): { title: string, message?: string } { + if (isInsert(edit)) { const name = edit.node instanceof Element ? edit.node.tagName : get('editing.node'); return { title: get('editing.created', { name }) }; - } else if (isUpdateV2(edit)) { + } else if (isUpdate(edit)) { const name = edit.element.tagName; return { title: get('editing.updated', { name }) }; - } else if (isRemoveV2(edit)) { + } else if (isRemove(edit)) { const name = edit.node instanceof Element ? edit.node.tagName : get('editing.node'); return { title: get('editing.deleted', { name }) }; - } else if (isComplexV2(edit)) { - const message = edit.map(this.getLogText).map(({ title }) => title).join(', '); + } else if (isComplex(edit)) { + const message = edit.map(e => this.getLogText(e)).map(({ title }) => title).join(', '); return { title: get('editing.complex'), message }; } @@ -93,13 +77,9 @@ export class OscdEditor extends LitElement { } private onAction(event: EditorActionEvent) { - console.log('old event', event); - const editV2 = convertEditV1toV2(event.detail.action); const initiator = event.detail.initiator; - console.log('dispatching new event', editV2); - this.host.dispatchEvent(newEditEvent(editV2, initiator)); } @@ -133,22 +113,19 @@ export class OscdEditor extends LitElement { super.connectedCallback(); this.host.addEventListener('editor-action', this.onAction.bind(this)); + this.host.addEventListener('oscd-edit', event => this.handleEditEvent(event)); this.host.addEventListener('open-doc', this.onOpenDoc); this.host.addEventListener('oscd-open', this.handleOpenDoc); - - // TODO: Test v2 API - this.host.addEventListener('oscd-edit', event => this.handleEditEventV2(event)); } render(): TemplateResult { return html``; } - // Test API v2 start - handleEditEventV2(event: EditEventV2) { + handleEditEvent(event: EditEvent) { console.log('Edit V2', event); const edit = event.detail.edit; - const undoEdit = handleEditV2(edit); + const undoEdit = handleEdit(edit); this.editCount += 1; this.dispatchEvent( @@ -169,17 +146,13 @@ export class OscdEditor extends LitElement { })); } } - - // Test API v2 end } -function handleEditV2(edit: EditV2): EditV2 { - console.log('Edit V2 event', edit); - - if (isInsertV2(edit)) return handleInsertV2(edit); - if (isUpdateV2(edit)) return handleUpdateV2(edit); - if (isRemoveV2(edit)) return handleRemoveV2(edit); - if (isComplexV2(edit)) return edit.map(handleEditV2).reverse(); +function handleEdit(edit: Edit): Edit { + if (isInsert(edit)) return handleInsert(edit); + if (isUpdate(edit)) return handleUpdate(edit); + if (isRemove(edit)) return handleRemove(edit); + if (isComplex(edit)) return edit.map(handleEdit).reverse(); return []; } @@ -187,11 +160,11 @@ function localAttributeName(attribute: string): string { return attribute.includes(':') ? attribute.split(':', 2)[1] : attribute; } -function handleInsertV2({ +function handleInsert({ parent, node, reference, -}: InsertV2): InsertV2 | RemoveV2 | [] { +}: Insert): Insert | Remove | [] { try { const { parentNode, nextSibling } = node; parent.insertBefore(node, reference); @@ -208,13 +181,13 @@ function handleInsertV2({ } } -function handleUpdateV2({ element, attributes }: UpdateV2): UpdateV2 { +function handleUpdate({ element, attributes }: Update): Update { const oldAttributes = { ...attributes }; Object.entries(attributes) .reverse() .forEach(([name, value]) => { - let oldAttribute: AttributeValueV2; - if (isNamespacedV2(value!)) + let oldAttribute: AttributeValue; + if (isNamespaced(value!)) oldAttribute = { value: element.getAttributeNS( value.namespaceURI, @@ -233,8 +206,8 @@ function handleUpdateV2({ element, attributes }: UpdateV2): UpdateV2 { }); for (const entry of Object.entries(attributes)) { try { - const [attribute, value] = entry as [string, AttributeValueV2]; - if (isNamespacedV2(value)) { + const [attribute, value] = entry as [string, AttributeValue]; + if (isNamespaced(value)) { if (value.value === null) element.removeAttributeNS( value.namespaceURI, @@ -254,7 +227,7 @@ function handleUpdateV2({ element, attributes }: UpdateV2): UpdateV2 { }; } -function handleRemoveV2({ node }: RemoveV2): InsertV2 | [] { +function handleRemove({ node }: Remove): Insert | [] { const { parentNode: parent, nextSibling: reference } = node; node.parentNode?.removeChild(node); if (parent) From 4f93c8e2b3858b917317e1bb3c8e82641bb4c4fc Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Fri, 18 Oct 2024 15:30:51 +0200 Subject: [PATCH 07/33] feat: Clean up code --- packages/openscd/src/addons/Editor.ts | 6 ------ packages/openscd/src/addons/History.ts | 20 +++----------------- 2 files changed, 3 insertions(+), 23 deletions(-) diff --git a/packages/openscd/src/addons/Editor.ts b/packages/openscd/src/addons/Editor.ts index bffbceb997..d86495875e 100644 --- a/packages/openscd/src/addons/Editor.ts +++ b/packages/openscd/src/addons/Editor.ts @@ -49,11 +49,6 @@ export class OscdEditor extends LitElement { }) host!: HTMLElement; - @property({ - type: Number, - }) - editCount = -1; - private getLogText(edit: Edit): { title: string, message?: string } { if (isInsert(edit)) { const name = edit.node instanceof Element ? @@ -126,7 +121,6 @@ export class OscdEditor extends LitElement { console.log('Edit V2', event); const edit = event.detail.edit; const undoEdit = handleEdit(edit); - this.editCount += 1; this.dispatchEvent( newEditCompletedEvent(event.detail.edit, event.detail.initiator) diff --git a/packages/openscd/src/addons/History.ts b/packages/openscd/src/addons/History.ts index 21160becee..b262a91cfa 100644 --- a/packages/openscd/src/addons/History.ts +++ b/packages/openscd/src/addons/History.ts @@ -199,27 +199,19 @@ export class OscdHistory extends LitElement { undo(): boolean { if (!this.canUndo) return false; - console.log(this.history); - const undoEdit = (this.history[this.editCount]).undo; this.dispatchEvent(newEditEvent(undoEdit, 'undo')); this.editCount = this.previousAction; - console.log('Undo - Setting edit count to ', this.editCount); - return true; } redo(): boolean { if (!this.canRedo) return false; - console.log(this.history); - const redoEdit = (this.history[this.nextAction]).redo; this.dispatchEvent(newEditEvent(redoEdit, 'redo')); this.editCount = this.nextAction; - console.log('Redo - Setting edit count to ', this.editCount); - return true; } @@ -230,16 +222,10 @@ export class OscdHistory extends LitElement { }; if (entry.kind === 'action') { - console.log('Edit ', detail); - - if (this.nextAction !== -1) this.history.splice(this.nextAction); - this.editCount = this.history.length; - /* - if (entry.action.derived) return; - entry.action.derived = true; - if (this.nextAction !== -1) this.history.splice(this.nextAction); + if (this.nextAction !== -1) { + this.history.splice(this.nextAction); + } this.editCount = this.history.length; - */ } this.history.push(entry); From 0ef22da3e9e52a51080e3806e1c6db26f69f56e5 Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Mon, 21 Oct 2024 13:49:54 +0200 Subject: [PATCH 08/33] feat: Add history state --- packages/openscd/src/addons/History.ts | 47 +++++++++++++++++++++----- packages/openscd/src/addons/Layout.ts | 34 ++++--------------- packages/openscd/src/open-scd.ts | 28 +++++++-------- 3 files changed, 57 insertions(+), 52 deletions(-) diff --git a/packages/openscd/src/addons/History.ts b/packages/openscd/src/addons/History.ts index b262a91cfa..42f7478a9f 100644 --- a/packages/openscd/src/addons/History.ts +++ b/packages/openscd/src/addons/History.ts @@ -35,16 +35,29 @@ import { LogEvent, } from '@openscd/core/foundation/deprecated/history.js'; -import { - newActionEvent, - invert, -} from '@openscd/core/foundation/deprecated/editor.js'; - import { getFilterIcon, iconColors } from '../icons/icons.js'; import { Plugin } from '../open-scd.js'; import { newEditEvent } from '@openscd/core'; +export const historyStateEvent = 'history-state'; +export interface HistoryState { + editCount: number; + canUndo: boolean; + canRedo: boolean; +} +export type HistoryStateEvent = CustomEvent; + +function newHistoryStateEvent(state: HistoryState): HistoryStateEvent { + return new CustomEvent(historyStateEvent, { detail: state }); +} + +declare global { + interface ElementEventMap { + [historyStateEvent]: HistoryStateEvent; + } +} + const icons = { info: 'info', warning: 'warning', @@ -201,7 +214,7 @@ export class OscdHistory extends LitElement { const undoEdit = (this.history[this.editCount]).undo; this.dispatchEvent(newEditEvent(undoEdit, 'undo')); - this.editCount = this.previousAction; + this.setEditCount(this.previousAction); return true; } @@ -210,7 +223,7 @@ export class OscdHistory extends LitElement { const redoEdit = (this.history[this.nextAction]).redo; this.dispatchEvent(newEditEvent(redoEdit, 'redo')); - this.editCount = this.nextAction; + this.setEditCount(this.nextAction); return true; } @@ -225,17 +238,32 @@ export class OscdHistory extends LitElement { if (this.nextAction !== -1) { this.history.splice(this.nextAction); } - this.editCount = this.history.length; } this.history.push(entry); + this.setEditCount(this.history.length - 1); this.requestUpdate('history', []); } private onReset() { this.log = []; this.history = []; - this.editCount = -1; + this.setEditCount(-1); + } + + private setEditCount(count: number): void { + this.editCount = count; + this.dispatchHistoryStateEvent(); + } + + private dispatchHistoryStateEvent(): void { + this.host.dispatchEvent( + newHistoryStateEvent({ + editCount: this.editCount, + canUndo: this.canUndo, + canRedo: this.canRedo + }) + ); } private onInfo(detail: InfoDetail) { @@ -313,6 +341,7 @@ export class OscdHistory extends LitElement { this.historyUIHandler = this.historyUIHandler.bind(this); this.emptyIssuesHandler = this.emptyIssuesHandler.bind(this); this.handleKeyPress = this.handleKeyPress.bind(this); + this.dispatchHistoryStateEvent = this.dispatchHistoryStateEvent.bind(this); document.onkeydown = this.handleKeyPress; } diff --git a/packages/openscd/src/addons/Layout.ts b/packages/openscd/src/addons/Layout.ts index 87f5cb2447..fe9eb42c43 100644 --- a/packages/openscd/src/addons/Layout.ts +++ b/packages/openscd/src/addons/Layout.ts @@ -25,6 +25,7 @@ import { newSetPluginsEvent, } from '../open-scd.js'; import { + HistoryState, HistoryUIKind, newEmptyIssuesEvent, newHistoryUIEvent, @@ -82,23 +83,15 @@ export class OscdLayout extends LitElement { @property({ type: Object }) host!: HTMLElement; + @property({ type: Object }) + historyState!: HistoryState; + @state() validated: Promise = Promise.resolve(); @state() shouldValidate = false; - @state() - redoCount = 0; - - get canUndo(): boolean { - return this.editCount >= 0; - } - - get canRedo(): boolean { - return this.redoCount > 0; - } - @query('#menu') menuUI!: Drawer; @query('#pluginManager') @@ -152,7 +145,7 @@ export class OscdLayout extends LitElement { action: (): void => { this.dispatchEvent(newUndoEvent()); }, - disabled: (): boolean => !this.canUndo, + disabled: (): boolean => !this.historyState.canUndo, kind: 'static', }, { @@ -162,7 +155,7 @@ export class OscdLayout extends LitElement { action: (): void => { this.dispatchEvent(newRedoEvent()); }, - disabled: (): boolean => !this.canRedo, + disabled: (): boolean => !this.historyState.canRedo, kind: 'static', }, ...validators, @@ -308,21 +301,6 @@ export class OscdLayout extends LitElement { }); this.handleKeyPress = this.handleKeyPress.bind(this); document.onkeydown = this.handleKeyPress; - - this.host.addEventListener( - 'oscd-edit-completed', - (evt: EditCompletedEvent) => { - const initiator = evt.detail.initiator; - - if (initiator === 'undo') { - this.redoCount += 1; - } else if (initiator === 'redo') { - this.redoCount -= 1; - } - - this.requestUpdate(); - } - ); } diff --git a/packages/openscd/src/open-scd.ts b/packages/openscd/src/open-scd.ts index 34ea9341f2..1c9e8efb54 100644 --- a/packages/openscd/src/open-scd.ts +++ b/packages/openscd/src/open-scd.ts @@ -45,6 +45,7 @@ import type { Plugin as CorePlugin, EditCompletedEvent, } from '@openscd/core'; +import { HistoryState, historyStateEvent } from './addons/History.js'; // HOSTING INTERFACES @@ -245,9 +246,12 @@ export class OpenSCD extends LitElement { /** The UUID of the current [[`doc`]] */ @property({ type: String }) docId = ''; - /** Index of the last [[`EditorAction`]] applied. */ @state() - editCount = -1; + historyState: HistoryState = { + editCount: -1, + canRedo: false, + canUndo: false, + } /** Object containing all *.nsdoc files and a function extracting element's label form them*/ @property({ attribute: false }) @@ -294,15 +298,8 @@ export class OpenSCD extends LitElement { this.updatePlugins(); this.requestUpdate(); - this.addEventListener('oscd-edit-completed', (evt: EditCompletedEvent) => { - const initiator = evt.detail.initiator; - - if (initiator === 'undo') { - this.editCount -= 1; - } else { - this.editCount += 1; - } - + this.addEventListener(historyStateEvent, (e: CustomEvent) => { + this.historyState = e.detail; this.requestUpdate(); }); } @@ -311,19 +308,20 @@ export class OpenSCD extends LitElement { return html` - + @@ -497,7 +495,7 @@ export class OpenSCD extends LitElement { content: staticTagHtml`<${tag} .doc=${this.doc} .docName=${this.docName} - .editCount=${this.editCount} + .editCount=${this.historyState.editCount} .docId=${this.docId} .pluginId=${plugin.src} .nsdoc=${this.nsdoc} From 782ba0e2637134dd52e77e262d5123f38b7a5669 Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Tue, 22 Oct 2024 09:43:18 +0200 Subject: [PATCH 09/33] feat: Remove editing mixin --- packages/openscd/src/Editing.ts | 470 ------------------------- packages/openscd/src/addons/History.ts | 6 +- 2 files changed, 2 insertions(+), 474 deletions(-) delete mode 100644 packages/openscd/src/Editing.ts diff --git a/packages/openscd/src/Editing.ts b/packages/openscd/src/Editing.ts deleted file mode 100644 index e60c4884e8..0000000000 --- a/packages/openscd/src/Editing.ts +++ /dev/null @@ -1,470 +0,0 @@ -import { OpenEvent } from '@openscd/core'; -import { property } from 'lit-element'; -import { get } from 'lit-translate'; - -import { newLogEvent } from '@openscd/core/foundation/deprecated/history.js'; -import { newValidateEvent } from '@openscd/core/foundation/deprecated/validation.js' -import { - Create, - Delete, - Move, - Update, - Replace, - SimpleAction, - EditorAction, - EditorActionEvent, - isCreate, - isDelete, - isMove, - isSimple, - isReplace, - isUpdate, -} from '@openscd/core/foundation/deprecated/editor.js'; -import { OpenDocEvent } from '@openscd/core/foundation/deprecated/open-event.js'; -import { - getReference, - SCLTag, - Mixin, - LitElementConstructor -} from './foundation.js'; - -/** Mixin that edits an `XML` `doc`, listening to [[`EditorActionEvent`]]s */ -export type EditingElement = Mixin; - -/** @typeParam TBase - a type extending `LitElement` - * @returns `Base` with an `XMLDocument` property "`doc`" and an event listener - * applying [[`EditorActionEvent`]]s and dispatching [[`LogEvent`]]s. */ -export function Editing(Base: TBase) { - class EditingElement extends Base { - /** The `XMLDocument` to be edited */ - @property({ attribute: false }) - doc: XMLDocument | null = null; - /** The name of the current [[`doc`]] */ - @property({ type: String }) docName = ''; - /** The UUID of the current [[`doc`]] */ - @property({ type: String }) docId = ''; - - private checkCreateValidity(create: Create): boolean { - if (create.checkValidity !== undefined) return create.checkValidity(); - - if ( - !(create.new.element instanceof Element) || - !(create.new.parent instanceof Element) - ) - return true; - - const invalidNaming = - create.new.element.hasAttribute('name') && - Array.from(create.new.parent.children).some( - elm => - elm.tagName === (create.new.element).tagName && - elm.getAttribute('name') === - (create.new.element).getAttribute('name') - ); - - if (invalidNaming) { - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.create', { - name: create.new.element.tagName, - }), - message: get('editing.error.nameClash', { - parent: - create.new.parent instanceof HTMLElement - ? create.new.parent.tagName - : 'Document', - child: create.new.element.tagName, - name: create.new.element.getAttribute('name')!, - }), - }) - ); - - return false; - } - - const invalidId = - create.new.element.hasAttribute('id') && - Array.from( - create.new.parent.ownerDocument.querySelectorAll( - 'LNodeType, DOType, DAType, EnumType' - ) - ).some( - elm => - elm.getAttribute('id') === - (create.new.element).getAttribute('id') - ); - - if (invalidId) { - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.create', { - name: create.new.element.tagName, - }), - message: get('editing.error.idClash', { - id: create.new.element.getAttribute('id')!, - }), - }) - ); - - return false; - } - - return true; - } - - private onCreate(action: Create) { - if (!this.checkCreateValidity(action)) return false; - - if ( - action.new.reference === undefined && - action.new.element instanceof Element && - action.new.parent instanceof Element - ) - action.new.reference = getReference( - action.new.parent, - action.new.element.tagName - ); - else action.new.reference = action.new.reference ?? null; - - action.new.parent.insertBefore(action.new.element, action.new.reference); - return true; - } - - private logCreate(action: Create) { - const name = - action.new.element instanceof Element - ? action.new.element.tagName - : get('editing.node'); - - this.dispatchEvent( - newLogEvent({ - kind: 'action', - title: get('editing.created', { name }), - action, - }) - ); - } - - private onDelete(action: Delete) { - if (!action.old.reference) - action.old.reference = action.old.element.nextSibling; - - if (action.old.element.parentNode !== action.old.parent) return false; - - action.old.parent.removeChild(action.old.element); - return true; - } - - private logDelete(action: Delete) { - const name = - action.old.element instanceof Element - ? action.old.element.tagName - : get('editing.node'); - - this.dispatchEvent( - newLogEvent({ - kind: 'action', - title: get('editing.deleted', { name }), - action, - }) - ); - } - - private checkMoveValidity(move: Move): boolean { - if (move.checkValidity !== undefined) return move.checkValidity(); - - const invalid = - move.old.element.hasAttribute('name') && - move.new.parent !== move.old.parent && - Array.from(move.new.parent.children).some( - elm => - elm.tagName === move.old.element.tagName && - elm.getAttribute('name') === move.old.element.getAttribute('name') - ); - - if (invalid) - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.move', { - name: move.old.element.tagName, - }), - message: get('editing.error.nameClash', { - parent: move.new.parent.tagName, - child: move.old.element.tagName, - name: move.old.element.getAttribute('name')!, - }), - }) - ); - - return !invalid; - } - - private onMove(action: Move) { - if (!this.checkMoveValidity(action)) return false; - - if (!action.old.reference) - action.old.reference = action.old.element.nextSibling; - - if (action.new.reference === undefined) - action.new.reference = getReference( - action.new.parent, - action.old.element.tagName - ); - - action.new.parent.insertBefore(action.old.element, action.new.reference); - return true; - } - - private logMove(action: Move) { - this.dispatchEvent( - newLogEvent({ - kind: 'action', - title: get('editing.moved', { - name: action.old.element.tagName, - }), - action: action, - }) - ); - } - - private checkReplaceValidity(replace: Replace): boolean { - if (replace.checkValidity !== undefined) return replace.checkValidity(); - - const invalidNaming = - replace.new.element.hasAttribute('name') && - replace.new.element.getAttribute('name') !== - replace.old.element.getAttribute('name') && - Array.from(replace.old.element.parentElement?.children ?? []).some( - elm => - elm.tagName === replace.new.element.tagName && - elm.getAttribute('name') === - replace.new.element.getAttribute('name') - ); - - if (invalidNaming) { - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.update', { - name: replace.new.element.tagName, - }), - message: get('editing.error.nameClash', { - parent: replace.old.element.parentElement!.tagName, - child: replace.new.element.tagName, - name: replace.new.element.getAttribute('name')!, - }), - }) - ); - - return false; - } - - const invalidId = - replace.new.element.hasAttribute('id') && - replace.new.element.getAttribute('id') !== - replace.old.element.getAttribute('id') && - Array.from( - replace.new.element.ownerDocument.querySelectorAll( - 'LNodeType, DOType, DAType, EnumType' - ) - ).some( - elm => - elm.getAttribute('id') === - (replace.new.element).getAttribute('id') - ); - - if (invalidId) { - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.update', { - name: replace.new.element.tagName, - }), - message: get('editing.error.idClash', { - id: replace.new.element.getAttribute('id')!, - }), - }) - ); - - return false; - } - - return true; - } - - private onReplace(action: Replace) { - if (!this.checkReplaceValidity(action)) return false; - - action.new.element.append(...Array.from(action.old.element.children)); - action.old.element.replaceWith(action.new.element); - return true; - } - - private logUpdate(action: Replace | Update) { - const name = isReplace(action) - ? action.new.element.tagName - : (action as Update).element.tagName; - - this.dispatchEvent( - newLogEvent({ - kind: 'action', - title: get('editing.updated', { - name, - }), - action: action, - }) - ); - } - - private checkUpdateValidity(update: Update): boolean { - if (update.checkValidity !== undefined) return update.checkValidity(); - - if (update.oldAttributes['name'] !== update.newAttributes['name']) { - const invalidNaming = Array.from( - update.element.parentElement?.children ?? [] - ).some( - elm => - elm.tagName === update.element.tagName && - elm.getAttribute('name') === update.newAttributes['name'] - ); - - if (invalidNaming) { - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.update', { - name: update.element.tagName, - }), - message: get('editing.error.nameClash', { - parent: update.element.parentElement!.tagName, - child: update.element.tagName, - name: update.newAttributes['name']!, - }), - }) - ); - - return false; - } - } - - const invalidId = - update.newAttributes['id'] && - Array.from( - update.element.ownerDocument.querySelectorAll( - 'LNodeType, DOType, DAType, EnumType' - ) - ).some(elm => elm.getAttribute('id') === update.newAttributes['id']); - - if (invalidId) { - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.update', { - name: update.element.tagName, - }), - message: get('editing.error.idClash', { - id: update.newAttributes['id']!, - }), - }) - ); - - return false; - } - - return true; - } - - private onUpdate(action: Update) { - if (!this.checkUpdateValidity(action)) return false; - - Array.from(action.element.attributes).forEach(attr => - action.element.removeAttributeNode(attr) - ); - - Object.entries(action.newAttributes).forEach(([key, value]) => { - if (value !== null && value !== undefined) - action.element.setAttribute(key, value); - }); - - return true; - } - - private onSimpleAction(action: SimpleAction) { - if (isMove(action)) return this.onMove(action as Move); - else if (isCreate(action)) return this.onCreate(action as Create); - else if (isDelete(action)) return this.onDelete(action as Delete); - else if (isReplace(action)) return this.onReplace(action as Replace); - else if (isUpdate(action)) return this.onUpdate(action as Update); - } - - private logSimpleAction(action: SimpleAction) { - if (isMove(action)) this.logMove(action as Move); - else if (isCreate(action)) this.logCreate(action as Create); - else if (isDelete(action)) this.logDelete(action as Delete); - else if (isReplace(action)) this.logUpdate(action as Replace); - else if (isUpdate(action)) this.logUpdate(action as Update); - } - - private async onAction(event: EditorActionEvent) { - if (isSimple(event.detail.action)) { - if (this.onSimpleAction(event.detail.action)) - this.logSimpleAction(event.detail.action); - } else if (event.detail.action.actions.length > 0) { - event.detail.action.actions.forEach(element => - this.onSimpleAction(element) - ); - this.dispatchEvent( - newLogEvent({ - kind: 'action', - title: event.detail.action.title, - action: event.detail.action, - }) - ); - } else return; - - if (!this.doc) return; - - await this.updateComplete; - this.dispatchEvent(newValidateEvent()); - } - - /** - * - * @deprecated [Move to handleOpenDoc instead] - */ - private async onOpenDoc(event: OpenDocEvent) { - this.doc = event.detail.doc; - this.docName = event.detail.docName; - this.docId = event.detail.docId ?? ''; - - await this.updateComplete; - - this.dispatchEvent(newValidateEvent()); - - this.dispatchEvent( - newLogEvent({ - kind: 'info', - title: get('openSCD.loaded', { name: this.docName }), - }) - ); - } - - handleOpenDoc({ detail: { docName, doc } }: OpenEvent) { - this.doc = doc; - this.docName = docName; - } - - constructor(...args: any[]) { - super(...args); - - this.addEventListener('editor-action', this.onAction); - this.addEventListener('open-doc', this.onOpenDoc); - this.addEventListener('oscd-open', this.handleOpenDoc); - } - } - - return EditingElement; -} diff --git a/packages/openscd/src/addons/History.ts b/packages/openscd/src/addons/History.ts index 42f7478a9f..b5100b2ff6 100644 --- a/packages/openscd/src/addons/History.ts +++ b/packages/openscd/src/addons/History.ts @@ -234,10 +234,8 @@ export class OscdHistory extends LitElement { ...detail, }; - if (entry.kind === 'action') { - if (this.nextAction !== -1) { - this.history.splice(this.nextAction); - } + if (this.nextAction !== -1) { + this.history.splice(this.nextAction); } this.history.push(entry); From 65fcef419c6f61ef1ac2d5a2390054a01edcc682 Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Tue, 22 Oct 2024 14:36:05 +0200 Subject: [PATCH 10/33] feat: Move edit event converter to openscd package --- packages/core/foundation.ts | 3 --- packages/openscd/src/addons/Editor.ts | 2 +- .../addons/editor}/edit-v1-to-v2-converter.ts | 20 ++++++++++++++++--- 3 files changed, 18 insertions(+), 7 deletions(-) rename packages/{core/foundation => openscd/src/addons/editor}/edit-v1-to-v2-converter.ts (85%) diff --git a/packages/core/foundation.ts b/packages/core/foundation.ts index d06a67cfb4..466136ed83 100644 --- a/packages/core/foundation.ts +++ b/packages/core/foundation.ts @@ -23,9 +23,6 @@ export type { Update, Remove, } from './foundation/edit-event.js'; -export { - convertEditV1toV2 -} from './foundation/edit-v1-to-v2-converter.js'; export { cyrb64 } from './foundation/cyrb64.js'; diff --git a/packages/openscd/src/addons/Editor.ts b/packages/openscd/src/addons/Editor.ts index d86495875e..17164e8187 100644 --- a/packages/openscd/src/addons/Editor.ts +++ b/packages/openscd/src/addons/Editor.ts @@ -32,7 +32,7 @@ import { Update, } from '@openscd/core'; -import { convertEditV1toV2 } from '@openscd/core'; +import { convertEditV1toV2 } from './editor/edit-v1-to-v2-converter.js'; @customElement('oscd-editor') export class OscdEditor extends LitElement { diff --git a/packages/core/foundation/edit-v1-to-v2-converter.ts b/packages/openscd/src/addons/editor/edit-v1-to-v2-converter.ts similarity index 85% rename from packages/core/foundation/edit-v1-to-v2-converter.ts rename to packages/openscd/src/addons/editor/edit-v1-to-v2-converter.ts index af2fb7aec8..5fb3e71b66 100644 --- a/packages/core/foundation/edit-v1-to-v2-converter.ts +++ b/packages/openscd/src/addons/editor/edit-v1-to-v2-converter.ts @@ -1,6 +1,20 @@ -import { Create, Delete, EditorAction, isCreate, isDelete, isMove, isReplace, isSimple, isUpdate, Move, Replace, SimpleAction, Update } from './deprecated/editor.js'; -import { Edit, Insert, Remove, Update as UpdateV2 } from './edit-event.js'; -import { getReference, SCLTag } from '@openscd/open-scd/src/foundation.js'; +import { + Create, + Delete, + EditorAction, + isCreate, + isDelete, + isMove, + isReplace, + isSimple, + isUpdate, + Move, + Replace, + SimpleAction, + Update +} from '@openscd/core/foundation/deprecated/editor.js'; +import { Edit, Insert, Remove, Update as UpdateV2 } from '@openscd/core'; +import { getReference, SCLTag } from '../../foundation.js'; export function convertEditV1toV2(action: EditorAction): Edit { From 64a05052bb48a4ab313159519ad87479c7049bc2 Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Tue, 22 Oct 2024 14:51:20 +0200 Subject: [PATCH 11/33] feat: Remove editing mixin --- packages/core/mixins/Editing.ts | 214 -------------------------------- 1 file changed, 214 deletions(-) delete mode 100644 packages/core/mixins/Editing.ts diff --git a/packages/core/mixins/Editing.ts b/packages/core/mixins/Editing.ts deleted file mode 100644 index c42845491a..0000000000 --- a/packages/core/mixins/Editing.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { LitElement } from 'lit'; - -import { property, state } from 'lit/decorators.js'; - -import { - AttributeValue, - Edit, - EditEvent, - Insert, - isComplex, - isInsert, - isNamespaced, - isRemove, - isUpdate, - LitElementConstructor, - OpenEvent, - Remove, - Update, -} from '../foundation.js'; - -function localAttributeName(attribute: string): string { - return attribute.includes(':') ? attribute.split(':', 2)[1] : attribute; -} - -function handleInsert({ - parent, - node, - reference, -}: Insert): Insert | Remove | [] { - try { - const { parentNode, nextSibling } = node; - parent.insertBefore(node, reference); - if (parentNode) - return { - node, - parent: parentNode, - reference: nextSibling, - }; - return { node }; - } catch (e) { - // do nothing if insert doesn't work on these nodes - return []; - } -} - -function handleUpdate({ element, attributes }: Update): Update { - const oldAttributes = { ...attributes }; - Object.entries(attributes) - .reverse() - .forEach(([name, value]) => { - let oldAttribute: AttributeValue; - if (isNamespaced(value!)) - oldAttribute = { - value: element.getAttributeNS( - value.namespaceURI, - localAttributeName(name) - ), - namespaceURI: value.namespaceURI, - }; - else - oldAttribute = element.getAttributeNode(name)?.namespaceURI - ? { - value: element.getAttribute(name), - namespaceURI: element.getAttributeNode(name)!.namespaceURI!, - } - : element.getAttribute(name); - oldAttributes[name] = oldAttribute; - }); - for (const entry of Object.entries(attributes)) { - try { - const [attribute, value] = entry as [string, AttributeValue]; - if (isNamespaced(value)) { - if (value.value === null) - element.removeAttributeNS( - value.namespaceURI, - localAttributeName(attribute) - ); - else element.setAttributeNS(value.namespaceURI, attribute, value.value); - } else if (value === null) element.removeAttribute(attribute); - else element.setAttribute(attribute, value); - } catch (e) { - // do nothing if update doesn't work on this attribute - delete oldAttributes[entry[0]]; - } - } - return { - element, - attributes: oldAttributes, - }; -} - -function handleRemove({ node }: Remove): Insert | [] { - const { parentNode: parent, nextSibling: reference } = node; - node.parentNode?.removeChild(node); - if (parent) - return { - node, - parent, - reference, - }; - return []; -} - -function handleEdit(edit: Edit): Edit { - if (isInsert(edit)) return handleInsert(edit); - if (isUpdate(edit)) return handleUpdate(edit); - if (isRemove(edit)) return handleRemove(edit); - if (isComplex(edit)) return edit.map(handleEdit).reverse(); - return []; -} - -export type LogEntry = { undo: Edit; redo: Edit }; - -export interface EditingMixin { - doc: XMLDocument; - history: LogEntry[]; - editCount: number; - last: number; - canUndo: boolean; - canRedo: boolean; - docs: Record; - docName: string; - handleOpenDoc(evt: OpenEvent): void; - handleEditEvent(evt: EditEvent): void; - undo(n?: number): void; - redo(n?: number): void; -} - -type ReturnConstructor = new (...args: any[]) => LitElement & EditingMixin; - -/** A mixin for editing a set of [[docs]] using [[EditEvent]]s */ -export function Editing( - Base: TBase -): TBase & ReturnConstructor { - class EditingElement extends Base { - @state() - /** The `XMLDocument` currently being edited */ - get doc(): XMLDocument { - return this.docs[this.docName]; - } - - @state() - history: LogEntry[] = []; - - @state() - editCount: number = 0; - - @state() - get last(): number { - return this.editCount - 1; - } - - @state() - get canUndo(): boolean { - return this.last >= 0; - } - - @state() - get canRedo(): boolean { - return this.editCount < this.history.length; - } - - /** - * The set of `XMLDocument`s currently loaded - * - * @prop {Record} docs - Record of loaded XML documents - */ - @state() - docs: Record = {}; - - /** - * The name of the [[`doc`]] currently being edited - * - * @prop {String} docName - name of the document that is currently being edited - */ - @property({ type: String, reflect: true }) docName = ''; - - handleOpenDoc({ detail: { docName, doc } }: OpenEvent) { - this.docName = docName; - this.docs[this.docName] = doc; - } - - handleEditEvent(event: EditEvent) { - const edit = event.detail.edit; - this.history.splice(this.editCount); - this.history.push({ undo: handleEdit(edit), redo: edit }); - this.editCount += 1; - } - - /** Undo the last `n` [[Edit]]s committed */ - undo(n = 1) { - if (!this.canUndo || n < 1) return; - handleEdit(this.history[this.last!].undo); - this.editCount -= 1; - if (n > 1) this.undo(n - 1); - } - - /** Redo the last `n` [[Edit]]s that have been undone */ - redo(n = 1) { - if (!this.canRedo || n < 1) return; - handleEdit(this.history[this.editCount].redo); - this.editCount += 1; - if (n > 1) this.redo(n - 1); - } - - constructor(...args: any[]) { - super(...args); - - this.addEventListener('oscd-open', this.handleOpenDoc); - this.addEventListener('oscd-edit', event => this.handleEditEvent(event)); - } - } - return EditingElement; -} From 18a2061ac5cfbc4d3de0d5e98398a94bc613611f Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Wed, 23 Oct 2024 13:31:38 +0200 Subject: [PATCH 12/33] test: Add tests --- packages/openscd/test/unit/Editor.test.ts | 29 ++++ .../test/unit/edit-v1-to-v2-converter.test.ts | 145 ++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 packages/openscd/test/unit/Editor.test.ts create mode 100644 packages/openscd/test/unit/edit-v1-to-v2-converter.test.ts diff --git a/packages/openscd/test/unit/Editor.test.ts b/packages/openscd/test/unit/Editor.test.ts new file mode 100644 index 0000000000..aa6f2d23a3 --- /dev/null +++ b/packages/openscd/test/unit/Editor.test.ts @@ -0,0 +1,29 @@ +import { html, fixture, expect } from '@open-wc/testing'; + +import '../../src/addons/Editor.js'; +import { OscdEditor } from '../../src/addons/Editor.js'; + + +describe('OSCD-Editor', () => { + let element: OscdEditor; + let host: HTMLElement; + let scd: XMLDocument; + + beforeEach(async () => { + scd = new DOMParser().parseFromString( + ` + + + `, + 'application/xml', + ); + + host = document.createElement('div'); + + element = await fixture(html``, { parentNode: host }); + }); + + it('is defined', () => { + expect(element).property('doc').to.exist; + }); +}); diff --git a/packages/openscd/test/unit/edit-v1-to-v2-converter.test.ts b/packages/openscd/test/unit/edit-v1-to-v2-converter.test.ts new file mode 100644 index 0000000000..9812b63dc7 --- /dev/null +++ b/packages/openscd/test/unit/edit-v1-to-v2-converter.test.ts @@ -0,0 +1,145 @@ +import { html, fixture, expect } from '@open-wc/testing'; + +import { + Create, + Delete, + EditorAction, + isCreate, + isDelete, + isMove, + isReplace, + isSimple, + isUpdate, + Move, + Replace, + SimpleAction, + Update, + createUpdateAction +} from '@openscd/core/foundation/deprecated/editor.js'; +import { Edit, Insert, Remove, Update as UpdateV2 } from '@openscd/core'; + +import { convertEditV1toV2 } from '../../src/addons/editor/edit-v1-to-v2-converter.js'; + + +describe('edit-v1-to-v2-converter', () => { + const doc = new DOMParser().parseFromString( + ` + + + + + + `, + 'application/xml' + ); + const substation = doc.querySelector('Substation')!; + const substation2 = doc.querySelector('Substation[name="sub2"]')!; + const bay = doc.querySelector('Bay')!; + + it('should convert delete to remove', () => { + const deleteAction: Delete = { + old: { + parent: substation, + element: bay + } + }; + + const remove = convertEditV1toV2(deleteAction); + + const expectedRemove: Remove = { + node: bay + }; + + expect(remove).to.deep.equal(expectedRemove); + }); + + it('should convert create to insert', () => { + const newBay = doc.createElement('Bay'); + newBay.setAttribute('name', 'bay2'); + + const createAction: Create = { + new: { + parent: substation, + element: newBay + } + }; + + const insert = convertEditV1toV2(createAction); + + const expectedInsert: Insert = { + parent: substation, + node: newBay, + reference: null + }; + + expect(insert).to.deep.equal(expectedInsert); + }); + + it('should convert update to updateV2', () => { + const newAttributes = { + name: 'newBayName', + }; + const updateAction = createUpdateAction(bay, newAttributes); + + const updateV2 = convertEditV1toV2(updateAction); + + const expectedUpdateV2: UpdateV2 = { + element: bay, + attributes: newAttributes + }; + + expect(updateV2).to.deep.equal(expectedUpdateV2); + }); + + it('should convert move to insert', () => { + const moveAction: Move = { + old: { + parent: substation, + element: bay, + reference: null + }, + new: { + parent: substation2, + reference: null + } + }; + + const insert = convertEditV1toV2(moveAction); + + const expectedInsert: Insert = { + parent: substation2, + node: bay, + reference: null + }; + + expect(insert).to.deep.equal(expectedInsert); + }); + + it('should convert replace to complex action with remove and insert', () => { + const ied = doc.createElement('IED'); + ied.setAttribute('name', 'ied'); + + const replace: Replace = { + old: { + element: bay + }, + new: { + element: ied + } + }; + + const [ remove, insert ] = convertEditV1toV2(replace) as Edit[]; + + const expectedRemove: Remove = { + node: bay + }; + const expectedInsert: Insert = { + parent: substation, + node: ied, + reference: bay.nextSibling + }; + + expect(remove).to.deep.equal(expectedRemove); + expect(insert).to.deep.equal(expectedInsert); + }); +}); From e606c8ba71ec6ffe2742dffde88976a10ab28935 Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Wed, 23 Oct 2024 13:37:37 +0200 Subject: [PATCH 13/33] chore: Remove outdated export --- packages/core/foundation.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/foundation.ts b/packages/core/foundation.ts index 466136ed83..c7cd164a43 100644 --- a/packages/core/foundation.ts +++ b/packages/core/foundation.ts @@ -26,7 +26,6 @@ export type { export { cyrb64 } from './foundation/cyrb64.js'; -export { Editing } from './mixins/Editing.js'; export type { Plugin, PluginSet } from './foundation/plugin.js'; export { newEditCompletedEvent } from './foundation/edit-completed-event.js'; From d54b30bc8a83ae5daed48aef97d0148166b628d1 Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Thu, 24 Oct 2024 14:26:00 +0200 Subject: [PATCH 14/33] test: Fix tests --- .../openscd/test/unit/wizard-dialog.test.ts | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/packages/openscd/test/unit/wizard-dialog.test.ts b/packages/openscd/test/unit/wizard-dialog.test.ts index 3419c35a0b..458a19705b 100644 --- a/packages/openscd/test/unit/wizard-dialog.test.ts +++ b/packages/openscd/test/unit/wizard-dialog.test.ts @@ -1,17 +1,15 @@ import { html, fixture, expect } from '@open-wc/testing'; -import './mock-editor.js'; - import { Button } from '@material/mwc-button'; import '../../src/wizard-textfield.js'; import '../../src/wizard-dialog.js'; import { WizardDialog } from '../../src/wizard-dialog.js'; -import { WizardInputElement } from '../../src/foundation.js'; +import { checkValidity, WizardInputElement } from '../../src/foundation.js'; import { WizardCheckbox } from '../../src/wizard-checkbox.js'; import { WizardSelect } from '../../src/wizard-select.js'; import { WizardTextField } from '../../src/wizard-textfield.js'; -import { EditorAction } from '@openscd/core/foundation/deprecated/editor.js'; +import { ComplexAction, Create, Delete, EditorAction } from '@openscd/core/foundation/deprecated/editor.js'; describe('wizard-dialog', () => { let element: WizardDialog; @@ -230,9 +228,7 @@ describe('wizard-dialog', () => { let host: Element; beforeEach(async () => { - element = await fixture( - html`` - ).then(elm => elm.querySelector('wizard-dialog')!); + element = await fixture(html``); localStorage.setItem('mode', 'pro'); element.requestUpdate(); await element.updateComplete; @@ -274,6 +270,9 @@ describe('wizard-dialog', () => { }); it('commits the code action on primary button click', async () => { + let editorAction: ComplexAction; + element.addEventListener('editor-action', (action) => editorAction = action.detail.action as ComplexAction); + element.dialog ?.querySelector('ace-editor') ?.setAttribute('value', ''); @@ -282,7 +281,22 @@ describe('wizard-dialog', () => { ?.querySelector