@@ -281,6 +281,41 @@ const WorkflowContent = React.memo(() => {
281281 [ getNodes ]
282282 )
283283
284+ // Compute the absolute position of a node's source anchor (right-middle)
285+ const getNodeAnchorPosition = useCallback (
286+ ( nodeId : string ) : { x : number ; y : number } => {
287+ const node = getNodes ( ) . find ( ( n ) => n . id === nodeId )
288+ const absPos = getNodeAbsolutePositionWrapper ( nodeId )
289+
290+ if ( ! node ) {
291+ return absPos
292+ }
293+
294+ // Use known defaults per node type without type casting
295+ const isSubflow = node . type === 'subflowNode'
296+ const width = isSubflow
297+ ? typeof node . data ?. width === 'number'
298+ ? node . data . width
299+ : 500
300+ : typeof node . width === 'number'
301+ ? node . width
302+ : 350
303+ const height = isSubflow
304+ ? typeof node . data ?. height === 'number'
305+ ? node . data . height
306+ : 300
307+ : typeof node . height === 'number'
308+ ? node . height
309+ : 100
310+
311+ return {
312+ x : absPos . x + width ,
313+ y : absPos . y + height / 2 ,
314+ }
315+ } ,
316+ [ getNodes , getNodeAbsolutePositionWrapper ]
317+ )
318+
284319 // Auto-layout handler - now uses frontend auto layout for immediate updates
285320 const handleAutoLayout = useCallback ( async ( ) => {
286321 if ( Object . keys ( blocks ) . length === 0 ) return
@@ -373,22 +408,37 @@ const WorkflowContent = React.memo(() => {
373408 // Handle drops
374409 const findClosestOutput = useCallback (
375410 ( newNodePosition : { x : number ; y : number } ) : BlockData | null => {
376- const existingBlocks = Object . entries ( blocks )
377- . filter ( ( [ _ , block ] ) => block . enabled )
378- . map ( ( [ id , block ] ) => ( {
379- id,
380- type : block . type ,
381- position : block . position ,
382- distance : Math . sqrt (
383- ( block . position . x - newNodePosition . x ) ** 2 +
384- ( block . position . y - newNodePosition . y ) ** 2
385- ) ,
386- } ) )
411+ // Determine if drop is inside a container; if not, exclude child nodes from candidates
412+ const containerAtPoint = isPointInLoopNodeWrapper ( newNodePosition )
413+ const nodeIndex = new Map ( getNodes ( ) . map ( ( n ) => [ n . id , n ] ) )
414+
415+ const candidates = Object . entries ( blocks )
416+ . filter ( ( [ id , block ] ) => {
417+ if ( ! block . enabled ) return false
418+ const node = nodeIndex . get ( id )
419+ if ( ! node ) return false
420+
421+ // If dropping outside containers, ignore blocks that are inside a container
422+ if ( ! containerAtPoint && node . parentId ) return false
423+ return true
424+ } )
425+ . map ( ( [ id , block ] ) => {
426+ const anchor = getNodeAnchorPosition ( id )
427+ const distance = Math . sqrt (
428+ ( anchor . x - newNodePosition . x ) ** 2 + ( anchor . y - newNodePosition . y ) ** 2
429+ )
430+ return {
431+ id,
432+ type : block . type ,
433+ position : anchor ,
434+ distance,
435+ }
436+ } )
387437 . sort ( ( a , b ) => a . distance - b . distance )
388438
389- return existingBlocks [ 0 ] || null
439+ return candidates [ 0 ] || null
390440 } ,
391- [ blocks ]
441+ [ blocks , getNodes , getNodeAnchorPosition , isPointInLoopNodeWrapper ]
392442 )
393443
394444 // Determine the appropriate source handle based on block type
@@ -1385,8 +1435,69 @@ const WorkflowContent = React.memo(() => {
13851435
13861436 // Update the node's parent relationship
13871437 if ( potentialParentId ) {
1438+ // Compute relative position BEFORE updating parent to avoid stale state
1439+ const containerAbsPosBefore = getNodeAbsolutePositionWrapper ( potentialParentId )
1440+ const nodeAbsPosBefore = getNodeAbsolutePositionWrapper ( node . id )
1441+ const relativePositionBefore = {
1442+ x : nodeAbsPosBefore . x - containerAbsPosBefore . x ,
1443+ y : nodeAbsPosBefore . y - containerAbsPosBefore . y ,
1444+ }
1445+
13881446 // Moving to a new parent container
13891447 updateNodeParent ( node . id , potentialParentId )
1448+
1449+ // Auto-connect when moving an existing block into a container
1450+ const isAutoConnectEnabled = useGeneralStore . getState ( ) . isAutoConnectEnabled
1451+ if ( isAutoConnectEnabled ) {
1452+ // Existing children in the target container (excluding the moved node)
1453+ const existingChildBlocks = Object . values ( blocks ) . filter (
1454+ ( b ) => b . data ?. parentId === potentialParentId && b . id !== node . id
1455+ )
1456+
1457+ if ( existingChildBlocks . length > 0 ) {
1458+ // Connect from nearest existing child inside the container
1459+ const closestBlock = existingChildBlocks
1460+ . map ( ( b ) => ( {
1461+ block : b ,
1462+ distance : Math . sqrt (
1463+ ( b . position . x - relativePositionBefore . x ) ** 2 +
1464+ ( b . position . y - relativePositionBefore . y ) ** 2
1465+ ) ,
1466+ } ) )
1467+ . sort ( ( a , b ) => a . distance - b . distance ) [ 0 ] ?. block
1468+
1469+ if ( closestBlock ) {
1470+ const sourceHandle = determineSourceHandle ( {
1471+ id : closestBlock . id ,
1472+ type : closestBlock . type ,
1473+ } )
1474+ addEdge ( {
1475+ id : crypto . randomUUID ( ) ,
1476+ source : closestBlock . id ,
1477+ target : node . id ,
1478+ sourceHandle,
1479+ targetHandle : 'target' ,
1480+ type : 'workflowEdge' ,
1481+ } )
1482+ }
1483+ } else {
1484+ // No children: connect from the container's start handle to the moved node
1485+ const containerNode = getNodes ( ) . find ( ( n ) => n . id === potentialParentId )
1486+ const startSourceHandle =
1487+ ( containerNode ?. data as any ) ?. kind === 'loop'
1488+ ? 'loop-start-source'
1489+ : 'parallel-start-source'
1490+
1491+ addEdge ( {
1492+ id : crypto . randomUUID ( ) ,
1493+ source : potentialParentId ,
1494+ target : node . id ,
1495+ sourceHandle : startSourceHandle ,
1496+ targetHandle : 'target' ,
1497+ type : 'workflowEdge' ,
1498+ } )
1499+ }
1500+ }
13901501 }
13911502
13921503 // Reset state
@@ -1400,6 +1511,10 @@ const WorkflowContent = React.memo(() => {
14001511 updateNodeParent ,
14011512 getNodeHierarchyWrapper ,
14021513 collaborativeUpdateBlockPosition ,
1514+ addEdge ,
1515+ determineSourceHandle ,
1516+ blocks ,
1517+ getNodeAbsolutePositionWrapper ,
14031518 ]
14041519 )
14051520
0 commit comments