fix(canvas): keep container children in place when editing a host node
Editing any field on a container-mode host node (Proxmox/VM/LXC/docker) collapsed all of its nested children into the top-left corner. Root cause: the node modal always includes container_mode in its update payload, and handleUpdateNode gated setProxmoxContainerMode on the field's presence rather than an actual change. So every edit (e.g. changing the icon) re-fired the container ON transition, which re-applied the absolute->relative position conversion to children that were already parent-relative -- piling them up in a corner. - App.tsx: only call setProxmoxContainerMode when container_mode actually changes (compare against the pre-update value). - canvasStore: make setProxmoxContainerMode per-child idempotent so a redundant ON/OFF call can never re-convert an already-nested child's position. - add regression test for the idempotent ON transition.
This commit is contained in:
@@ -459,8 +459,13 @@ export default function App() {
|
||||
snapshotHistory()
|
||||
const existingNode = nodes.find((n) => n.id === editNodeId)
|
||||
updateNode(editNodeId, data)
|
||||
// If container_mode changed, apply structural changes (children parentId, node dimensions)
|
||||
if (typeof data.container_mode === 'boolean') {
|
||||
// Only run the structural container transition when container_mode ACTUALLY
|
||||
// changed. The modal always includes container_mode in its payload, so firing
|
||||
// on presence alone re-ran the absolute<->relative child-position conversion on
|
||||
// every edit (e.g. an icon change), corrupting nested child positions (they pile
|
||||
// up in a corner). Gate on a real toggle instead.
|
||||
const prevContainerMode = !!existingNode?.data.container_mode
|
||||
if (typeof data.container_mode === 'boolean' && data.container_mode !== prevContainerMode) {
|
||||
setProxmoxContainerMode(editNodeId, data.container_mode)
|
||||
}
|
||||
// Sync virtual edge when parent_id changes on an LXC/VM node
|
||||
|
||||
@@ -169,6 +169,23 @@ describe('canvasStore — containers & nesting', () => {
|
||||
expect(updatedChild?.extent).toBeUndefined()
|
||||
})
|
||||
|
||||
// Regression: editing any field on a container host (e.g. its icon) used to
|
||||
// re-fire setProxmoxContainerMode(true) because the modal always re-sends
|
||||
// container_mode. Re-running the ON transition re-applied the
|
||||
// absolute->relative conversion to children that were ALREADY relative,
|
||||
// collapsing them into a corner. A redundant ON call must now be a no-op for
|
||||
// already-nested children.
|
||||
it('setProxmoxContainerMode ON is idempotent for already-nested children', () => {
|
||||
const proxmox: Node<NodeData> = { id: 'px', type: 'proxmox', position: { x: 500, y: -300 }, width: 852, height: 212, data: { label: 'px', type: 'proxmox', status: 'unknown', services: [], container_mode: true } }
|
||||
const child: Node<NodeData> = { id: 'vm1', type: 'vm', position: { x: 10, y: 62 }, data: { label: 'vm1', type: 'vm', status: 'unknown', services: [], parent_id: 'px' }, parentId: 'px', extent: 'parent' }
|
||||
useCanvasStore.setState({ nodes: [proxmox, child] })
|
||||
useCanvasStore.getState().setProxmoxContainerMode('px', true)
|
||||
const c = useCanvasStore.getState().nodes.find((n) => n.id === 'vm1')
|
||||
expect(c?.position).toEqual({ x: 10, y: 62 })
|
||||
expect(c?.parentId).toBe('px')
|
||||
expect(c?.extent).toBe('parent')
|
||||
})
|
||||
|
||||
it('loadCanvas sorts parents before children', () => {
|
||||
const parent = makeNode('p1')
|
||||
const child: Node<NodeData> = { ...makeNode('c1', { parent_id: 'p1' }), parentId: 'p1', extent: 'parent' }
|
||||
|
||||
@@ -448,7 +448,14 @@ export const useCanvasStore = create<CanvasState>((set) => ({
|
||||
: { ...withMode, width: undefined, height: undefined }
|
||||
}
|
||||
if (n.data.parent_id === proxmoxId) {
|
||||
// Idempotency guard: only convert a child's position when its nesting
|
||||
// state actually changes. A child that already matches the target mode
|
||||
// keeps its position untouched -- re-running the absolute<->relative
|
||||
// conversion on an already-relative child corrupts it (children pile
|
||||
// into a corner). See handleUpdateNode in App.tsx.
|
||||
const alreadyNested = n.parentId === proxmoxId && n.extent === 'parent'
|
||||
if (enabled && parentNode) {
|
||||
if (alreadyNested) return n
|
||||
return {
|
||||
...n,
|
||||
parentId: proxmoxId,
|
||||
@@ -460,6 +467,7 @@ export const useCanvasStore = create<CanvasState>((set) => ({
|
||||
}
|
||||
}
|
||||
if (!enabled && parentNode) {
|
||||
if (!n.parentId) return n
|
||||
return {
|
||||
...n,
|
||||
parentId: undefined,
|
||||
|
||||
Reference in New Issue
Block a user