@@ -2286,9 +2286,6 @@ const WorkflowContent = React.memo(() => {
22862286 // Only consider container nodes that aren't the dragged node
22872287 if ( n . type !== 'subflowNode' || n . id === node . id ) return false
22882288
2289- // Skip if this container is already the parent of the node being dragged
2290- if ( n . id === currentParentId ) return false
2291-
22922289 // Get the container's absolute position
22932290 const containerAbsolutePos = getNodeAbsolutePosition ( n . id )
22942291
@@ -2439,32 +2436,55 @@ const WorkflowContent = React.memo(() => {
24392436 previousPositions : multiNodeDragStartRef . current ,
24402437 } )
24412438
2442- // Process parent updates for all selected nodes if dropping into a subflow
2443- if ( potentialParentId && potentialParentId !== dragStartParentId ) {
2444- // Filter out nodes that cannot be moved into subflows
2445- const validNodes = selectedNodes . filter ( ( n ) => {
2446- const block = blocks [ n . id ]
2447- if ( ! block ) return false
2448- // Starter blocks cannot be in containers
2449- if ( n . data ?. type === 'starter' ) return false
2450- // Trigger blocks cannot be in containers
2451- if ( TriggerUtils . isTriggerBlock ( block ) ) return false
2452- // Subflow nodes (loop/parallel) cannot be nested
2453- if ( n . type === 'subflowNode' ) return false
2439+ // Process parent updates for nodes whose parent is changing
2440+ // Check each node individually - don't rely on dragStartParentId since
2441+ // multi-node selections can contain nodes from different parents
2442+ const selectedNodeIds = new Set ( selectedNodes . map ( ( n ) => n . id ) )
2443+ const nodesNeedingParentUpdate = selectedNodes . filter ( ( n ) => {
2444+ const block = blocks [ n . id ]
2445+ if ( ! block ) return false
2446+ const currentParent = block . data ?. parentId || null
2447+ // Skip if the node's parent is also being moved (keep children with their parent)
2448+ if ( currentParent && selectedNodeIds . has ( currentParent ) ) return false
2449+ // Node needs update if current parent !== target parent
2450+ return currentParent !== potentialParentId
2451+ } )
2452+
2453+ if ( nodesNeedingParentUpdate . length > 0 ) {
2454+ // Filter out nodes that cannot be moved into subflows (when target is a subflow)
2455+ const validNodes = nodesNeedingParentUpdate . filter ( ( n ) => {
2456+ // These restrictions only apply when moving INTO a subflow
2457+ if ( potentialParentId ) {
2458+ if ( n . data ?. type === 'starter' ) return false
2459+ const block = blocks [ n . id ]
2460+ if ( block && TriggerUtils . isTriggerBlock ( block ) ) return false
2461+ if ( n . type === 'subflowNode' ) return false
2462+ }
24542463 return true
24552464 } )
24562465
24572466 if ( validNodes . length > 0 ) {
2467+ // Use boundary edge logic - only remove edges crossing the boundary
2468+ const movingNodeIds = new Set ( validNodes . map ( ( n ) => n . id ) )
2469+ const boundaryEdges = edgesForDisplay . filter ( ( e ) => {
2470+ const sourceInSelection = movingNodeIds . has ( e . source )
2471+ const targetInSelection = movingNodeIds . has ( e . target )
2472+ return sourceInSelection !== targetInSelection
2473+ } )
2474+
24582475 const rawUpdates = validNodes . map ( ( n ) => {
2459- const edgesToRemove = edgesForDisplay . filter (
2476+ const edgesForThisNode = boundaryEdges . filter (
24602477 ( e ) => e . source === n . id || e . target === n . id
24612478 )
2462- const newPosition = calculateRelativePosition ( n . id , potentialParentId , true )
2479+ // Use relative position when moving into a container, absolute position when moving to root
2480+ const newPosition = potentialParentId
2481+ ? calculateRelativePosition ( n . id , potentialParentId , true )
2482+ : getNodeAbsolutePosition ( n . id )
24632483 return {
24642484 blockId : n . id ,
24652485 newParentId : potentialParentId ,
24662486 newPosition,
2467- affectedEdges : edgesToRemove ,
2487+ affectedEdges : edgesForThisNode ,
24682488 }
24692489 } )
24702490
@@ -2494,7 +2514,7 @@ const WorkflowContent = React.memo(() => {
24942514 return {
24952515 ...node ,
24962516 position : update . newPosition ,
2497- parentId : update . newParentId ,
2517+ parentId : update . newParentId ?? undefined ,
24982518 }
24992519 }
25002520 return node
@@ -2503,7 +2523,7 @@ const WorkflowContent = React.memo(() => {
25032523
25042524 resizeLoopNodesWrapper ( )
25052525
2506- logger . info ( 'Batch moved nodes into subflow ' , {
2526+ logger . info ( 'Batch moved nodes to new parent ' , {
25072527 targetParentId : potentialParentId ,
25082528 nodeCount : validNodes . length ,
25092529 } )
@@ -2631,6 +2651,30 @@ const WorkflowContent = React.memo(() => {
26312651 edgesToAdd . forEach ( ( edge ) => addEdge ( edge ) )
26322652
26332653 window . dispatchEvent ( new CustomEvent ( 'skip-edge-recording' , { detail : { skip : false } } ) )
2654+ } else if ( ! potentialParentId && dragStartParentId ) {
2655+ // Moving OUT of a subflow to canvas
2656+ // Remove edges connected to this node since it's leaving its parent
2657+ const edgesToRemove = edgesForDisplay . filter (
2658+ ( e ) => e . source === node . id || e . target === node . id
2659+ )
2660+
2661+ if ( edgesToRemove . length > 0 ) {
2662+ removeEdgesForNode ( node . id , edgesToRemove )
2663+
2664+ logger . info ( 'Removed edges when moving node out of subflow' , {
2665+ blockId : node . id ,
2666+ sourceParentId : dragStartParentId ,
2667+ edgeCount : edgesToRemove . length ,
2668+ } )
2669+ }
2670+
2671+ // Clear the parent relationship
2672+ updateNodeParent ( node . id , null , edgesToRemove )
2673+
2674+ logger . info ( 'Moved node out of subflow' , {
2675+ blockId : node . id ,
2676+ sourceParentId : dragStartParentId ,
2677+ } )
26342678 }
26352679
26362680 // Reset state
@@ -2829,29 +2873,55 @@ const WorkflowContent = React.memo(() => {
28292873 previousPositions : multiNodeDragStartRef . current ,
28302874 } )
28312875
2832- // Process parent updates if dropping into a subflow
2833- if ( potentialParentId && potentialParentId !== dragStartParentId ) {
2834- // Filter out nodes that cannot be moved into subflows
2835- const validNodes = nodes . filter ( ( n : Node ) => {
2836- const block = blocks [ n . id ]
2837- if ( ! block ) return false
2838- if ( n . data ?. type === 'starter' ) return false
2839- if ( TriggerUtils . isTriggerBlock ( block ) ) return false
2840- if ( n . type === 'subflowNode' ) return false
2876+ // Process parent updates for nodes whose parent is changing
2877+ // Check each node individually - don't rely on dragStartParentId since
2878+ // multi-node selections can contain nodes from different parents
2879+ const selectedNodeIds = new Set ( nodes . map ( ( n : Node ) => n . id ) )
2880+ const nodesNeedingParentUpdate = nodes . filter ( ( n : Node ) => {
2881+ const block = blocks [ n . id ]
2882+ if ( ! block ) return false
2883+ const currentParent = block . data ?. parentId || null
2884+ // Skip if the node's parent is also being moved (keep children with their parent)
2885+ if ( currentParent && selectedNodeIds . has ( currentParent ) ) return false
2886+ // Node needs update if current parent !== target parent
2887+ return currentParent !== potentialParentId
2888+ } )
2889+
2890+ if ( nodesNeedingParentUpdate . length > 0 ) {
2891+ // Filter out nodes that cannot be moved into subflows (when target is a subflow)
2892+ const validNodes = nodesNeedingParentUpdate . filter ( ( n : Node ) => {
2893+ // These restrictions only apply when moving INTO a subflow
2894+ if ( potentialParentId ) {
2895+ if ( n . data ?. type === 'starter' ) return false
2896+ const block = blocks [ n . id ]
2897+ if ( block && TriggerUtils . isTriggerBlock ( block ) ) return false
2898+ if ( n . type === 'subflowNode' ) return false
2899+ }
28412900 return true
28422901 } )
28432902
28442903 if ( validNodes . length > 0 ) {
2904+ // Use boundary edge logic - only remove edges crossing the boundary
2905+ const movingNodeIds = new Set ( validNodes . map ( ( n : Node ) => n . id ) )
2906+ const boundaryEdges = edgesForDisplay . filter ( ( e ) => {
2907+ const sourceInSelection = movingNodeIds . has ( e . source )
2908+ const targetInSelection = movingNodeIds . has ( e . target )
2909+ return sourceInSelection !== targetInSelection
2910+ } )
2911+
28452912 const rawUpdates = validNodes . map ( ( n : Node ) => {
2846- const edgesToRemove = edgesForDisplay . filter (
2913+ const edgesForThisNode = boundaryEdges . filter (
28472914 ( e ) => e . source === n . id || e . target === n . id
28482915 )
2849- const newPosition = calculateRelativePosition ( n . id , potentialParentId , true )
2916+ // Use relative position when moving into a container, absolute position when moving to root
2917+ const newPosition = potentialParentId
2918+ ? calculateRelativePosition ( n . id , potentialParentId , true )
2919+ : getNodeAbsolutePosition ( n . id )
28502920 return {
28512921 blockId : n . id ,
28522922 newParentId : potentialParentId ,
28532923 newPosition,
2854- affectedEdges : edgesToRemove ,
2924+ affectedEdges : edgesForThisNode ,
28552925 }
28562926 } )
28572927
@@ -2881,7 +2951,7 @@ const WorkflowContent = React.memo(() => {
28812951 return {
28822952 ...node ,
28832953 position : update . newPosition ,
2884- parentId : update . newParentId ,
2954+ parentId : update . newParentId ?? undefined ,
28852955 }
28862956 }
28872957 return node
@@ -2890,7 +2960,7 @@ const WorkflowContent = React.memo(() => {
28902960
28912961 resizeLoopNodesWrapper ( )
28922962
2893- logger . info ( 'Batch moved selection into subflow ' , {
2963+ logger . info ( 'Batch moved selection to new parent ' , {
28942964 targetParentId : potentialParentId ,
28952965 nodeCount : validNodes . length ,
28962966 } )
@@ -2910,7 +2980,6 @@ const WorkflowContent = React.memo(() => {
29102980 calculateRelativePosition ,
29112981 resizeLoopNodesWrapper ,
29122982 potentialParentId ,
2913- dragStartParentId ,
29142983 edgesForDisplay ,
29152984 clearDragHighlights ,
29162985 ]
@@ -2995,7 +3064,6 @@ const WorkflowContent = React.memo(() => {
29953064
29963065 /** Transforms edges to include selection state and delete handlers. Memoized to prevent re-renders. */
29973066 const edgesWithSelection = useMemo ( ( ) => {
2998- // Build node lookup map once - O(n) instead of O(n) per edge
29993067 const nodeMap = new Map ( displayNodes . map ( ( n ) => [ n . id , n ] ) )
30003068
30013069 return edgesForDisplay . map ( ( edge ) => {
0 commit comments