From 02c19a758015a7be55a41c058d588ac649f33692 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Mon, 13 Apr 2026 20:06:33 +0000 Subject: [PATCH] feat(network): add undo/redo shortcuts (Ctrl+Z/Y) and arrow key nudging Co-Authored-By: Claude Sonnet 4.6 --- .../network/hooks/useCanvasShortcuts.ts | 33 ++++++++++++++++++- .../pages/NetworkDiagrams/DiagramEditor.tsx | 14 ++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/network/hooks/useCanvasShortcuts.ts b/frontend/src/components/network/hooks/useCanvasShortcuts.ts index 2f04ff97..2aeb1645 100644 --- a/frontend/src/components/network/hooks/useCanvasShortcuts.ts +++ b/frontend/src/components/network/hooks/useCanvasShortcuts.ts @@ -33,6 +33,9 @@ export function useCanvasShortcuts({ setEdges, setIsDirty, canvasRef, + onUndo, + onRedo, + onNudge, }: { nodes: Node[] edges: Edge[] @@ -40,6 +43,9 @@ export function useCanvasShortcuts({ setEdges: React.Dispatch> setIsDirty: (dirty: boolean) => void canvasRef: React.RefObject + onUndo: () => void + onRedo: () => void + onNudge: (dx: number, dy: number) => void }) { const { getNodes, fitView, screenToFlowPosition, setNodes: rfSetNodes } = useReactFlow() const clipboardRef = useRef(null) @@ -211,6 +217,31 @@ export function useCanvasShortcuts({ const ctrl = e.ctrlKey || e.metaKey + // Undo: Ctrl+Z / Cmd+Z + if (e.key === 'z' && ctrl && !e.shiftKey) { + e.preventDefault() + onUndo() + return + } + // Redo: Ctrl+Y or Ctrl+Shift+Z + if ((e.key === 'y' && ctrl) || (e.key === 'z' && ctrl && e.shiftKey)) { + e.preventDefault() + onRedo() + return + } + // Arrow key nudging + if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) { + e.preventDefault() + const delta = e.shiftKey ? 10 : 1 + switch (e.key) { + case 'ArrowUp': onNudge(0, -delta); break + case 'ArrowDown': onNudge(0, delta); break + case 'ArrowLeft': onNudge(-delta, 0); break + case 'ArrowRight': onNudge( delta, 0); break + } + return + } + if (ctrl && e.key === 'c') { e.preventDefault() copyNodes() @@ -237,7 +268,7 @@ export function useCanvasShortcuts({ document.addEventListener('keydown', handleKeyDown) return () => document.removeEventListener('keydown', handleKeyDown) - }, [copyNodes, pasteNodes, duplicateNodes, selectAll, fitView, bringSelectedToFront, sendSelectedToBack]) + }, [copyNodes, pasteNodes, duplicateNodes, selectAll, fitView, bringSelectedToFront, sendSelectedToBack, onUndo, onRedo, onNudge]) return { copyNodes, diff --git a/frontend/src/pages/NetworkDiagrams/DiagramEditor.tsx b/frontend/src/pages/NetworkDiagrams/DiagramEditor.tsx index 2e99d718..24a93f9f 100644 --- a/frontend/src/pages/NetworkDiagrams/DiagramEditor.tsx +++ b/frontend/src/pages/NetworkDiagrams/DiagramEditor.tsx @@ -113,6 +113,17 @@ function DiagramEditorInner() { const canUndo = historyIndex.current > 0 const canRedo = historyIndex.current < historyStack.current.length - 1 + const onNudge = useCallback((dx: number, dy: number) => { + const selected = nodes.filter(n => n.selected) + if (selected.length === 0) return + pushHistory(nodes, edges) + setNodes(prev => prev.map(n => + n.selected + ? { ...n, position: { x: n.position.x + dx, y: n.position.y + dy } } + : n + )) + }, [nodes, edges, pushHistory, setNodes]) + const { copyNodes, pasteNodes, @@ -127,6 +138,9 @@ function DiagramEditorInner() { setEdges, setIsDirty: (v: boolean) => setIsDirty(v), canvasRef, + onUndo: undo, + onRedo: redo, + onNudge, }) const handleNodesChange: typeof onNodesChange = useCallback((changes) => {