feat(canvas): edge endpoint reconnect + proxmox container snap points

- Selected edge shows interactive dots at both endpoints; drag onto any
  handle (different snap point or different node) to reconnect. Edge
  follows the cursor live during drag; drop on empty space leaves edge
  unchanged.
- Endpoint dots render in EdgeLabelRenderer above the node layer and walk
  the elementsFromPoint stack so the underlying handle wins over the
  dragged dot itself.
- ProxmoxGroupNode in container mode now renders bottom_handles snap
  points (was always single 'bottom' handle), matching BaseNode.
- New canvasStore.reconnectEdge action: swaps source/target/handles via
  normalizeHandle, snapshots history, marks dirty.
This commit is contained in:
Pouzor
2026-05-15 03:05:38 +02:00
parent 4795dc40b8
commit 17d4ddd070
6 changed files with 227 additions and 12 deletions
@@ -497,6 +497,31 @@ describe('canvasStore', () => {
expect(useCanvasStore.getState().hasUnsavedChanges).toBe(true)
})
it('reconnectEdge swaps source/target and normalizes handles', () => {
useCanvasStore.setState((s) => ({
edges: [...s.edges, { ...makeEdge('e1', 'n1', 'n2'), sourceHandle: 'bottom', targetHandle: 'top' }],
}))
useCanvasStore.getState().markSaved()
useCanvasStore.getState().reconnectEdge('e1', {
source: 'n1',
target: 'n3',
sourceHandle: 'bottom-2-t',
targetHandle: 'top-t',
})
const edge = useCanvasStore.getState().edges.find((e) => e.id === 'e1')
expect(edge?.target).toBe('n3')
expect(edge?.source).toBe('n1')
expect(edge?.sourceHandle).toBe('bottom-2')
expect(edge?.targetHandle).toBe('top')
expect(useCanvasStore.getState().hasUnsavedChanges).toBe(true)
})
it('reconnectEdge snapshots history for undo', () => {
useCanvasStore.setState((s) => ({ edges: [...s.edges, makeEdge('e1', 'n1', 'n2')], past: [] }))
useCanvasStore.getState().reconnectEdge('e1', { source: 'n1', target: 'n3', sourceHandle: null, targetHandle: null })
expect(useCanvasStore.getState().past.length).toBe(1)
})
it('deleteEdge removes the edge and marks unsaved', () => {
useCanvasStore.setState((s) => ({ edges: [...s.edges, makeEdge('e1', 'n1', 'n2'), makeEdge('e2', 'n2', 'n3')] }))
useCanvasStore.getState().markSaved()