diff --git a/frontend/src/components/canvas/CanvasContainer.tsx b/frontend/src/components/canvas/CanvasContainer.tsx
index 8fb6f97..9892cac 100644
--- a/frontend/src/components/canvas/CanvasContainer.tsx
+++ b/frontend/src/components/canvas/CanvasContainer.tsx
@@ -27,7 +27,7 @@ export function CanvasContainer({ onConnect: onConnectProp, onEdgeDoubleClick, o
const {
nodes, edges,
onNodesChange, onEdgesChange,
- setSelectedNode,
+ setSelectedNode, snapshotHistory,
} = useCanvasStore()
const activeTheme = useThemeStore((s) => s.activeTheme)
@@ -59,6 +59,8 @@ export function CanvasContainer({ onConnect: onConnectProp, onEdgeDoubleClick, o
onNodeDragStart={onNodeDragStart}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
+ deleteKeyCode={['Backspace', 'Delete']}
+ onBeforeDelete={async () => { snapshotHistory(); return true }}
snapToGrid
snapGrid={[16, 16]}
fitView
diff --git a/frontend/src/components/canvas/__tests__/CanvasContainer.test.tsx b/frontend/src/components/canvas/__tests__/CanvasContainer.test.tsx
index b71adc8..1c51f84 100644
--- a/frontend/src/components/canvas/__tests__/CanvasContainer.test.tsx
+++ b/frontend/src/components/canvas/__tests__/CanvasContainer.test.tsx
@@ -143,4 +143,22 @@ describe('CanvasContainer', () => {
render()
expect(rfProps.snapGrid).toEqual([16, 16])
})
+
+ // ── Delete key ────────────────────────────────────────────────────────────
+
+ it('sets deleteKeyCode to include both Backspace and Delete', () => {
+ render()
+ expect(rfProps.deleteKeyCode).toEqual(['Backspace', 'Delete'])
+ })
+
+ // ── onBeforeDelete snapshot ───────────────────────────────────────────────
+
+ it('onBeforeDelete calls snapshotHistory and returns true', async () => {
+ const snapshotHistory = vi.fn()
+ useCanvasStore.setState({ snapshotHistory } as unknown as Parameters[0])
+ render()
+ const result = await (rfProps.onBeforeDelete as () => Promise)()
+ expect(snapshotHistory).toHaveBeenCalledOnce()
+ expect(result).toBe(true)
+ })
})
diff --git a/frontend/src/components/panels/DetailPanel.tsx b/frontend/src/components/panels/DetailPanel.tsx
index 63a74af..ca046c3 100644
--- a/frontend/src/components/panels/DetailPanel.tsx
+++ b/frontend/src/components/panels/DetailPanel.tsx
@@ -15,7 +15,7 @@ type SvcForm = { port: string; protocol: 'tcp' | 'udp'; service_name: string }
const EMPTY_FORM: SvcForm = { port: '', protocol: 'tcp', service_name: '' }
export function DetailPanel({ onEdit }: DetailPanelProps) {
- const { nodes, selectedNodeId, setSelectedNode, deleteNode, updateNode } = useCanvasStore()
+ const { nodes, selectedNodeId, setSelectedNode, deleteNode, updateNode, snapshotHistory } = useCanvasStore()
const node = nodes.find((n) => n.id === selectedNodeId)
const [addingForNode, setAddingForNode] = useState(null)
@@ -35,6 +35,7 @@ export function DetailPanel({ onEdit }: DetailPanelProps) {
const handleDelete = () => {
if (confirm(`Delete "${data.label}"?`)) {
+ snapshotHistory()
deleteNode(node.id)
}
}
diff --git a/frontend/src/components/panels/__tests__/DetailPanel.test.tsx b/frontend/src/components/panels/__tests__/DetailPanel.test.tsx
index 231225b..ca39877 100644
--- a/frontend/src/components/panels/__tests__/DetailPanel.test.tsx
+++ b/frontend/src/components/panels/__tests__/DetailPanel.test.tsx
@@ -29,6 +29,7 @@ function setupStore(nodeData: Partial = {}) {
setSelectedNode: vi.fn(),
deleteNode: vi.fn(),
updateNode: vi.fn(),
+ snapshotHistory: vi.fn(),
} as unknown as ReturnType)
}
@@ -40,6 +41,7 @@ describe('DetailPanel', () => {
setSelectedNode: vi.fn(),
deleteNode: vi.fn(),
updateNode: vi.fn(),
+ snapshotHistory: vi.fn(),
} as unknown as ReturnType)
})
@@ -125,6 +127,7 @@ describe('DetailPanel', () => {
setSelectedNode,
deleteNode: vi.fn(),
updateNode: vi.fn(),
+ snapshotHistory: vi.fn(),
} as unknown as ReturnType)
render()
fireEvent.click(screen.getByLabelText('Close panel'))
@@ -139,33 +142,39 @@ describe('DetailPanel', () => {
expect(onEdit).toHaveBeenCalledWith('n1')
})
- it('calls deleteNode when delete confirmed', () => {
+ it('calls snapshotHistory then deleteNode when delete confirmed', () => {
const deleteNode = vi.fn()
+ const snapshotHistory = vi.fn()
vi.mocked(canvasStore.useCanvasStore).mockReturnValue({
nodes: [makeNode({ label: 'My Server' })],
selectedNodeId: 'n1',
setSelectedNode: vi.fn(),
deleteNode,
updateNode: vi.fn(),
+ snapshotHistory,
} as unknown as ReturnType)
vi.spyOn(window, 'confirm').mockReturnValue(true)
render()
fireEvent.click(screen.getByLabelText('Delete node'))
+ expect(snapshotHistory).toHaveBeenCalledOnce()
expect(deleteNode).toHaveBeenCalledWith('n1')
})
- it('does not call deleteNode when delete is cancelled', () => {
+ it('does not call deleteNode or snapshotHistory when delete is cancelled', () => {
const deleteNode = vi.fn()
+ const snapshotHistory = vi.fn()
vi.mocked(canvasStore.useCanvasStore).mockReturnValue({
nodes: [makeNode({})],
selectedNodeId: 'n1',
setSelectedNode: vi.fn(),
deleteNode,
updateNode: vi.fn(),
+ snapshotHistory,
} as unknown as ReturnType)
vi.spyOn(window, 'confirm').mockReturnValue(false)
render()
fireEvent.click(screen.getByLabelText('Delete node'))
+ expect(snapshotHistory).not.toHaveBeenCalled()
expect(deleteNode).not.toHaveBeenCalled()
})
})
@@ -186,6 +195,7 @@ describe('DetailPanel', () => {
setSelectedNode: vi.fn(),
deleteNode: vi.fn(),
updateNode,
+ snapshotHistory: vi.fn(),
} as unknown as ReturnType)
render()
fireEvent.click(screen.getByText('Add'))
@@ -207,6 +217,7 @@ describe('DetailPanel', () => {
setSelectedNode: vi.fn(),
deleteNode: vi.fn(),
updateNode,
+ snapshotHistory: vi.fn(),
} as unknown as ReturnType)
render()
fireEvent.click(screen.getByTitle('Remove service'))
@@ -221,6 +232,7 @@ describe('DetailPanel', () => {
setSelectedNode: vi.fn(),
deleteNode: vi.fn(),
updateNode: vi.fn(),
+ snapshotHistory: vi.fn(),
} as unknown as ReturnType)
expect(() => render()).not.toThrow()
})
@@ -249,6 +261,7 @@ describe('DetailPanel', () => {
setSelectedNode: vi.fn(),
deleteNode: vi.fn(),
updateNode,
+ snapshotHistory: vi.fn(),
} as unknown as ReturnType)
render()
@@ -271,6 +284,7 @@ describe('DetailPanel', () => {
setSelectedNode: vi.fn(),
deleteNode: vi.fn(),
updateNode,
+ snapshotHistory: vi.fn(),
} as unknown as ReturnType)
render()