feat: resizable nodes with width/height persistence
Add NodeResizer to BaseNode so users can drag corners to resize any node. Persist width/height through the full stack: DB model, schemas, canvas save/load route, and migration for existing databases. Add tests covering save, update, clear, and load of node dimensions.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { createElement } from 'react'
|
||||
import { Handle, Position, type NodeProps, type Node } from '@xyflow/react'
|
||||
import { Handle, Position, NodeResizer, type NodeProps, type Node } from '@xyflow/react'
|
||||
import { Cpu, MemoryStick, HardDrive, type LucideIcon } from 'lucide-react'
|
||||
import type { NodeData } from '@/types'
|
||||
import { resolveNodeColors } from '@/utils/nodeColors'
|
||||
@@ -18,7 +18,7 @@ function formatStorage(gb: number): string {
|
||||
return `${gb} GB`
|
||||
}
|
||||
|
||||
export function BaseNode({ data, selected, icon: typeIcon }: BaseNodeProps) {
|
||||
export function BaseNode({ data, selected, icon: typeIcon, width, height }: BaseNodeProps) {
|
||||
const activeTheme = useThemeStore((s) => s.activeTheme)
|
||||
const hideIp = useCanvasStore((s) => s.hideIp)
|
||||
const theme = THEMES[activeTheme]
|
||||
@@ -43,8 +43,17 @@ export function BaseNode({ data, selected, icon: typeIcon }: BaseNodeProps) {
|
||||
: 'none',
|
||||
opacity: data.status === 'offline' ? 0.55 : 1,
|
||||
minWidth: 140,
|
||||
width: width ? '100%' : undefined,
|
||||
height: height ? '100%' : undefined,
|
||||
}}
|
||||
>
|
||||
<NodeResizer
|
||||
isVisible={selected}
|
||||
minWidth={140}
|
||||
minHeight={50}
|
||||
lineStyle={{ borderColor: colors.border, borderWidth: 1 }}
|
||||
handleStyle={{ borderColor: colors.border, background: colors.border, width: 8, height: 8 }}
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Top}
|
||||
@@ -69,7 +78,7 @@ export function BaseNode({ data, selected, icon: typeIcon }: BaseNodeProps) {
|
||||
{/* Label + IP */}
|
||||
<div className="flex flex-col min-w-0">
|
||||
<div
|
||||
className="text-xs font-medium leading-tight truncate max-w-[110px]"
|
||||
className="text-xs font-medium leading-tight truncate"
|
||||
style={{ color: theme.colors.nodeLabelColor }}
|
||||
title={data.label}
|
||||
>
|
||||
|
||||
@@ -328,4 +328,40 @@ describe('canvasStore', () => {
|
||||
useCanvasStore.getState().pasteNodes()
|
||||
expect(useCanvasStore.getState().nodes).toHaveLength(1)
|
||||
})
|
||||
|
||||
// --- Node resizing (width / height) ---
|
||||
|
||||
it('addNode preserves explicit width and height', () => {
|
||||
const node: Node<NodeData> = { ...makeNode('n1'), width: 280, height: 120 }
|
||||
useCanvasStore.getState().addNode(node)
|
||||
const stored = useCanvasStore.getState().nodes.find((n) => n.id === 'n1')
|
||||
expect(stored?.width).toBe(280)
|
||||
expect(stored?.height).toBe(120)
|
||||
})
|
||||
|
||||
it('onNodesChange dimensions change updates width and height', () => {
|
||||
useCanvasStore.getState().addNode(makeNode('n1'))
|
||||
useCanvasStore.getState().markSaved()
|
||||
useCanvasStore.getState().onNodesChange([
|
||||
{ type: 'dimensions', id: 'n1', dimensions: { width: 320, height: 180 }, resizing: true },
|
||||
])
|
||||
const node = useCanvasStore.getState().nodes.find((n) => n.id === 'n1')
|
||||
expect(node?.measured?.width ?? node?.width).toBeDefined()
|
||||
expect(useCanvasStore.getState().hasUnsavedChanges).toBe(true)
|
||||
})
|
||||
|
||||
it('loadCanvas preserves width and height on resized nodes', () => {
|
||||
const resized: Node<NodeData> = { ...makeNode('n1'), width: 300, height: 160 }
|
||||
useCanvasStore.getState().loadCanvas([resized], [])
|
||||
const stored = useCanvasStore.getState().nodes.find((n) => n.id === 'n1')
|
||||
expect(stored?.width).toBe(300)
|
||||
expect(stored?.height).toBe(160)
|
||||
})
|
||||
|
||||
it('loadCanvas preserves undefined width/height for default-sized nodes', () => {
|
||||
useCanvasStore.getState().loadCanvas([makeNode('n1')], [])
|
||||
const stored = useCanvasStore.getState().nodes.find((n) => n.id === 'n1')
|
||||
expect(stored?.width).toBeUndefined()
|
||||
expect(stored?.height).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user