@@ -2273,9 +2273,6 @@ const WorkflowContent = React.memo(() => {
22732273 // Only consider container nodes that aren't the dragged node
22742274 if ( n . type !== 'subflowNode' || n . id === node . id ) return false
22752275
2276- // Skip if this container is already the parent of the node being dragged
2277- if ( n . id === currentParentId ) return false
2278-
22792276 // Get the container's absolute position
22802277 const containerAbsolutePos = getNodeAbsolutePosition ( n . id )
22812278
@@ -2426,37 +2423,56 @@ const WorkflowContent = React.memo(() => {
24262423 previousPositions : multiNodeDragStartRef . current ,
24272424 } )
24282425
2429- // Process parent updates for all selected nodes if dropping into a subflow
2430- if ( potentialParentId && potentialParentId !== dragStartParentId ) {
2431- // Filter out nodes that cannot be moved into subflows
2432- const validNodes = selectedNodes . filter ( ( n ) => {
2433- const block = blocks [ n . id ]
2434- if ( ! block ) return false
2435- // Starter blocks cannot be in containers
2436- if ( n . data ?. type === 'starter' ) return false
2437- // Trigger blocks cannot be in containers
2438- if ( TriggerUtils . isTriggerBlock ( block ) ) return false
2439- // Subflow nodes (loop/parallel) cannot be nested
2440- if ( n . type === 'subflowNode' ) return false
2426+ // Process parent updates for nodes whose parent is changing
2427+ // Check each node individually - don't rely on dragStartParentId since
2428+ // multi-node selections can contain nodes from different parents
2429+ const selectedNodeIds = new Set ( selectedNodes . map ( ( n ) => n . id ) )
2430+ const nodesNeedingParentUpdate = selectedNodes . filter ( ( n ) => {
2431+ const block = blocks [ n . id ]
2432+ if ( ! block ) return false
2433+ const currentParent = block . data ?. parentId || null
2434+ // Skip if the node's parent is also being moved (keep children with their parent)
2435+ if ( currentParent && selectedNodeIds . has ( currentParent ) ) return false
2436+ // Node needs update if current parent !== target parent
2437+ return currentParent !== potentialParentId
2438+ } )
2439+
2440+ if ( nodesNeedingParentUpdate . length > 0 ) {
2441+ // Filter out nodes that cannot be moved into subflows (when target is a subflow)
2442+ const validNodes = nodesNeedingParentUpdate . filter ( ( n ) => {
2443+ // These restrictions only apply when moving INTO a subflow
2444+ if ( potentialParentId ) {
2445+ if ( n . data ?. type === 'starter' ) return false
2446+ const block = blocks [ n . id ]
2447+ if ( block && TriggerUtils . isTriggerBlock ( block ) ) return false
2448+ if ( n . type === 'subflowNode' ) return false
2449+ }
24412450 return true
24422451 } )
24432452
24442453 if ( validNodes . length > 0 ) {
2445- // Build updates for all valid nodes
2454+ // Use boundary edge logic - only remove edges crossing the boundary
2455+ const movingNodeIds = new Set ( validNodes . map ( ( n ) => n . id ) )
2456+ const boundaryEdges = edgesForDisplay . filter ( ( e ) => {
2457+ const sourceInSelection = movingNodeIds . has ( e . source )
2458+ const targetInSelection = movingNodeIds . has ( e . target )
2459+ return sourceInSelection !== targetInSelection
2460+ } )
2461+
24462462 const updates = validNodes . map ( ( n ) => {
2447- const edgesToRemove = edgesForDisplay . filter (
2463+ const edgesForThisNode = boundaryEdges . filter (
24482464 ( e ) => e . source === n . id || e . target === n . id
24492465 )
24502466 return {
24512467 blockId : n . id ,
24522468 newParentId : potentialParentId ,
2453- affectedEdges : edgesToRemove ,
2469+ affectedEdges : edgesForThisNode ,
24542470 }
24552471 } )
24562472
24572473 collaborativeBatchUpdateParent ( updates )
24582474
2459- logger . info ( 'Batch moved nodes into subflow ' , {
2475+ logger . info ( 'Batch moved nodes to new parent ' , {
24602476 targetParentId : potentialParentId ,
24612477 nodeCount : validNodes . length ,
24622478 } )
@@ -2584,6 +2600,30 @@ const WorkflowContent = React.memo(() => {
25842600 edgesToAdd . forEach ( ( edge ) => addEdge ( edge ) )
25852601
25862602 window . dispatchEvent ( new CustomEvent ( 'skip-edge-recording' , { detail : { skip : false } } ) )
2603+ } else if ( ! potentialParentId && dragStartParentId ) {
2604+ // Moving OUT of a subflow to canvas
2605+ // Remove edges connected to this node since it's leaving its parent
2606+ const edgesToRemove = edgesForDisplay . filter (
2607+ ( e ) => e . source === node . id || e . target === node . id
2608+ )
2609+
2610+ if ( edgesToRemove . length > 0 ) {
2611+ removeEdgesForNode ( node . id , edgesToRemove )
2612+
2613+ logger . info ( 'Removed edges when moving node out of subflow' , {
2614+ blockId : node . id ,
2615+ sourceParentId : dragStartParentId ,
2616+ edgeCount : edgesToRemove . length ,
2617+ } )
2618+ }
2619+
2620+ // Clear the parent relationship
2621+ updateNodeParent ( node . id , null , edgesToRemove )
2622+
2623+ logger . info ( 'Moved node out of subflow' , {
2624+ blockId : node . id ,
2625+ sourceParentId : dragStartParentId ,
2626+ } )
25872627 }
25882628
25892629 // Reset state
@@ -2780,33 +2820,56 @@ const WorkflowContent = React.memo(() => {
27802820 previousPositions : multiNodeDragStartRef . current ,
27812821 } )
27822822
2783- // Process parent updates if dropping into a subflow
2784- if ( potentialParentId && potentialParentId !== dragStartParentId ) {
2785- // Filter out nodes that cannot be moved into subflows
2786- const validNodes = nodes . filter ( ( n : Node ) => {
2787- const block = blocks [ n . id ]
2788- if ( ! block ) return false
2789- if ( n . data ?. type === 'starter' ) return false
2790- if ( TriggerUtils . isTriggerBlock ( block ) ) return false
2791- if ( n . type === 'subflowNode' ) return false
2823+ // Process parent updates for nodes whose parent is changing
2824+ // Check each node individually - don't rely on dragStartParentId since
2825+ // multi-node selections can contain nodes from different parents
2826+ const selectedNodeIds = new Set ( nodes . map ( ( n : Node ) => n . id ) )
2827+ const nodesNeedingParentUpdate = nodes . filter ( ( n : Node ) => {
2828+ const block = blocks [ n . id ]
2829+ if ( ! block ) return false
2830+ const currentParent = block . data ?. parentId || null
2831+ // Skip if the node's parent is also being moved (keep children with their parent)
2832+ if ( currentParent && selectedNodeIds . has ( currentParent ) ) return false
2833+ // Node needs update if current parent !== target parent
2834+ return currentParent !== potentialParentId
2835+ } )
2836+
2837+ if ( nodesNeedingParentUpdate . length > 0 ) {
2838+ // Filter out nodes that cannot be moved into subflows (when target is a subflow)
2839+ const validNodes = nodesNeedingParentUpdate . filter ( ( n : Node ) => {
2840+ // These restrictions only apply when moving INTO a subflow
2841+ if ( potentialParentId ) {
2842+ if ( n . data ?. type === 'starter' ) return false
2843+ const block = blocks [ n . id ]
2844+ if ( block && TriggerUtils . isTriggerBlock ( block ) ) return false
2845+ if ( n . type === 'subflowNode' ) return false
2846+ }
27922847 return true
27932848 } )
27942849
27952850 if ( validNodes . length > 0 ) {
2851+ // Use boundary edge logic - only remove edges crossing the boundary
2852+ const movingNodeIds = new Set ( validNodes . map ( ( n : Node ) => n . id ) )
2853+ const boundaryEdges = edgesForDisplay . filter ( ( e ) => {
2854+ const sourceInSelection = movingNodeIds . has ( e . source )
2855+ const targetInSelection = movingNodeIds . has ( e . target )
2856+ return sourceInSelection !== targetInSelection
2857+ } )
2858+
27962859 const updates = validNodes . map ( ( n : Node ) => {
2797- const edgesToRemove = edgesForDisplay . filter (
2860+ const edgesForThisNode = boundaryEdges . filter (
27982861 ( e ) => e . source === n . id || e . target === n . id
27992862 )
28002863 return {
28012864 blockId : n . id ,
28022865 newParentId : potentialParentId ,
2803- affectedEdges : edgesToRemove ,
2866+ affectedEdges : edgesForThisNode ,
28042867 }
28052868 } )
28062869
28072870 collaborativeBatchUpdateParent ( updates )
28082871
2809- logger . info ( 'Batch moved selection into subflow ' , {
2872+ logger . info ( 'Batch moved selection to new parent ' , {
28102873 targetParentId : potentialParentId ,
28112874 nodeCount : validNodes . length ,
28122875 } )
@@ -2824,7 +2887,6 @@ const WorkflowContent = React.memo(() => {
28242887 collaborativeBatchUpdatePositions ,
28252888 collaborativeBatchUpdateParent ,
28262889 potentialParentId ,
2827- dragStartParentId ,
28282890 edgesForDisplay ,
28292891 clearDragHighlights ,
28302892 ]
@@ -2909,7 +2971,6 @@ const WorkflowContent = React.memo(() => {
29092971
29102972 /** Transforms edges to include selection state and delete handlers. Memoized to prevent re-renders. */
29112973 const edgesWithSelection = useMemo ( ( ) => {
2912- // Build node lookup map once - O(n) instead of O(n) per edge
29132974 const nodeMap = new Map ( displayNodes . map ( ( n ) => [ n . id , n ] ) )
29142975
29152976 return edgesForDisplay . map ( ( edge ) => {
0 commit comments