@@ -6,6 +6,61 @@ import { getBlock } from '@/blocks/registry'
66
77const logger = createLogger ( 'NodeUtilities' )
88
9+ /**
10+ * Estimates block dimensions based on block type.
11+ * Uses subblock count to estimate height for blocks that haven't been measured yet.
12+ *
13+ * @param blockType - The type of block (e.g., 'condition', 'agent')
14+ * @returns Estimated width and height for the block
15+ */
16+ export function estimateBlockDimensions ( blockType : string ) : { width : number ; height : number } {
17+ const blockConfig = getBlock ( blockType )
18+ const subBlockCount = blockConfig ?. subBlocks ?. length ?? 3
19+ // Many subblocks are conditionally rendered (advanced mode, provider-specific, etc.)
20+ // Use roughly half the config count as a reasonable estimate, capped between 3-7 rows
21+ const estimatedRows = Math . max ( 3 , Math . min ( Math . ceil ( subBlockCount / 2 ) , 7 ) )
22+ const hasErrorRow = blockType !== 'starter' && blockType !== 'response' ? 1 : 0
23+
24+ const height =
25+ BLOCK_DIMENSIONS . HEADER_HEIGHT +
26+ BLOCK_DIMENSIONS . WORKFLOW_CONTENT_PADDING +
27+ ( estimatedRows + hasErrorRow ) * BLOCK_DIMENSIONS . WORKFLOW_ROW_HEIGHT
28+
29+ return {
30+ width : BLOCK_DIMENSIONS . FIXED_WIDTH ,
31+ height : Math . max ( height , BLOCK_DIMENSIONS . MIN_HEIGHT ) ,
32+ }
33+ }
34+
35+ /**
36+ * Clamps a position to keep a block fully inside a container's content area.
37+ * Content area starts after the header and padding, and ends before the right/bottom padding.
38+ *
39+ * @param position - Raw position relative to container origin
40+ * @param containerDimensions - Container width and height
41+ * @param blockDimensions - Block width and height
42+ * @returns Clamped position that keeps block inside content area
43+ */
44+ export function clampPositionToContainer (
45+ position : { x : number ; y : number } ,
46+ containerDimensions : { width : number ; height : number } ,
47+ blockDimensions : { width : number ; height : number }
48+ ) : { x : number ; y : number } {
49+ const { width : containerWidth , height : containerHeight } = containerDimensions
50+ const { width : blockWidth , height : blockHeight } = blockDimensions
51+
52+ // Content area bounds (where blocks can be placed)
53+ const minX = CONTAINER_DIMENSIONS . LEFT_PADDING
54+ const minY = CONTAINER_DIMENSIONS . HEADER_HEIGHT + CONTAINER_DIMENSIONS . TOP_PADDING
55+ const maxX = containerWidth - CONTAINER_DIMENSIONS . RIGHT_PADDING - blockWidth
56+ const maxY = containerHeight - CONTAINER_DIMENSIONS . BOTTOM_PADDING - blockHeight
57+
58+ return {
59+ x : Math . max ( minX , Math . min ( position . x , Math . max ( minX , maxX ) ) ) ,
60+ y : Math . max ( minY , Math . min ( position . y , Math . max ( minY , maxY ) ) ) ,
61+ }
62+ }
63+
964/**
1065 * Hook providing utilities for node position, hierarchy, and dimension calculations
1166 */
@@ -21,7 +76,7 @@ export function useNodeUtilities(blocks: Record<string, any>) {
2176
2277 /**
2378 * Get the dimensions of a block.
24- * For regular blocks, estimates height based on block config if not yet measured .
79+ * For regular blocks, uses stored height or estimates based on block config.
2580 */
2681 const getBlockDimensions = useCallback (
2782 ( blockId : string ) : { width : number ; height : number } => {
@@ -41,32 +96,16 @@ export function useNodeUtilities(blocks: Record<string, any>) {
4196 }
4297 }
4398
44- // Workflow block nodes have fixed visual width
45- const width = BLOCK_DIMENSIONS . FIXED_WIDTH
46-
4799 // Prefer deterministic height published by the block component; fallback to estimate
48- let height = block . height
49-
50- if ( ! height ) {
51- // Estimate height based on block config's subblock count for more accurate initial sizing
52- // This is critical for subflow containers to size correctly before child blocks are measured
53- const blockConfig = getBlock ( block . type )
54- const subBlockCount = blockConfig ?. subBlocks ?. length ?? 3
55- // Many subblocks are conditionally rendered (advanced mode, provider-specific, etc.)
56- // Use roughly half the config count as a reasonable estimate, capped between 3-7 rows
57- const estimatedRows = Math . max ( 3 , Math . min ( Math . ceil ( subBlockCount / 2 ) , 7 ) )
58- const hasErrorRow = block . type !== 'starter' && block . type !== 'response' ? 1 : 0
59-
60- height =
61- BLOCK_DIMENSIONS . HEADER_HEIGHT +
62- BLOCK_DIMENSIONS . WORKFLOW_CONTENT_PADDING +
63- ( estimatedRows + hasErrorRow ) * BLOCK_DIMENSIONS . WORKFLOW_ROW_HEIGHT
100+ if ( block . height ) {
101+ return {
102+ width : BLOCK_DIMENSIONS . FIXED_WIDTH ,
103+ height : Math . max ( block . height , BLOCK_DIMENSIONS . MIN_HEIGHT ) ,
104+ }
64105 }
65106
66- return {
67- width,
68- height : Math . max ( height , BLOCK_DIMENSIONS . MIN_HEIGHT ) ,
69- }
107+ // Use shared estimation utility for blocks without measured height
108+ return estimateBlockDimensions ( block . type )
70109 } ,
71110 [ blocks , isContainerType ]
72111 )
@@ -164,29 +203,36 @@ export function useNodeUtilities(blocks: Record<string, any>) {
164203 )
165204
166205 /**
167- * Calculates the relative position of a node to a new parent's content area.
168- * Accounts for header height and padding offsets in container nodes.
206+ * Calculates the relative position of a node to a new parent's origin.
207+ * React Flow positions children relative to parent origin, so we clamp
208+ * to the content area bounds (after header and padding).
169209 * @param nodeId ID of the node being repositioned
170210 * @param newParentId ID of the new parent
171- * @returns Relative position coordinates {x, y} within the parent's content area
211+ * @returns Relative position coordinates {x, y} within the parent
172212 */
173213 const calculateRelativePosition = useCallback (
174214 ( nodeId : string , newParentId : string ) : { x : number ; y : number } => {
175215 const nodeAbsPos = getNodeAbsolutePosition ( nodeId )
176216 const parentAbsPos = getNodeAbsolutePosition ( newParentId )
217+ const parentNode = getNodes ( ) . find ( ( n ) => n . id === newParentId )
177218
178- // Account for container's header and padding
179- // Children are positioned relative to content area, not container origin
180- const headerHeight = 50
181- const leftPadding = 16
182- const topPadding = 16
219+ // Calculate raw relative position (relative to parent origin)
220+ const rawPosition = {
221+ x : nodeAbsPos . x - parentAbsPos . x ,
222+ y : nodeAbsPos . y - parentAbsPos . y ,
223+ }
183224
184- return {
185- x : nodeAbsPos . x - parentAbsPos . x - leftPadding ,
186- y : nodeAbsPos . y - parentAbsPos . y - headerHeight - topPadding ,
225+ // Get container and block dimensions
226+ const containerDimensions = {
227+ width : parentNode ?. data ?. width || CONTAINER_DIMENSIONS . DEFAULT_WIDTH ,
228+ height : parentNode ?. data ?. height || CONTAINER_DIMENSIONS . DEFAULT_HEIGHT ,
187229 }
230+ const blockDimensions = getBlockDimensions ( nodeId )
231+
232+ // Clamp position to keep block inside content area
233+ return clampPositionToContainer ( rawPosition , containerDimensions , blockDimensions )
188234 } ,
189- [ getNodeAbsolutePosition ]
235+ [ getNodeAbsolutePosition , getNodes , getBlockDimensions ]
190236 )
191237
192238 /**
@@ -252,7 +298,11 @@ export function useNodeUtilities(blocks: Record<string, any>) {
252298 */
253299 const calculateLoopDimensions = useCallback (
254300 ( nodeId : string ) : { width : number ; height : number } => {
255- const childNodes = getNodes ( ) . filter ( ( node ) => node . parentId === nodeId )
301+ // Check both React Flow's node.parentId AND blocks store's data.parentId
302+ // This ensures we catch children even if React Flow hasn't re-rendered yet
303+ const childNodes = getNodes ( ) . filter (
304+ ( node ) => node . parentId === nodeId || blocks [ node . id ] ?. data ?. parentId === nodeId
305+ )
256306 if ( childNodes . length === 0 ) {
257307 return {
258308 width : CONTAINER_DIMENSIONS . DEFAULT_WIDTH ,
@@ -265,8 +315,11 @@ export function useNodeUtilities(blocks: Record<string, any>) {
265315
266316 childNodes . forEach ( ( node ) => {
267317 const { width : nodeWidth , height : nodeHeight } = getBlockDimensions ( node . id )
268- maxRight = Math . max ( maxRight , node . position . x + nodeWidth )
269- maxBottom = Math . max ( maxBottom , node . position . y + nodeHeight )
318+ // Use block position from store if available (more up-to-date)
319+ const block = blocks [ node . id ]
320+ const position = block ?. position || node . position
321+ maxRight = Math . max ( maxRight , position . x + nodeWidth )
322+ maxBottom = Math . max ( maxBottom , position . y + nodeHeight )
270323 } )
271324
272325 const width = Math . max (
@@ -283,7 +336,7 @@ export function useNodeUtilities(blocks: Record<string, any>) {
283336
284337 return { width, height }
285338 } ,
286- [ getNodes , getBlockDimensions ]
339+ [ getNodes , getBlockDimensions , blocks ]
287340 )
288341
289342 /**
0 commit comments