fix: LXC/VM parent-child UX bugs with Proxmox container mode
- Don't create virtual edge when parent Proxmox is in container mode (containment is shown visually — edge was redundant and confusing) - updateNode now syncs React Flow parentId/extent/position when data.parent_id changes, so nesting/un-nesting is immediate without requiring a save+reload - Attaching to container: converts position to parent-relative coords - Detaching from container: converts position back to absolute coords so the node escapes the container box immediately - Ensure parent nodes precede children in array on attachment (React Flow rendering requirement)
This commit is contained in:
@@ -258,10 +258,13 @@ export default function App() {
|
||||
)
|
||||
if (oldEdge) deleteEdge(oldEdge.id)
|
||||
}
|
||||
// Create new virtual edge: LXC top → Proxmox bottom
|
||||
// Create virtual edge only when parent is NOT in container mode
|
||||
// (container mode shows containment visually — no edge needed)
|
||||
if (newParentId) {
|
||||
// Pass type as extra field — canvasStore.onConnect casts to Connection & Partial<EdgeData>
|
||||
onConnect({ source: editNodeId, sourceHandle: 'top', target: newParentId, targetHandle: 'bottom', type: 'virtual' } as unknown as Connection)
|
||||
const parentNode = nodes.find((n) => n.id === newParentId)
|
||||
if (!parentNode?.data.container_mode) {
|
||||
onConnect({ source: editNodeId, sourceHandle: 'top', target: newParentId, targetHandle: 'bottom', type: 'virtual' } as unknown as Connection)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,57 @@ describe('canvasStore', () => {
|
||||
expect(node?.data.ip).toBe('10.0.0.1')
|
||||
})
|
||||
|
||||
it('updateNode setting parent_id on container-mode proxmox sets parentId and relative position', () => {
|
||||
const proxmox = { ...makeNode('px1', { type: 'proxmox', container_mode: true }), position: { x: 100, y: 100 } }
|
||||
const lxc = { ...makeNode('lxc1', { type: 'lxc' }), position: { x: 160, y: 180 } }
|
||||
useCanvasStore.getState().addNode(proxmox)
|
||||
useCanvasStore.getState().addNode(lxc)
|
||||
useCanvasStore.getState().updateNode('lxc1', { parent_id: 'px1' })
|
||||
const node = useCanvasStore.getState().nodes.find((n) => n.id === 'lxc1')
|
||||
expect(node?.parentId).toBe('px1')
|
||||
expect(node?.extent).toBe('parent')
|
||||
// Position should be relative to parent (160-100=60, 180-100=80)
|
||||
expect(node?.position.x).toBe(60)
|
||||
expect(node?.position.y).toBe(80)
|
||||
})
|
||||
|
||||
it('updateNode setting parent_id on non-container proxmox does NOT set React Flow parentId', () => {
|
||||
const proxmox = { ...makeNode('px1', { type: 'proxmox', container_mode: false }), position: { x: 100, y: 100 } }
|
||||
const lxc = { ...makeNode('lxc1', { type: 'lxc' }), position: { x: 160, y: 180 } }
|
||||
useCanvasStore.getState().addNode(proxmox)
|
||||
useCanvasStore.getState().addNode(lxc)
|
||||
useCanvasStore.getState().updateNode('lxc1', { parent_id: 'px1' })
|
||||
const node = useCanvasStore.getState().nodes.find((n) => n.id === 'lxc1')
|
||||
expect(node?.parentId).toBeUndefined()
|
||||
expect(node?.extent).toBeUndefined()
|
||||
})
|
||||
|
||||
it('updateNode clearing parent_id converts position to absolute and clears parentId', () => {
|
||||
const proxmox = { ...makeNode('px1', { type: 'proxmox', container_mode: true }), position: { x: 100, y: 100 } }
|
||||
const lxc = { ...makeNode('lxc1', { type: 'lxc', parent_id: 'px1' }), position: { x: 30, y: 40 }, parentId: 'px1', extent: 'parent' as const }
|
||||
useCanvasStore.getState().addNode(proxmox)
|
||||
useCanvasStore.getState().addNode(lxc)
|
||||
useCanvasStore.getState().updateNode('lxc1', { parent_id: undefined })
|
||||
const node = useCanvasStore.getState().nodes.find((n) => n.id === 'lxc1')
|
||||
expect(node?.parentId).toBeUndefined()
|
||||
expect(node?.extent).toBeUndefined()
|
||||
// Position should be absolute (100+30=130, 100+40=140)
|
||||
expect(node?.position.x).toBe(130)
|
||||
expect(node?.position.y).toBe(140)
|
||||
})
|
||||
|
||||
it('updateNode with parent_id puts parents before children in array', () => {
|
||||
const proxmox = { ...makeNode('px1', { type: 'proxmox', container_mode: true }), position: { x: 0, y: 0 } }
|
||||
const lxc = { ...makeNode('lxc1', { type: 'lxc' }), position: { x: 10, y: 10 } }
|
||||
useCanvasStore.getState().addNode(proxmox)
|
||||
useCanvasStore.getState().addNode(lxc)
|
||||
useCanvasStore.getState().updateNode('lxc1', { parent_id: 'px1' })
|
||||
const { nodes } = useCanvasStore.getState()
|
||||
const pxIdx = nodes.findIndex((n) => n.id === 'px1')
|
||||
const lxcIdx = nodes.findIndex((n) => n.id === 'lxc1')
|
||||
expect(pxIdx).toBeLessThan(lxcIdx)
|
||||
})
|
||||
|
||||
it('deleteNode removes node and its connected edges', () => {
|
||||
const store = useCanvasStore.getState()
|
||||
store.addNode(makeNode('n1'))
|
||||
|
||||
@@ -187,12 +187,47 @@ export const useCanvasStore = create<CanvasState>((set) => ({
|
||||
}),
|
||||
|
||||
updateNode: (id, data) =>
|
||||
set((state) => ({
|
||||
nodes: state.nodes.map((n) =>
|
||||
n.id === id ? { ...n, data: { ...n.data, ...data } } : n
|
||||
),
|
||||
hasUnsavedChanges: true,
|
||||
})),
|
||||
set((state) => {
|
||||
let nodes = state.nodes.map((n) => {
|
||||
if (n.id !== id) return n
|
||||
const updated: Node<NodeData> = { ...n, data: { ...n.data, ...data } }
|
||||
if ('parent_id' in data) {
|
||||
const newParentId = data.parent_id ?? undefined
|
||||
if (!newParentId && n.parentId) {
|
||||
// Detaching from a container: convert position back to absolute canvas coords
|
||||
const parent = state.nodes.find((p) => p.id === n.parentId)
|
||||
if (parent) {
|
||||
updated.position = {
|
||||
x: parent.position.x + n.position.x,
|
||||
y: parent.position.y + n.position.y,
|
||||
}
|
||||
}
|
||||
updated.parentId = undefined
|
||||
updated.extent = undefined
|
||||
} else if (newParentId && newParentId !== n.parentId) {
|
||||
const parent = state.nodes.find((p) => p.id === newParentId)
|
||||
if (parent?.data.container_mode) {
|
||||
// Attaching to a container-mode Proxmox: nest visually
|
||||
updated.parentId = newParentId
|
||||
updated.extent = 'parent' as const
|
||||
// Convert absolute position to parent-relative (keep node visible inside)
|
||||
updated.position = {
|
||||
x: Math.max(10, n.position.x - parent.position.x),
|
||||
y: Math.max(10, n.position.y - parent.position.y),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return updated
|
||||
})
|
||||
// React Flow requires parent nodes to precede their children in the array
|
||||
if ('parent_id' in data) {
|
||||
const parents = nodes.filter((n) => !n.parentId)
|
||||
const children = nodes.filter((n) => !!n.parentId)
|
||||
nodes = [...parents, ...children]
|
||||
}
|
||||
return { nodes, hasUnsavedChanges: true }
|
||||
}),
|
||||
|
||||
deleteNode: (id) =>
|
||||
set((state) => {
|
||||
|
||||
Reference in New Issue
Block a user