From b1564325550db4e7a6f71fef1b0801a264f4fd70 Mon Sep 17 00:00:00 2001 From: Alexey Morozov Date: Thu, 28 May 2026 01:17:40 +0300 Subject: [PATCH] Fix undo/redo not adding or removing graph elements or links after reloading the workspace: * Fix undo/redo not adding or removing graph elements or links after `DataDiagramModel.discardLayout()` call (e.g. by a reload from `useLoadedWorkspace()`). * Add translation strings for commands to add/remove graph elements or links, title for "remove vertex" button on a link. --- CHANGELOG.md | 1 + i18n/i18n.schema.json | 7 ++- .../en.reactodia-translation.json | 7 ++- src/diagram/linkLayer.tsx | 40 ++++++------ src/diagram/model.ts | 63 +++++++++++-------- 5 files changed, 70 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 957a141a..8da0350a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p #### 🐛 Fixed - Fix partially or fully hidden outlines for `WorkspaceLayoutItem` headers and `Navigator` toggle button. +- Fix undo/redo not adding or removing graph elements or links after `DataDiagramModel.discardLayout()` call (e.g. by a reload from `useLoadedWorkspace()`). #### ⏱ Performance - Fix canvas panning optimization not being applied due to incorrect `z-index` value. diff --git a/i18n/i18n.schema.json b/i18n/i18n.schema.json index 453cadf3..ae543b97 100644 --- a/i18n/i18n.schema.json +++ b/i18n/i18n.schema.json @@ -101,12 +101,16 @@ "$ref": "#/$defs/Group", "additionalProperties": false, "properties": { + "add_element.command": { "$ref": "#/$defs/Value" }, + "add_link.command": { "$ref": "#/$defs/Value" }, "create_links.command": { "$ref": "#/$defs/Value" }, "group_entities.command": { "$ref": "#/$defs/Value" }, "import_layout.command": { "$ref": "#/$defs/Value" }, "regroup_relations.command": { "$ref": "#/$defs/Value" }, "request_entities.command": { "$ref": "#/$defs/Value" }, "request_relations.command": { "$ref": "#/$defs/Value" }, + "remove_element.command": { "$ref": "#/$defs/Value" }, + "remove_link.command": { "$ref": "#/$defs/Value" }, "ungroup_entities.command": { "$ref": "#/$defs/Value" } } }, @@ -196,7 +200,8 @@ "edit_relation.title_disabled": { "$ref": "#/$defs/Value" }, "rename_link.title": { "$ref": "#/$defs/Value" }, "move_relation.move_source_title": { "$ref": "#/$defs/Value" }, - "move_relation.move_target_title": { "$ref": "#/$defs/Value" } + "move_relation.move_target_title": { "$ref": "#/$defs/Value" }, + "vertex.remove_title": { "$ref": "#/$defs/Value" } } }, "navigator": { diff --git a/i18n/translations/en.reactodia-translation.json b/i18n/translations/en.reactodia-translation.json index 1be1f2e6..c1cb0d90 100644 --- a/i18n/translations/en.reactodia-translation.json +++ b/i18n/translations/en.reactodia-translation.json @@ -74,12 +74,16 @@ "sort_smart.title": "Smart sort" }, "data_diagram_model": { + "add_element.command": "Add element", + "add_link.command": "Add link", "create_links.command": "Create links", "group_entities.command": "Group entities", "import_layout.command": "Import layout", "regroup_relations.command": "Regroup relations", "request_entities.command": "Request entity data", "request_relations.command": "Request relations between entities", + "remove_element.command": "Remove element", + "remove_link.command": "Remove link", "ungroup_entities.command": "Ungroup entities" }, "default_data_locale": { @@ -137,7 +141,8 @@ "edit_relation.title_disabled": "Cannot edit the relation", "rename_link.title": "Rename link", "move_relation.move_source_title": "Move link source", - "move_relation.move_target_title": "Move link target" + "move_relation.move_target_title": "Move link target", + "vertex.remove_title": "Remove vertex" }, "navigator": { "toggle_collapse.title": "Collapse navigator", diff --git a/src/diagram/linkLayer.tsx b/src/diagram/linkLayer.tsx index 449f5a9e..662abce4 100644 --- a/src/diagram/linkLayer.tsx +++ b/src/diagram/linkLayer.tsx @@ -4,6 +4,7 @@ import { createPortal, flushSync } from 'react-dom'; import { EventObserver } from '../coreUtils/events'; import { useEventStore, useSyncStore } from '../coreUtils/hooks'; +import { useTranslation } from '../coreUtils/i18n'; import { HtmlPaperLayer, type PaperTransform } from '../paper/paperLayers'; @@ -674,35 +675,34 @@ export function LinkVertices(props: LinkVerticesProps) { return {elements}; } -class VertexTools extends React.Component<{ +function VertexTools(props: { vertexIndex: number; vertexRadius: number; x: number; y: number; onRemove: (vertexIndex: number) => void; -}> { - render() { - const {vertexRadius, x, y} = this.props; - const transform = `translate(${x + 2 * vertexRadius},${y - 2 * vertexRadius})scale(${vertexRadius})`; - return ( - - Remove vertex - - - - ); - } - - private onRemoveVertex = (e: React.MouseEvent) => { - if (e.button !== 0 /* left button */) { return; } +}) { + const {vertexIndex, vertexRadius, x, y, onRemove} = props; + const t = useTranslation(); + const transform = `translate(${x + 2 * vertexRadius},${y - 2 * vertexRadius})scale(${vertexRadius})`; + const onRemoveVertex = (e: React.MouseEvent) => { + if (e.button !== 0 /* left button */) { + return; + } e.preventDefault(); e.stopPropagation(); - const {onRemove, vertexIndex} = this.props; onRemove(vertexIndex); }; + return ( + + {t.text('link_action.vertex.remove_title')} + + + + ); } function LinkMarkersInner(props: { diff --git a/src/diagram/model.ts b/src/diagram/model.ts index 785a0658..fb689373 100644 --- a/src/diagram/model.ts +++ b/src/diagram/model.ts @@ -1,6 +1,6 @@ import { moveComparator } from '../coreUtils/collections'; import { EventSource, Events, EventObserver, AnyEvent, PropertyChange } from '../coreUtils/events'; -import { Translation } from '../coreUtils/i18n'; +import { TranslatedText, Translation } from '../coreUtils/i18n'; import { LinkTypeIri } from '../data/model'; import * as Rdf from '../data/rdf/rdfModel'; @@ -121,6 +121,8 @@ export interface DiagramModelOptions { translation: Translation; } +const InternalGetGraph: unique symbol = Symbol('DiagramModel.getGraph'); + /** * Stores the diagram content: graph (elements, links); * maintains selection and the current language to display the data. @@ -157,6 +159,11 @@ export class DiagramModel implements GraphStructure { this.history = history; } + /** @hidden */ + [InternalGetGraph]() { + return this.graph; + } + /** * Current language for the diagram content. * @@ -174,7 +181,7 @@ export class DiagramModel implements GraphStructure { */ setLanguage(value: string): void { if (!value) { - throw new Error('Cannot set empty language.'); + throw new Error('Reactodia: cannot set empty language'); } const previous = this._language; if (previous === value) { return; } @@ -351,7 +358,7 @@ export class DiagramModel implements GraphStructure { */ addElement(element: Element): void { this.history.execute( - new AddElementCommand(this.graph, element, []) + new AddElementCommand(this, element, []) ); } @@ -366,7 +373,7 @@ export class DiagramModel implements GraphStructure { const element = this.getElement(elementId); if (element) { this.history.execute( - new RemoveElementCommand(this.graph, element) + new RemoveElementCommand(this, element) ); } } @@ -380,7 +387,7 @@ export class DiagramModel implements GraphStructure { * The operation puts a command to the {@link DiagramModel.history command history}. */ addLink(link: Link): void { - this.history.execute(new AddLinkCommand(this.graph, link)); + this.history.execute(new AddLinkCommand(this, link)); } /** @@ -391,24 +398,25 @@ export class DiagramModel implements GraphStructure { removeLink(linkId: string): void { const link = this.graph.getLink(linkId); if (link) { - this.history.execute(new RemoveLinkCommand(this.graph, link)); + this.history.execute(new RemoveLinkCommand(this, link)); } } } class AddElementCommand implements Command { constructor( - readonly graph: Graph, + readonly model: DiagramModel, readonly element: Element, readonly connectedLinks: ReadonlyArray ) {} - get title(): string { - return 'Add element'; + get title(): TranslatedText { + return TranslatedText.text('data_diagram_model.add_element.command'); } invoke(): Command { - const {graph, element, connectedLinks} = this; + const {model, element, connectedLinks} = this; + const graph = model[InternalGetGraph](); graph.addElement(element); for (const link of connectedLinks) { const existing = graph.getLink(link.id); @@ -416,58 +424,61 @@ class AddElementCommand implements Command { graph.addLink(link); } } - return new RemoveElementCommand(graph, element); + return new RemoveElementCommand(model, element); } } class RemoveElementCommand implements Command { constructor( - readonly graph: Graph, + readonly model: DiagramModel, readonly element: Element ) {} - get title(): string { - return 'Remove element'; + get title(): TranslatedText { + return TranslatedText.text('data_diagram_model.remove_element.command'); } invoke(): Command { - const {graph, element} = this; + const {model, element} = this; + const graph = model[InternalGetGraph](); const connectedLinks = [...graph.getElementLinks(element)]; graph.removeElement(element.id); - return new AddElementCommand(graph, element, connectedLinks); + return new AddElementCommand(model, element, connectedLinks); } } class AddLinkCommand implements Command { constructor( - readonly graph: Graph, + readonly model: DiagramModel, readonly link: Link ) {} - get title(): string { - return 'Add link'; + get title(): TranslatedText { + return TranslatedText.text('data_diagram_model.add_link.command'); } invoke(): Command { - const {graph, link} = this; + const {model, link} = this; + const graph = model[InternalGetGraph](); graph.addLink(link); - return new RemoveLinkCommand(graph, link); + return new RemoveLinkCommand(model, link); } } class RemoveLinkCommand implements Command { constructor( - readonly graph: Graph, + readonly model: DiagramModel, readonly link: Link ) {} - get title(): string { - return 'Remove link'; + get title(): TranslatedText { + return TranslatedText.text('data_diagram_model.remove_link.command'); } invoke(): Command { - const {graph, link} = this; + const {model, link} = this; + const graph = model[InternalGetGraph](); graph.removeLink(link.id); - return new AddLinkCommand(graph, link); + return new AddLinkCommand(model, link); } }