fix(canvas): keep container host height when editing an unrelated field
Editing any field on a container-mode vm/lxc/docker_host node (e.g. just renaming it or adding a property) reset its manually-set height to undefined, snapping the container back to auto-fit content size and scrambling/overflowing its nested children. Root cause: updateNode clears height on every 'properties' change and the node modal always includes properties in its payload. Only proxmox was excluded from the reset; vm/lxc/docker_host in container mode were not. - export CONTAINER_MODE_TYPES from virtualEdgeParent (reuse, no re-declare) - updateNode: skip the height reset for any container-mode host type; proxmox stays always-excluded (legacy), plain leaf nodes still auto-fit - regression tests: manual height preserved for container-mode vm/lxc/docker_host and proxmox; still reset for a non-container vm Fixes #278 ha-relevant: yes
This commit is contained in:
@@ -118,4 +118,53 @@ describe('canvasStore — sizing & z-order', () => {
|
|||||||
useCanvasStore.getState().updateNode('r1', { properties: [] })
|
useCanvasStore.getState().updateNode('r1', { properties: [] })
|
||||||
expect(useCanvasStore.getState().nodes.find((n) => n.id === 'r1')?.height).toBe(240)
|
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<NodeData> = {
|
||||||
|
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<NodeData> = {
|
||||||
|
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<NodeData> = {
|
||||||
|
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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { generateUUID } from '@/utils/uuid'
|
|||||||
import { normalizeHandle, removedHandleIds, handleCountField, sideDefault, handleId, SIDES } from '@/utils/handleUtils'
|
import { normalizeHandle, removedHandleIds, handleCountField, sideDefault, handleId, SIDES } from '@/utils/handleUtils'
|
||||||
import { applyOpacity } from '@/utils/colorUtils'
|
import { applyOpacity } from '@/utils/colorUtils'
|
||||||
import { readHideIp, writeHideIp } from '@/utils/ipDisplay'
|
import { readHideIp, writeHideIp } from '@/utils/ipDisplay'
|
||||||
|
import { CONTAINER_MODE_TYPES } from '@/utils/virtualEdgeParent'
|
||||||
|
|
||||||
type HistoryEntry = { nodes: Node<NodeData>[]; edges: Edge<EdgeData>[] }
|
type HistoryEntry = { nodes: Node<NodeData>[]; edges: Edge<EdgeData>[] }
|
||||||
type Clipboard = { nodes: Node<NodeData>[]; edges: Edge<EdgeData>[] }
|
type Clipboard = { nodes: Node<NodeData>[]; edges: Edge<EdgeData>[] }
|
||||||
@@ -325,8 +326,13 @@ export const useCanvasStore = create<CanvasState>((set) => ({
|
|||||||
let nodes = state.nodes.map((n) => {
|
let nodes = state.nodes.map((n) => {
|
||||||
if (n.id !== id) return n
|
if (n.id !== id) return n
|
||||||
const updated: Node<NodeData> = { ...n, data: { ...n.data, ...data } }
|
const updated: Node<NodeData> = { ...n, data: { ...n.data, ...data } }
|
||||||
// When properties change, clear stored height so the node auto-sizes to fit new content
|
// 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') {
|
// 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
|
updated.height = undefined
|
||||||
}
|
}
|
||||||
if ('parent_id' in data) {
|
if ('parent_id' in data) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { NodeData } from '@/types'
|
|||||||
|
|
||||||
export type NodeType = NodeData['type']
|
export type NodeType = NodeData['type']
|
||||||
|
|
||||||
const CONTAINER_MODE_TYPES = new Set<NodeType>(['proxmox', 'vm', 'lxc', 'docker_host'])
|
export const CONTAINER_MODE_TYPES = new Set<NodeType>(['proxmox', 'vm', 'lxc', 'docker_host'])
|
||||||
const DOCKER_CONTAINER_PARENT_TYPES = new Set<NodeType>(['docker_host', 'lxc', 'vm', 'proxmox'])
|
const DOCKER_CONTAINER_PARENT_TYPES = new Set<NodeType>(['docker_host', 'lxc', 'vm', 'proxmox'])
|
||||||
|
|
||||||
export interface VirtualEdgeEndpoint {
|
export interface VirtualEdgeEndpoint {
|
||||||
|
|||||||
Reference in New Issue
Block a user