Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support edit api v2 #1581

Merged
merged 36 commits into from
Nov 14, 2024
Merged
Changes from 1 commit
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
ccdfa48
WIP: Add edit api v2 support
clepski Oct 4, 2024
44255c3
feat: WIP Convert v1 to v2 events
clepski Oct 10, 2024
e2060cc
feat: Dispatch edit completed
clepski Oct 11, 2024
1cae3d5
feat: WIP Handle undo and redo events
clepski Oct 14, 2024
a2f6552
feat: Add log text and remove old code
clepski Oct 17, 2024
5b8b62c
feat: Cleanup and fix complex event
clepski Oct 17, 2024
4f93c8e
feat: Clean up code
clepski Oct 18, 2024
0ef22da
feat: Add history state
clepski Oct 21, 2024
782ba0e
feat: Remove editing mixin
clepski Oct 22, 2024
65fcef4
feat: Move edit event converter to openscd package
clepski Oct 22, 2024
64a0505
feat: Remove editing mixin
clepski Oct 22, 2024
18a2061
test: Add tests
clepski Oct 23, 2024
e606c8b
chore: Remove outdated export
clepski Oct 23, 2024
d54b30b
test: Fix tests
clepski Oct 24, 2024
20b16c9
test: Fix history tests
clepski Oct 25, 2024
50f26fc
test: Remove editing tests
clepski Oct 25, 2024
c8b0652
test: Disable broken test
clepski Oct 25, 2024
78b922f
Revert "test: Disable broken test"
clepski Oct 25, 2024
146de19
Merge branch 'main' into feat/edit-api-v2
clepski Oct 25, 2024
faa1511
feat: Fix convert move ref
clepski Oct 25, 2024
a36755c
feat: Adjust converter to edit api v1 behavior
clepski Oct 25, 2024
5393f00
test: Fix test
clepski Oct 25, 2024
9359d83
Merge branch 'main' into feat/edit-api-v2
clepski Oct 28, 2024
e370f96
test: Adjust mock wizard editor
clepski Oct 28, 2024
41cf660
test: Fix tests
clepski Oct 28, 2024
70ddafb
feat: Fix self reference bug
clepski Oct 28, 2024
2772a04
test: Fix tests
clepski Oct 30, 2024
7ee2e37
test: Skip broken tests
clepski Oct 30, 2024
f50c71c
test: Add Editor tests
clepski Oct 30, 2024
6b2ceb9
test: Add Editor tests
clepski Oct 31, 2024
c4b774d
doc: Add edit api doc
clepski Nov 4, 2024
83e68a6
doc: Add editor action API doc
clepski Nov 4, 2024
305109b
doc: Add link to edit event doc
clepski Nov 4, 2024
c23c849
feat: Trigger validate after edit
clepski Nov 13, 2024
26a01eb
fix: Add review suggestions
clepski Nov 13, 2024
f455522
Merge branch 'main' into feat/edit-api-v2
clepski Nov 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: Add log text and remove old code
clepski committed Oct 17, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
commit a2f6552e35cdea477258d1e8c0205f15fce6d7e2
435 changes: 22 additions & 413 deletions packages/openscd/src/addons/Editor.ts
Original file line number Diff line number Diff line change
@@ -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 === (<Element>create.new.element).tagName &&
elm.getAttribute('name') ===
(<Element>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') ===
(<Element>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,
<SCLTag>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,
<SCLTag>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') ===
(<Element>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<EditorAction>) {
@@ -451,34 +103,6 @@ export class OscdEditor extends LitElement {
this.host.dispatchEvent(newEditEvent(editV2, initiator));
}

/* TODO: Remove
private async onActionOld(event: EditorActionEvent<EditorAction>) {
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
}

1 change: 1 addition & 0 deletions packages/openscd/src/translations/de.ts
Original file line number Diff line number Diff line change
@@ -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',
1 change: 1 addition & 0 deletions packages/openscd/src/translations/en.ts
Original file line number Diff line number Diff line change
@@ -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 }}',