fix: don't trap new child nodes under a non-container parent
Adding an LXC/VM with a parent_id pointed at a Proxmox node that is NOT in container_mode set extent:'parent' anyway, confining the node to the parent's ~140px bounding box with no way to drag it out. Nesting is now gated on the parent's container_mode in both places: App.handleAddNode no longer sets parentId/extent itself (it seeds an absolute position and lets addNode decide), and addNode strips any stray parentId/extent when it isn't nesting. New children of a real container still land at the container's top-left. ha-relevant: yes
This commit is contained in:
+10
-4
@@ -245,9 +245,16 @@ export default function App() {
|
|||||||
const id = generateUUID()
|
const id = generateUUID()
|
||||||
const isContainerNode = data.container_mode === true
|
const isContainerNode = data.container_mode === true
|
||||||
const parentNode = data.parent_id ? nodes.find((n) => n.id === data.parent_id) : null
|
const parentNode = data.parent_id ? nodes.find((n) => n.id === data.parent_id) : null
|
||||||
// Children position is relative to parent; place near top-left with padding
|
// Only nest when the parent is an actual container. For a non-container
|
||||||
const position = parentNode
|
// parent the LXC/VM stays a free node (linked by a virtual edge) — setting
|
||||||
? { x: 20, y: 50 }
|
// extent:'parent' on a non-container would trap it inside the parent's tiny
|
||||||
|
// bounding box with no way to drag it out (issue #205 follow-up).
|
||||||
|
const nestInParent = !!parentNode?.data.container_mode
|
||||||
|
// Seed an ABSOLUTE position near the container's top-left; addNode converts
|
||||||
|
// it to container-relative. addNode is the single authority for parentId /
|
||||||
|
// extent, so we don't set them here.
|
||||||
|
const position = nestInParent && parentNode
|
||||||
|
? { x: parentNode.position.x + 20, y: parentNode.position.y + 50 }
|
||||||
: { x: 300, y: 300 }
|
: { x: 300, y: 300 }
|
||||||
|
|
||||||
const newNode: Node<NodeData> = {
|
const newNode: Node<NodeData> = {
|
||||||
@@ -255,7 +262,6 @@ export default function App() {
|
|||||||
type: data.type ?? 'generic',
|
type: data.type ?? 'generic',
|
||||||
position,
|
position,
|
||||||
data: { status: 'unknown', services: [], ...data } as NodeData,
|
data: { status: 'unknown', services: [], ...data } as NodeData,
|
||||||
...(data.parent_id ? { parentId: data.parent_id, extent: 'parent' as const } : {}),
|
|
||||||
...(isContainerNode ? { width: 300, height: 200 } : {}),
|
...(isContainerNode ? { width: 300, height: 200 } : {}),
|
||||||
}
|
}
|
||||||
addNode(newNode)
|
addNode(newNode)
|
||||||
|
|||||||
@@ -150,6 +150,23 @@ describe('canvasStore', () => {
|
|||||||
expect(nested?.extent).toBe('parent')
|
expect(nested?.extent).toBe('parent')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('addNode strips parentId/extent when the parent is not a container', () => {
|
||||||
|
// Regression: a stray extent:'parent' on a non-container parent traps the
|
||||||
|
// node in the parent's tiny box with no way to drag it out (issue #205).
|
||||||
|
const parent = { ...makeNode('p1', { container_mode: false }), position: { x: 100, y: 100 } }
|
||||||
|
useCanvasStore.getState().addNode(parent)
|
||||||
|
const trapped: Node<NodeData> = {
|
||||||
|
...makeNode('c1', { parent_id: 'p1' }),
|
||||||
|
position: { x: 150, y: 180 },
|
||||||
|
parentId: 'p1',
|
||||||
|
extent: 'parent',
|
||||||
|
}
|
||||||
|
useCanvasStore.getState().addNode(trapped)
|
||||||
|
const child = useCanvasStore.getState().nodes.find((n) => n.id === 'c1')
|
||||||
|
expect(child?.parentId).toBeUndefined()
|
||||||
|
expect(child?.extent).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
it('docker_container nests under docker_host with container_mode on', () => {
|
it('docker_container nests under docker_host with container_mode on', () => {
|
||||||
const host = { ...makeNode('dh1', { type: 'docker_host', container_mode: true }), position: { x: 100, y: 100 } }
|
const host = { ...makeNode('dh1', { type: 'docker_host', container_mode: true }), position: { x: 100, y: 100 } }
|
||||||
const container = { ...makeNode('dc1', { type: 'docker_container' }), position: { x: 160, y: 180 } }
|
const container = { ...makeNode('dc1', { type: 'docker_container' }), position: { x: 160, y: 180 } }
|
||||||
|
|||||||
@@ -289,7 +289,9 @@ export const useCanvasStore = create<CanvasState>((set) => ({
|
|||||||
y: Math.max(10, node.position.y - parent.position.y),
|
y: Math.max(10, node.position.y - parent.position.y),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: node
|
// Not nesting: strip any parentId/extent a caller may have set so a
|
||||||
|
// non-container parent can't trap the node in its bounding box.
|
||||||
|
: { ...node, parentId: undefined, extent: undefined }
|
||||||
// Parents must come before children in the array (React Flow requirement)
|
// Parents must come before children in the array (React Flow requirement)
|
||||||
const withoutNew = state.nodes.filter((n) => n.id !== node.id)
|
const withoutNew = state.nodes.filter((n) => n.id !== node.id)
|
||||||
if (enriched.parentId) {
|
if (enriched.parentId) {
|
||||||
|
|||||||
Reference in New Issue
Block a user