@@ -47,6 +47,7 @@ import {
4747 computeClampedPositionUpdates ,
4848 getClampedPositionForNode ,
4949 isInEditableElement ,
50+ resolveParentChildSelectionConflicts ,
5051 selectNodesDeferred ,
5152 useAutoLayout ,
5253 useCurrentWorkflow ,
@@ -55,6 +56,7 @@ import {
5556} from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
5657import { useCanvasContextMenu } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-canvas-context-menu'
5758import {
59+ calculateContainerDimensions ,
5860 clampPositionToContainer ,
5961 estimateBlockDimensions ,
6062} from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities'
@@ -697,7 +699,8 @@ const WorkflowContent = React.memo(() => {
697699
698700 selectNodesDeferred (
699701 pastedBlocksArray . map ( ( b ) => b . id ) ,
700- setDisplayNodes
702+ setDisplayNodes ,
703+ blocks
701704 )
702705 } , [
703706 hasClipboard ,
@@ -745,7 +748,8 @@ const WorkflowContent = React.memo(() => {
745748
746749 selectNodesDeferred (
747750 pastedBlocksArray . map ( ( b ) => b . id ) ,
748- setDisplayNodes
751+ setDisplayNodes ,
752+ blocks
749753 )
750754 } , [
751755 contextMenuBlocks ,
@@ -890,7 +894,8 @@ const WorkflowContent = React.memo(() => {
890894
891895 selectNodesDeferred (
892896 pastedBlocks . map ( ( b ) => b . id ) ,
893- setDisplayNodes
897+ setDisplayNodes ,
898+ blocks
894899 )
895900 }
896901 }
@@ -2037,10 +2042,19 @@ const WorkflowContent = React.memo(() => {
20372042 window . removeEventListener ( 'remove-from-subflow' , handleRemoveFromSubflow as EventListener )
20382043 } , [ blocks , edgesForDisplay , getNodeAbsolutePosition , collaborativeBatchUpdateParent ] )
20392044
2040- /** Handles node position changes - updates local state for smooth drag, syncs to store only on drag end. */
2041- const onNodesChange = useCallback ( ( changes : NodeChange [ ] ) => {
2042- setDisplayNodes ( ( nds ) => applyNodeChanges ( changes , nds ) )
2043- } , [ ] )
2045+ /** Handles node changes - applies changes and resolves parent-child selection conflicts. */
2046+ const onNodesChange = useCallback (
2047+ ( changes : NodeChange [ ] ) => {
2048+ setDisplayNodes ( ( nds ) => {
2049+ const updated = applyNodeChanges ( changes , nds )
2050+ const hasSelectionChange = changes . some (
2051+ ( c ) => c . type === 'select' && ( c as { selected ?: boolean } ) . selected
2052+ )
2053+ return hasSelectionChange ? resolveParentChildSelectionConflicts ( updated , blocks ) : updated
2054+ } )
2055+ } ,
2056+ [ blocks ]
2057+ )
20442058
20452059 /**
20462060 * Updates container dimensions in displayNodes during drag.
@@ -2055,28 +2069,13 @@ const WorkflowContent = React.memo(() => {
20552069 const childNodes = currentNodes . filter ( ( n ) => n . parentId === parentId )
20562070 if ( childNodes . length === 0 ) return currentNodes
20572071
2058- let maxRight = 0
2059- let maxBottom = 0
2060-
2061- childNodes . forEach ( ( node ) => {
2072+ const childPositions = childNodes . map ( ( node ) => {
20622073 const nodePosition = node . id === draggedNodeId ? draggedNodePosition : node . position
2063- const { width : nodeWidth , height : nodeHeight } = getBlockDimensions ( node . id )
2064-
2065- maxRight = Math . max ( maxRight , nodePosition . x + nodeWidth )
2066- maxBottom = Math . max ( maxBottom , nodePosition . y + nodeHeight )
2074+ const { width, height } = getBlockDimensions ( node . id )
2075+ return { x : nodePosition . x , y : nodePosition . y , width, height }
20672076 } )
20682077
2069- const newWidth = Math . max (
2070- CONTAINER_DIMENSIONS . DEFAULT_WIDTH ,
2071- CONTAINER_DIMENSIONS . LEFT_PADDING + maxRight + CONTAINER_DIMENSIONS . RIGHT_PADDING
2072- )
2073- const newHeight = Math . max (
2074- CONTAINER_DIMENSIONS . DEFAULT_HEIGHT ,
2075- CONTAINER_DIMENSIONS . HEADER_HEIGHT +
2076- CONTAINER_DIMENSIONS . TOP_PADDING +
2077- maxBottom +
2078- CONTAINER_DIMENSIONS . BOTTOM_PADDING
2079- )
2078+ const { width : newWidth , height : newHeight } = calculateContainerDimensions ( childPositions )
20802079
20812080 return currentNodes . map ( ( node ) => {
20822081 if ( node . id === parentId ) {
@@ -2844,27 +2843,38 @@ const WorkflowContent = React.memo(() => {
28442843 } , [ isShiftPressed ] )
28452844
28462845 const onSelectionEnd = useCallback ( ( ) => {
2847- requestAnimationFrame ( ( ) => setIsSelectionDragActive ( false ) )
2848- } , [ ] )
2846+ requestAnimationFrame ( ( ) => {
2847+ setIsSelectionDragActive ( false )
2848+ setDisplayNodes ( ( nodes ) => resolveParentChildSelectionConflicts ( nodes , blocks ) )
2849+ } )
2850+ } , [ blocks ] )
28492851
28502852 /** Captures initial positions when selection drag starts (for marquee-selected nodes). */
28512853 const onSelectionDragStart = useCallback (
28522854 ( _event : React . MouseEvent , nodes : Node [ ] ) => {
2853- // Capture the parent ID of the first node as reference (they should all be in the same context)
28542855 if ( nodes . length > 0 ) {
28552856 const firstNodeParentId = blocks [ nodes [ 0 ] . id ] ?. data ?. parentId || null
28562857 setDragStartParentId ( firstNodeParentId )
28572858 }
28582859
2859- // Capture all selected nodes' positions for undo/redo
2860+ // Resolve parent-child conflicts and capture positions for undo/redo
2861+ setDisplayNodes ( ( allNodes ) => resolveParentChildSelectionConflicts ( allNodes , blocks ) )
2862+
2863+ // Filter to nodes that won't be deselected (exclude children whose parent is selected)
2864+ const nodeIds = new Set ( nodes . map ( ( n ) => n . id ) )
2865+ const effectiveNodes = nodes . filter ( ( n ) => {
2866+ const parentId = blocks [ n . id ] ?. data ?. parentId
2867+ return ! parentId || ! nodeIds . has ( parentId )
2868+ } )
2869+
28602870 multiNodeDragStartRef . current . clear ( )
2861- nodes . forEach ( ( n ) => {
2862- const block = blocks [ n . id ]
2863- if ( block ) {
2871+ effectiveNodes . forEach ( ( n ) => {
2872+ const blk = blocks [ n . id ]
2873+ if ( blk ) {
28642874 multiNodeDragStartRef . current . set ( n . id , {
28652875 x : n . position . x ,
28662876 y : n . position . y ,
2867- parentId : block . data ?. parentId ,
2877+ parentId : blk . data ?. parentId ,
28682878 } )
28692879 }
28702880 } )
@@ -2903,7 +2913,6 @@ const WorkflowContent = React.memo(() => {
29032913
29042914 eligibleNodes . forEach ( ( node ) => {
29052915 const absolutePos = getNodeAbsolutePosition ( node . id )
2906- const block = blocks [ node . id ]
29072916 const width = BLOCK_DIMENSIONS . FIXED_WIDTH
29082917 const height = Math . max (
29092918 node . height || BLOCK_DIMENSIONS . MIN_HEIGHT ,
@@ -3129,13 +3138,11 @@ const WorkflowContent = React.memo(() => {
31293138
31303139 /**
31313140 * Handles node click to select the node in ReactFlow.
3132- * This ensures clicking anywhere on a block (not just the drag handle)
3133- * selects it for delete/backspace and multi-select operations.
3141+ * Parent-child conflict resolution happens automatically in onNodesChange.
31343142 */
31353143 const handleNodeClick = useCallback (
31363144 ( event : React . MouseEvent , node : Node ) => {
31373145 const isMultiSelect = event . shiftKey || event . metaKey || event . ctrlKey
3138-
31393146 setNodes ( ( nodes ) =>
31403147 nodes . map ( ( n ) => ( {
31413148 ...n ,
0 commit comments