fix: DEL key deletes nodes and deletion is now undoable
- Add 'Delete' to deleteKeyCode so both Backspace and Delete remove nodes - Call snapshotHistory() in onBeforeDelete (keyboard) and in DetailPanel handleDelete (button) so deletions can be undone with Ctrl+Z
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -143,4 +143,22 @@ describe('CanvasContainer', () => {
|
||||
render(<CanvasContainer />)
|
||||
expect(rfProps.snapGrid).toEqual([16, 16])
|
||||
})
|
||||
|
||||
// ── Delete key ────────────────────────────────────────────────────────────
|
||||
|
||||
it('sets deleteKeyCode to include both Backspace and Delete', () => {
|
||||
render(<CanvasContainer />)
|
||||
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<typeof useCanvasStore.setState>[0])
|
||||
render(<CanvasContainer />)
|
||||
const result = await (rfProps.onBeforeDelete as () => Promise<boolean>)()
|
||||
expect(snapshotHistory).toHaveBeenCalledOnce()
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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<string | null>(null)
|
||||
@@ -35,6 +35,7 @@ export function DetailPanel({ onEdit }: DetailPanelProps) {
|
||||
|
||||
const handleDelete = () => {
|
||||
if (confirm(`Delete "${data.label}"?`)) {
|
||||
snapshotHistory()
|
||||
deleteNode(node.id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ function setupStore(nodeData: Partial<NodeData> = {}) {
|
||||
setSelectedNode: vi.fn(),
|
||||
deleteNode: vi.fn(),
|
||||
updateNode: vi.fn(),
|
||||
snapshotHistory: vi.fn(),
|
||||
} as unknown as ReturnType<typeof canvasStore.useCanvasStore>)
|
||||
}
|
||||
|
||||
@@ -40,6 +41,7 @@ describe('DetailPanel', () => {
|
||||
setSelectedNode: vi.fn(),
|
||||
deleteNode: vi.fn(),
|
||||
updateNode: vi.fn(),
|
||||
snapshotHistory: vi.fn(),
|
||||
} as unknown as ReturnType<typeof canvasStore.useCanvasStore>)
|
||||
})
|
||||
|
||||
@@ -125,6 +127,7 @@ describe('DetailPanel', () => {
|
||||
setSelectedNode,
|
||||
deleteNode: vi.fn(),
|
||||
updateNode: vi.fn(),
|
||||
snapshotHistory: vi.fn(),
|
||||
} as unknown as ReturnType<typeof canvasStore.useCanvasStore>)
|
||||
render(<DetailPanel onEdit={vi.fn()} />)
|
||||
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<typeof canvasStore.useCanvasStore>)
|
||||
vi.spyOn(window, 'confirm').mockReturnValue(true)
|
||||
render(<DetailPanel onEdit={vi.fn()} />)
|
||||
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<typeof canvasStore.useCanvasStore>)
|
||||
vi.spyOn(window, 'confirm').mockReturnValue(false)
|
||||
render(<DetailPanel onEdit={vi.fn()} />)
|
||||
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<typeof canvasStore.useCanvasStore>)
|
||||
render(<DetailPanel onEdit={vi.fn()} />)
|
||||
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<typeof canvasStore.useCanvasStore>)
|
||||
render(<DetailPanel onEdit={vi.fn()} />)
|
||||
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<typeof canvasStore.useCanvasStore>)
|
||||
expect(() => render(<DetailPanel onEdit={vi.fn()} />)).not.toThrow()
|
||||
})
|
||||
@@ -249,6 +261,7 @@ describe('DetailPanel', () => {
|
||||
setSelectedNode: vi.fn(),
|
||||
deleteNode: vi.fn(),
|
||||
updateNode,
|
||||
snapshotHistory: vi.fn(),
|
||||
} as unknown as ReturnType<typeof canvasStore.useCanvasStore>)
|
||||
|
||||
render(<DetailPanel onEdit={vi.fn()} />)
|
||||
@@ -271,6 +284,7 @@ describe('DetailPanel', () => {
|
||||
setSelectedNode: vi.fn(),
|
||||
deleteNode: vi.fn(),
|
||||
updateNode,
|
||||
snapshotHistory: vi.fn(),
|
||||
} as unknown as ReturnType<typeof canvasStore.useCanvasStore>)
|
||||
|
||||
render(<DetailPanel onEdit={vi.fn()} />)
|
||||
|
||||
Reference in New Issue
Block a user