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()