Skip to content

Commit ef19aeb

Browse files
authored
Merge pull request #52 from LIDSOL/fix-update-undo-redo
The undo/redo functionality has been modified to prevent duplicate ac…
2 parents 6f0af51 + a19ba83 commit ef19aeb

24 files changed

Lines changed: 1029 additions & 1209 deletions

‎CONTRIBUTING.md‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,42 @@ Please follow these steps to have your contribution considered by the maintainer
6464
- Ensure your code passes ESLint.
6565
- Make sure the code base is in English, this includes comments and variable names.
6666

67+
## Undo/Redo: pushUndo vs setUndoStack
68+
69+
When recording history, prefer the high-level helper `pushUndo(action)` from UI/components for simple append-and-clear behavior. `pushUndo` centralizes validation, deduplication and coalescing so the app maintains a sane undo/redo history.
70+
71+
However, some modules implement the low-level undo/redo engine and must manipulate the stacks directly using `setUndoStack` / `setRedoStack`. Do NOT replace those internal usages with `pushUndo`.
72+
73+
Files that intentionally use direct stack manipulation include (but are not limited to):
74+
75+
- `src/components/EditorCanvas/Canvas.jsx`
76+
- `src/components/EditorHeader/ControlPanel.jsx`
77+
- `src/components/EditorHeader/Modal/Modal.jsx`
78+
- `src/components/Workspace.jsx`
79+
- `src/context/UndoRedoContext.jsx`
80+
81+
Correct usage (UI-side append+clear):
82+
```js
83+
// Add a simple EDIT action and clear redo
84+
pushUndo({
85+
action: Action.EDIT,
86+
element: ObjectType.TABLE,
87+
tid,
88+
undo,
89+
redo,
90+
message,
91+
});
92+
```
93+
94+
Incorrect to change internal code to this (keep low-level code as-is):
95+
```js
96+
// Internal undo engine — keep setUndoStack for low-level manipulation
97+
setUndoStack((prev) => prev.filter((_, i) => i !== prev.length - 1));
98+
setRedoStack((prev) => [...prev, action]);
99+
```
100+
101+
If you're unsure which to use, prefer `pushUndo` for component-level changes and ask during code review for anything touching the stacks directly.
102+
67103
## Additional Notes
68104

69105
### Issue and Pull Request Labels
@@ -87,3 +123,4 @@ If you have any questions, please feel free to reach out to us through the follo
87123
---
88124

89125
Thank you for your contributions! ❤️
126+

