diff --git a/frontend/src/stores/__tests__/canvasStore/sizing.test.ts b/frontend/src/stores/__tests__/canvasStore/sizing.test.ts index 7c21529..cf3b996 100644 --- a/frontend/src/stores/__tests__/canvasStore/sizing.test.ts +++ b/frontend/src/stores/__tests__/canvasStore/sizing.test.ts @@ -118,4 +118,53 @@ describe('canvasStore — sizing & z-order', () => { useCanvasStore.getState().updateNode('r1', { properties: [] }) expect(useCanvasStore.getState().nodes.find((n) => n.id === 'r1')?.height).toBe(240) }) + + // Regression (#278): editing any field on a container-mode vm/lxc/docker_host host used to + // reset its manually-set height to undefined, snapping it back to auto-fit size and + // scrambling nested children. Only proxmox was excluded. Keep the manual height for every + // container-mode host type. + it.each(['vm', 'lxc', 'docker_host'] as const)( + 'updateNode preserves manual height for a container-mode %s when properties change', + (type) => { + const host: Node = { + id: 'h1', + type, + position: { x: 0, y: 0 }, + width: 400, + height: 300, + data: { label: type, type, status: 'unknown', services: [], container_mode: true }, + } + useCanvasStore.setState({ nodes: [host], edges: [] }) + useCanvasStore.getState().updateNode('h1', { properties: [], label: 'renamed' }) + expect(useCanvasStore.getState().nodes.find((n) => n.id === 'h1')?.height).toBe(300) + } + ) + + it('updateNode still resets height for a vm NOT in container mode when properties change', () => { + const leaf: Node = { + id: 'v1', + type: 'vm', + position: { x: 0, y: 0 }, + width: 200, + height: 120, + data: { label: 'vm', type: 'vm', status: 'unknown', services: [], container_mode: false }, + } + useCanvasStore.setState({ nodes: [leaf], edges: [] }) + useCanvasStore.getState().updateNode('v1', { properties: [] }) + expect(useCanvasStore.getState().nodes.find((n) => n.id === 'v1')?.height).toBeUndefined() + }) + + it('updateNode preserves height for a proxmox host when properties change', () => { + const px: Node = { + id: 'px1', + type: 'proxmox', + position: { x: 0, y: 0 }, + width: 500, + height: 350, + data: { label: 'px', type: 'proxmox', status: 'unknown', services: [], container_mode: true }, + } + useCanvasStore.setState({ nodes: [px], edges: [] }) + useCanvasStore.getState().updateNode('px1', { properties: [] }) + expect(useCanvasStore.getState().nodes.find((n) => n.id === 'px1')?.height).toBe(350) + }) }) diff --git a/frontend/src/stores/canvasStore.ts b/frontend/src/stores/canvasStore.ts index c3ef3d3..2ad4202 100644 --- a/frontend/src/stores/canvasStore.ts +++ b/frontend/src/stores/canvasStore.ts @@ -13,6 +13,7 @@ import { generateUUID } from '@/utils/uuid' import { normalizeHandle, removedHandleIds, handleCountField, sideDefault, handleId, SIDES } from '@/utils/handleUtils' import { applyOpacity } from '@/utils/colorUtils' import { readHideIp, writeHideIp } from '@/utils/ipDisplay' +import { CONTAINER_MODE_TYPES } from '@/utils/virtualEdgeParent' type HistoryEntry = { nodes: Node[]; edges: Edge[] } type Clipboard = { nodes: Node[]; edges: Edge[] } @@ -325,8 +326,13 @@ export const useCanvasStore = create((set) => ({ let nodes = state.nodes.map((n) => { if (n.id !== id) return n const updated: Node = { ...n, data: { ...n.data, ...data } } - // When properties change, clear stored height so the node auto-sizes to fit new content - if ('properties' in data && n.data.type !== 'proxmox' && n.data.type !== 'groupRect' && n.data.type !== 'group') { + // When properties change, clear stored height so the node auto-sizes to fit new content. + // A container-mode host (vm/lxc/docker_host) keeps a manually-set height: resetting it + // snaps the container back to auto-fit size and scrambles its nested children (#278). + // proxmox is always excluded (legacy behavior); the other container types are excluded + // only while actually in container mode. + const isContainerHost = CONTAINER_MODE_TYPES.has(n.data.type) && !!n.data.container_mode + if ('properties' in data && n.data.type !== 'proxmox' && n.data.type !== 'groupRect' && n.data.type !== 'group' && !isContainerHost) { updated.height = undefined } if ('parent_id' in data) { diff --git a/frontend/src/utils/virtualEdgeParent.ts b/frontend/src/utils/virtualEdgeParent.ts index 9cef306..838fef1 100644 --- a/frontend/src/utils/virtualEdgeParent.ts +++ b/frontend/src/utils/virtualEdgeParent.ts @@ -2,7 +2,7 @@ import type { NodeData } from '@/types' export type NodeType = NodeData['type'] -const CONTAINER_MODE_TYPES = new Set(['proxmox', 'vm', 'lxc', 'docker_host']) +export const CONTAINER_MODE_TYPES = new Set(['proxmox', 'vm', 'lxc', 'docker_host']) const DOCKER_CONTAINER_PARENT_TYPES = new Set(['docker_host', 'lxc', 'vm', 'proxmox']) export interface VirtualEdgeEndpoint {