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:
@@ -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()
|
||||
|
||||
@@ -44,6 +44,7 @@ interface CanvasState {
|
||||
updateNode: (id: string, data: Partial<NodeData>) => void
|
||||
deleteNode: (id: string) => void
|
||||
updateEdge: (id: string, data: Partial<EdgeData>) => void
|
||||
reconnectEdge: (id: string, connection: Connection) => void
|
||||
deleteEdge: (id: string) => void
|
||||
setProxmoxContainerMode: (proxmoxId: string, enabled: boolean) => void
|
||||
setNodeZIndex: (id: string, zIndex: number) => void
|
||||
@@ -292,6 +293,24 @@ export const useCanvasStore = create<CanvasState>((set) => ({
|
||||
hasUnsavedChanges: true,
|
||||
})),
|
||||
|
||||
reconnectEdge: (id, connection) =>
|
||||
set((state) => ({
|
||||
edges: state.edges.map((e) =>
|
||||
e.id === id
|
||||
? {
|
||||
...e,
|
||||
source: connection.source ?? e.source,
|
||||
target: connection.target ?? e.target,
|
||||
sourceHandle: normalizeHandle(connection.sourceHandle),
|
||||
targetHandle: normalizeHandle(connection.targetHandle),
|
||||
}
|
||||
: e
|
||||
),
|
||||
past: [...state.past.slice(-49), { nodes: state.nodes, edges: state.edges }],
|
||||
future: [],
|
||||
hasUnsavedChanges: true,
|
||||
})),
|
||||
|
||||
deleteEdge: (id) =>
|
||||
set((state) => ({
|
||||
edges: state.edges.filter((e) => e.id !== id),
|
||||
|
||||
Reference in New Issue
Block a user