fix: persist manual node sizes and add width/height inputs

Nested vm/lxc/docker leaf nodes lost their size on reload: the
deserialize restore branch excluded those types entirely, so a
resized child snapped back to content-fit. Gate the restore on
container_mode instead of node type.

Also prefer explicit width/height over the DOM-measured value when
serializing, so a manual resize persists its exact target rather than
drifting to the fractional content-fit size.

Add a Size section to the detail panel with W/H number inputs
(setNodeSize store action, clamped to the resizer minimums). Inputs
resync live when the node is resized by corner drag, without
clobbering active keystrokes.

ha-relevant: yes
This commit is contained in:
Pouzor
2026-06-17 11:55:55 +02:00
parent 96bb048228
commit 2a4d109ee6
6 changed files with 237 additions and 10 deletions
@@ -1263,4 +1263,29 @@ describe('canvasStore — custom style apply', () => {
expect(e.data?.custom_color).toBe('#aabbcc')
expect(useCanvasStore.getState().hasUnsavedChanges).toBe(true)
})
it('setNodeSize sets explicit width/height and marks unsaved', () => {
useCanvasStore.setState({ nodes: [makeNode('n1')], hasUnsavedChanges: false })
useCanvasStore.getState().setNodeSize('n1', { width: 220, height: 130 })
const n = useCanvasStore.getState().nodes.find((x) => x.id === 'n1')!
expect(n.width).toBe(220)
expect(n.height).toBe(130)
expect(useCanvasStore.getState().hasUnsavedChanges).toBe(true)
})
it('setNodeSize clamps below the minimum box', () => {
useCanvasStore.setState({ nodes: [makeNode('n1')] })
useCanvasStore.getState().setNodeSize('n1', { width: 10, height: 10 })
const n = useCanvasStore.getState().nodes.find((x) => x.id === 'n1')!
expect(n.width).toBe(140)
expect(n.height).toBe(50)
})
it('setNodeSize updates only the provided axis', () => {
useCanvasStore.setState({ nodes: [{ ...makeNode('n1'), width: 200, height: 100 }] })
useCanvasStore.getState().setNodeSize('n1', { width: 300 })
const n = useCanvasStore.getState().nodes.find((x) => x.id === 'n1')!
expect(n.width).toBe(300)
expect(n.height).toBe(100)
})
})
+17
View File
@@ -61,6 +61,7 @@ interface CanvasState {
deleteEdge: (id: string) => void
setProxmoxContainerMode: (proxmoxId: string, enabled: boolean) => void
setNodeZIndex: (id: string, zIndex: number) => void
setNodeSize: (id: string, size: { width?: number; height?: number }) => void
editingGroupRectId: string | null
setEditingGroupRectId: (id: string | null) => void
editingTextId: string | null
@@ -466,6 +467,22 @@ export const useCanvasStore = create<CanvasState>((set) => ({
hasUnsavedChanges: true,
})),
// Manual width/height entry. Lets the user type an exact size instead of
// dragging the resize handle (which lands on fractional content-fit pixels).
// A clamp matches the NodeResizer minimums so the box can't collapse.
setNodeSize: (id, size) =>
set((state) => ({
nodes: state.nodes.map((n) => {
if (n.id !== id) return n
return {
...n,
...(size.width != null ? { width: Math.max(140, size.width) } : {}),
...(size.height != null ? { height: Math.max(50, size.height) } : {}),
}
}),
hasUnsavedChanges: true,
})),
setEditingGroupRectId: (id) => set({ editingGroupRectId: id }),
setEditingTextId: (id) => set({ editingTextId: id }),