Add undo/redo to planner

Yjs UndoManager tracks waypoints, no-go areas, and notes. Only
local mutations (origin "local") are undoable — remote changes
from other users are not.

- Keyboard shortcuts: Ctrl/Cmd+Z (undo), Ctrl/Cmd+Shift+Z/Y (redo)
- Shortcuts suppressed in text inputs (browser-native undo there)
- Undo/redo buttons in header with disabled state
- stopCapturing on drag to isolate drag as one undo step
- All mutation sites wrapped with "local" origin
- 5 unit tests for UndoManager behavior
- Archived undo-redo change

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-04-04 10:04:01 +01:00
parent 52bd063a2a
commit 743169550e
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9
14 changed files with 236 additions and 32 deletions

View file

@ -294,23 +294,26 @@ export function PlannerMap({ yjs, onRouteRequest, highlightPosition, onImportErr
const addWaypoint = useCallback(
(lat: number, lng: number) => {
const yMap = new Y.Map();
yMap.set("lat", lat);
yMap.set("lon", lng);
yjs.waypoints.push([yMap]);
yjs.doc.transact(() => {
const yMap = new Y.Map();
yMap.set("lat", lat);
yMap.set("lon", lng);
yjs.waypoints.push([yMap]);
}, "local");
},
[yjs.waypoints],
[yjs.doc, yjs.waypoints],
);
const insertWaypointAtSegment = useCallback(
(segmentIndex: number, lat: number, lon: number) => {
const yMap = new Y.Map();
yMap.set("lat", lat);
yMap.set("lon", lon);
// Insert after the segment's start waypoint
yjs.waypoints.insert(segmentIndex + 1, [yMap]);
yjs.doc.transact(() => {
const yMap = new Y.Map();
yMap.set("lat", lat);
yMap.set("lon", lon);
yjs.waypoints.insert(segmentIndex + 1, [yMap]);
}, "local");
},
[yjs.waypoints],
[yjs.doc, yjs.waypoints],
);
const handleRouteInsert = useCallback(
@ -329,7 +332,7 @@ export function PlannerMap({ yjs, onRouteRequest, highlightPosition, onImportErr
yjs.doc.transact(() => {
yMap.set("lat", lat);
yMap.set("lon", lng);
});
}, "local");
}
},
[yjs.waypoints, yjs.doc],
@ -337,7 +340,9 @@ export function PlannerMap({ yjs, onRouteRequest, highlightPosition, onImportErr
const deleteWaypoint = useCallback(
(index: number) => {
yjs.waypoints.delete(index, 1);
yjs.doc.transact(() => {
yjs.waypoints.delete(index, 1);
}, "local");
},
[yjs.waypoints],
);
@ -395,7 +400,7 @@ export function PlannerMap({ yjs, onRouteRequest, highlightPosition, onImportErr
yMap.set("points", area.points);
yjs.noGoAreas.push([yMap]);
}
});
}, "local");
} catch {
onImportError?.(t("importGpxError"));
}
@ -449,6 +454,7 @@ export function PlannerMap({ yjs, onRouteRequest, highlightPosition, onImportErr
},
dragstart: () => {
waypointDraggingRef.current = true;
yjs.undoManager.stopCapturing();
routeInteractionSuspendedRef.current = true;
},
dragend: (e) => {