feat(network): add undo/redo shortcuts (Ctrl+Z/Y) and arrow key nudging

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-04-13 20:06:33 +00:00
parent a392d24101
commit 02c19a7580
2 changed files with 46 additions and 1 deletions

View File

@@ -33,6 +33,9 @@ export function useCanvasShortcuts({
setEdges, setEdges,
setIsDirty, setIsDirty,
canvasRef, canvasRef,
onUndo,
onRedo,
onNudge,
}: { }: {
nodes: Node[] nodes: Node[]
edges: Edge[] edges: Edge[]
@@ -40,6 +43,9 @@ export function useCanvasShortcuts({
setEdges: React.Dispatch<React.SetStateAction<Edge[]>> setEdges: React.Dispatch<React.SetStateAction<Edge[]>>
setIsDirty: (dirty: boolean) => void setIsDirty: (dirty: boolean) => void
canvasRef: React.RefObject<HTMLDivElement | null> canvasRef: React.RefObject<HTMLDivElement | null>
onUndo: () => void
onRedo: () => void
onNudge: (dx: number, dy: number) => void
}) { }) {
const { getNodes, fitView, screenToFlowPosition, setNodes: rfSetNodes } = useReactFlow() const { getNodes, fitView, screenToFlowPosition, setNodes: rfSetNodes } = useReactFlow()
const clipboardRef = useRef<ClipboardData | null>(null) const clipboardRef = useRef<ClipboardData | null>(null)
@@ -211,6 +217,31 @@ export function useCanvasShortcuts({
const ctrl = e.ctrlKey || e.metaKey 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') { if (ctrl && e.key === 'c') {
e.preventDefault() e.preventDefault()
copyNodes() copyNodes()
@@ -237,7 +268,7 @@ export function useCanvasShortcuts({
document.addEventListener('keydown', handleKeyDown) document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('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 { return {
copyNodes, copyNodes,

View File

@@ -113,6 +113,17 @@ function DiagramEditorInner() {
const canUndo = historyIndex.current > 0 const canUndo = historyIndex.current > 0
const canRedo = historyIndex.current < historyStack.current.length - 1 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 { const {
copyNodes, copyNodes,
pasteNodes, pasteNodes,
@@ -127,6 +138,9 @@ function DiagramEditorInner() {
setEdges, setEdges,
setIsDirty: (v: boolean) => setIsDirty(v), setIsDirty: (v: boolean) => setIsDirty(v),
canvasRef, canvasRef,
onUndo: undo,
onRedo: redo,
onNudge,
}) })
const handleNodesChange: typeof onNodesChange = useCallback((changes) => { const handleNodesChange: typeof onNodesChange = useCallback((changes) => {