‎src/components/EditorCanvas/Area.jsx‎

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ function EditPopoverContent({ data }) {
203203
const [editField, setEditField] = useState({});
204204
const { setSaveState } = useSaveState();
205205
const { updateArea, deleteArea } = useAreas();
206-
const { setUndoStack, setRedoStack } = useUndoRedo();
206+
const { pushUndo } = useUndoRedo();
207207
const { t } = useTranslation();
208208

209209
return (
@@ -216,11 +216,9 @@ function EditPopoverContent({ data }) {
216216
className="me-2"
217217
onChange={(value) => updateArea(data.id, { name: value })}
218218
onFocus={(e) => setEditField({ name: e.target.value })}
219-
onBlur={(e) => {
220-
if (e.target.value === editField.name) return;
221-
setUndoStack((prev) => [
222-
...prev,
223-
{
219+
onBlur={(e) => {
220+
if (e.target.value === editField.name) return;
221+
pushUndo({
224222
action: Action.EDIT,
225223
element: ObjectType.AREA,
226224
aid: data.id,
@@ -230,32 +228,26 @@ function EditPopoverContent({ data }) {
230228
areaName: e.target.value,
231229
extra: "[name]",
232230
}),
233-
},
234-
]);
235-
setRedoStack([]);
236-
}}
231+
});
232+
}}
237233
/>
238234
<Popover
239235
content={
240236
<div className="popover-theme">
241237
<ColorPalette
242238
currentColor={data.color}
243239
onPickColor={(c) => {
244-
setUndoStack((prev) => [
245-
...prev,
246-
{
247-
action: Action.EDIT,
248-
element: ObjectType.AREA,
249-
aid: data.id,
250-
undo: { color: data.color },
251-
redo: { color: c },
252-
message: t("edit_area", {
253-
areaName: data.name,
254-
extra: "[color]",
255-
}),
256-
},
257-
]);
258-
setRedoStack([]);
240+
pushUndo({
241+
action: Action.EDIT,
242+
element: ObjectType.AREA,
243+
aid: data.id,
244+
undo: { color: data.color },
245+
redo: { color: c },
246+
message: t("edit_area", {
247+
areaName: data.name,
248+
extra: "[color]",
249+
}),
250+
});
259251
updateArea(data.id, {
260252
color: c,
261253
});

‎src/components/EditorCanvas/Canvas.jsx‎

Lines changed: 47 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ export default function Canvas() {
9898
const { notes, updateNote, addNote, deleteNote } = useNotes();
9999
const { layout } = useLayout();
100100
const { settings } = useSettings();
101+
// IMPORTANT: this file implements the internal undo/redo machinery.
102+
// Do NOT replace direct calls to `setUndoStack` or `setRedoStack` here with
103+
// `pushUndo`. `setUndoStack` is intentionally used for low-level operations
104+
// (popping, re-queuing, reconstructing complex actions) that must remain
105+
// under explicit control. `pushUndo` is a higher-level helper for UI-side
106+
// append+clear behavior and will not preserve the internal semantics.
101107
const { undoStack, redoStack, setRedoStack, setUndoStack, pushUndo } =
102108
useUndoRedo();
103109

@@ -808,18 +814,14 @@ export default function Canvas() {
808814
}
809815
}
810816

811-
setUndoStack((prev) => [
812-
...prev,
813-
{
814-
action: Action.EDIT,
815-
element: ObjectType.RELATIONSHIP,
816-
rid: relationshipContextMenu.relationshipId,
817-
undo: undoData,
818-
redo: redoData,
819-
message: `Swap direction for ${relationship.name}`,
820-
},
821-
]);
822-
setRedoStack([]);
817+
pushUndo({
818+
action: Action.EDIT,
819+
element: ObjectType.RELATIONSHIP,
820+
rid: relationshipContextMenu.relationshipId,
821+
undo: undoData,
822+
redo: redoData,
823+
message: `Swap direction for ${relationship.name}`,
824+
});
823825

824826
setRelationships((prev) =>
825827
prev.map((e, idx) => {
@@ -867,28 +869,24 @@ export default function Canvas() {
867869
? SubtypeRestriction.DISJOINT_TOTAL
868870
: undefined;
869871

870-
setUndoStack((prev) => [
871-
...prev,
872-
{
873-
action: Action.EDIT,
874-
element: ObjectType.RELATIONSHIP,
875-
rid: relationshipContextMenu.relationshipId,
876-
undo: {
877-
relationshipType: relationship.relationshipType,
878-
cardinality: relationship.cardinality,
879-
subtype: relationship.subtype,
880-
subtype_restriction: relationship.subtype_restriction,
881-
},
882-
redo: {
883-
relationshipType: newType,
884-
cardinality: defaultCardinality,
885-
subtype: isBecomingSubtype,
886-
subtype_restriction: defaultSubtypeRestriction,
887-
},
888-
message: `Change type for ${relationship.name}`,
872+
pushUndo({
873+
action: Action.EDIT,
874+
element: ObjectType.RELATIONSHIP,
875+
rid: relationshipContextMenu.relationshipId,
876+
undo: {
877+
relationshipType: relationship.relationshipType,
878+
cardinality: relationship.cardinality,
879+
subtype: relationship.subtype,
880+
subtype_restriction: relationship.subtype_restriction,
881+
},
882+
redo: {
883+
relationshipType: newType,
884+
cardinality: defaultCardinality,
885+
subtype: isBecomingSubtype,
886+
subtype_restriction: defaultSubtypeRestriction,
889887
},
890-
]);
891-
setRedoStack([]);
888+
message: `Change type for ${relationship.name}`,
889+
});
892890

893891
// When becoming subtype, convert existing FK fields to primary keys
894892
if (isBecomingSubtype && !wasSubtype) {
@@ -945,18 +943,14 @@ export default function Canvas() {
945943
const relationship =
946944
relationships[relationshipContextMenu.relationshipId];
947945
if (relationship) {
948-
setUndoStack((prev) => [
949-
...prev,
950-
{
951-
action: Action.EDIT,
952-
element: ObjectType.RELATIONSHIP,
953-
rid: relationshipContextMenu.relationshipId,
954-
undo: { cardinality: relationship.cardinality },
955-
redo: { cardinality: newCardinality },
956-
message: `Change cardinality for ${relationship.name}`,
957-
},
958-
]);
959-
setRedoStack([]);
946+
pushUndo({
947+
action: Action.EDIT,
948+
element: ObjectType.RELATIONSHIP,
949+
rid: relationshipContextMenu.relationshipId,
950+
undo: { cardinality: relationship.cardinality },
951+
redo: { cardinality: newCardinality },
952+
message: `Change cardinality for ${relationship.name}`,
953+
});
960954
setRelationships((prev) =>
961955
prev.map((e, idx) =>
962956
idx === relationshipContextMenu.relationshipId
@@ -1023,18 +1017,14 @@ export default function Canvas() {
10231017
return; // Already has default name
10241018
}
10251019

1026-
setUndoStack((prev) => [
1027-
...prev,
1028-
{
1029-
action: Action.EDIT,
1030-
element: ObjectType.RELATIONSHIP,
1031-
rid: relationshipContextMenu.relationshipId,
1032-
undo: { name: relationship.name },
1033-
redo: { name: defaultName },
1034-
message: `Set default name for ${defaultName}`,
1035-
},
1036-
]);
1037-
setRedoStack([]);
1020+
pushUndo({
1021+
action: Action.EDIT,
1022+
element: ObjectType.RELATIONSHIP,
1023+
rid: relationshipContextMenu.relationshipId,
1024+
undo: { name: relationship.name },
1025+
redo: { name: defaultName },
1026+
message: `Set default name for ${defaultName}`,
1027+
});
10381028
updateRelationship(relationshipContextMenu.relationshipId, {
10391029
name: defaultName,
10401030
});
@@ -2540,16 +2530,6 @@ export default function Canvas() {
25402530
setDragging({ element: ObjectType.NONE, id: -1, prevX: 0, prevY: 0 });
25412531
setResizing({ element: ObjectType.NONE, id: -1, prevX: 0, prevY: 0 });
25422532
if (panning.isPanning && didPan()) {
2543-
pushUndo({
2544-
action: Action.PAN,
2545-
undo: { x: panning.x, y: panning.y },
2546-
redo: transform.pan,
2547-
message: t("move_element", {
2548-
coords: `(${transform?.pan.x}, ${transform?.pan.y})`,
2549-
name: "diagram",
2550-
}),
2551-
});
2552-
setRedoStack([]);
25532533
setSelectedElement((prev) => ({
25542534
...prev,
25552535
element: ObjectType.NONE,

‎src/components/EditorCanvas/Note.jsx‎

Lines changed: 34 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export default function Note({ data, onPointerDown, onContextMenu }) {
3131
const { t } = useTranslation();
3232
const { setSaveState } = useSaveState();
3333
const { updateNote, deleteNote } = useNotes();
34-
const { setUndoStack, setRedoStack } = useUndoRedo();
34+
const { pushUndo } = useUndoRedo();
3535
const { selectedElement, setSelectedElement } = useSelect();
3636

3737
const handleChange = (e) => {
@@ -48,21 +48,17 @@ export default function Note({ data, onPointerDown, onContextMenu }) {
4848
textarea.style.height = "0";
4949
textarea.style.height = textarea.scrollHeight + "px";
5050
const newHeight = textarea.scrollHeight + 16 + 20 + 4;
51-
setUndoStack((prev) => [
52-
...prev,
53-
{
54-
action: Action.EDIT,
55-
element: ObjectType.NOTE,
56-
nid: data.id,
57-
undo: editField,
58-
redo: { content: e.target.value, height: newHeight },
59-
message: t("edit_note", {
60-
noteTitle: e.target.value,
61-
extra: "[content]",
62-
}),
63-
},
64-
]);
65-
setRedoStack([]);
51+
pushUndo({
52+
action: Action.EDIT,
53+
element: ObjectType.NOTE,
54+
nid: data.id,
55+
undo: editField,
56+
redo: { content: e.target.value, height: newHeight },
57+
message: t("edit_note", {
58+
noteTitle: e.target.value,
59+
extra: "[content]",
60+
}),
61+
});
6662
};
6763

6864
const edit = () => {
@@ -195,21 +191,17 @@ export default function Note({ data, onPointerDown, onContextMenu }) {
195191
}
196192
onBlur={(e) => {
197193
if (e.target.value === editField.title) return;
198-
setUndoStack((prev) => [
199-
...prev,
200-
{
201-
action: Action.EDIT,
202-
element: ObjectType.NOTE,
203-
nid: data.id,
204-
undo: editField,
205-
redo: { title: e.target.value },
206-
message: t("edit_note", {
207-
noteTitle: e.target.value,
208-
extra: "[title]",
209-
}),
210-
},
211-
]);
212-
setRedoStack([]);
194+
pushUndo({
195+
action: Action.EDIT,
196+
element: ObjectType.NOTE,
197+
nid: data.id,
198+
undo: editField,
199+
redo: { title: e.target.value },
200+
message: t("edit_note", {
201+
noteTitle: e.target.value,
202+
extra: "[title]",
203+
}),
204+
});
213205
}}
214206
/>
215207
<Popover
@@ -226,21 +218,17 @@ export default function Note({ data, onPointerDown, onContextMenu }) {
226218
style={{ backgroundColor: c }}
227219
className="p-3 rounded-full mx-1"
228220
onClick={() => {
229-
setUndoStack((prev) => [
230-
...prev,
231-
{
232-
action: Action.EDIT,
233-
element: ObjectType.NOTE,
234-
nid: data.id,
235-
undo: { color: data.color },
236-
redo: { color: c },
237-
message: t("edit_note", {
238-
noteTitle: data.title,
239-
extra: "[color]",
240-
}),
241-
},
242-
]);
243-
setRedoStack([]);
221+
pushUndo({
222+
action: Action.EDIT,
223+
element: ObjectType.NOTE,
224+
nid: data.id,
225+
undo: { color: data.color },
226+
redo: { color: c },
227+
message: t("edit_note", {
228+
noteTitle: data.title,
229+
extra: "[color]",
230+
}),
231+
});
244232
updateNote(data.id, { color: c });
245233
}}
246234
>

0 commit comments

Comments
 (0)