@@ -222,6 +222,12 @@ interface BlockData {
222222 position : { x : number ; y : number }
223223}
224224
225+ interface PendingConnectionContext {
226+ sourceNodeId : string
227+ sourceHandleId ?: string
228+ dropPosition : { x : number ; y : number }
229+ }
230+
225231/**
226232 * Main workflow canvas content component.
227233 * Renders the ReactFlow canvas with blocks, edges, and all interactive features.
@@ -254,6 +260,7 @@ const WorkflowContent = React.memo(() => {
254260 const workflowIdParam = params . workflowId as string
255261
256262 const addNotification = useNotificationStore ( ( state ) => state . addNotification )
263+ const isSearchModalOpen = useSearchModalStore ( ( state ) => state . isOpen )
257264
258265 useEffect ( ( ) => {
259266 const OAUTH_CONNECT_PENDING_KEY = 'sim.oauth-connect-pending'
@@ -506,11 +513,36 @@ const WorkflowContent = React.memo(() => {
506513 )
507514
508515 /** Stores source node/handle info when a connection drag starts for drop-on-block detection. */
509- const connectionSourceRef = useRef < { nodeId : string ; handleId : string } | null > ( null )
516+ const connectionSourceRef = useRef < { nodeId : string ; handleId ? : string } | null > ( null )
510517
511518 /** Tracks whether onConnect successfully handled the connection (ReactFlow pattern). */
512519 const connectionCompletedRef = useRef ( false )
513520
521+ /** Stores pending context for edge-drag-to-create flow until a block is selected. */
522+ const pendingConnectionContextRef = useRef < PendingConnectionContext | null > ( null )
523+
524+ const setPendingConnectionContext = useCallback ( ( context : PendingConnectionContext ) => {
525+ pendingConnectionContextRef . current = context
526+ } , [ ] )
527+
528+ const clearPendingConnectionContext = useCallback ( ( ) => {
529+ pendingConnectionContextRef . current = null
530+ } , [ ] )
531+
532+ const consumePendingConnectionContext = useCallback ( ( ) => {
533+ const context = pendingConnectionContextRef . current
534+ pendingConnectionContextRef . current = null
535+ return context
536+ } , [ ] )
537+
538+ const wasSearchModalOpenRef = useRef ( isSearchModalOpen )
539+ useEffect ( ( ) => {
540+ if ( wasSearchModalOpenRef . current && ! isSearchModalOpen ) {
541+ clearPendingConnectionContext ( )
542+ }
543+ wasSearchModalOpenRef . current = isSearchModalOpen
544+ } , [ isSearchModalOpen , clearPendingConnectionContext ] )
545+
514546 /** Stores start positions for multi-node drag undo/redo recording. */
515547 const multiNodeDragStartRef = useRef < Map < string , { x : number ; y : number ; parentId ?: string } > > (
516548 new Map ( )
@@ -1444,6 +1476,69 @@ const WorkflowContent = React.memo(() => {
14441476 [ ]
14451477 )
14461478
1479+ /**
1480+ * Creates an explicit edge from a pending edge-drag context to a newly created block.
1481+ * Returns undefined if the connection would violate subflow boundary rules.
1482+ */
1483+ const createPendingConnectionEdge = useCallback (
1484+ (
1485+ pendingConnection : PendingConnectionContext ,
1486+ targetBlockId : string ,
1487+ targetParentId : string | null
1488+ ) : Edge | undefined => {
1489+ const sourceBlock = blocks [ pendingConnection . sourceNodeId ]
1490+ if ( ! sourceBlock ) return undefined
1491+
1492+ const sourceHandle =
1493+ pendingConnection . sourceHandleId ||
1494+ determineSourceHandle ( {
1495+ id : pendingConnection . sourceNodeId ,
1496+ type : sourceBlock . type ,
1497+ } )
1498+
1499+ const sourceParentId =
1500+ sourceBlock . data ?. parentId ||
1501+ ( sourceHandle === 'loop-start-source' || sourceHandle === 'parallel-start-source'
1502+ ? pendingConnection . sourceNodeId
1503+ : undefined )
1504+
1505+ const normalizedTargetParentId = targetParentId || undefined
1506+
1507+ // Mirror onConnect container-boundary rules for edge-drag-to-create flow.
1508+ if (
1509+ ( sourceParentId && ! normalizedTargetParentId ) ||
1510+ ( ! sourceParentId && normalizedTargetParentId ) ||
1511+ ( sourceParentId && normalizedTargetParentId && sourceParentId !== normalizedTargetParentId )
1512+ ) {
1513+ addNotification ( {
1514+ level : 'info' ,
1515+ message : 'Block added, but connection was skipped due to subflow boundary rules.' ,
1516+ workflowId : activeWorkflowId || undefined ,
1517+ } )
1518+ return undefined
1519+ }
1520+
1521+ const isInsideContainer = Boolean ( sourceParentId ) || Boolean ( normalizedTargetParentId )
1522+ const parentId = sourceParentId || normalizedTargetParentId
1523+
1524+ return {
1525+ id : crypto . randomUUID ( ) ,
1526+ source : pendingConnection . sourceNodeId ,
1527+ sourceHandle,
1528+ target : targetBlockId ,
1529+ targetHandle : 'target' ,
1530+ type : 'workflowEdge' ,
1531+ data : isInsideContainer
1532+ ? {
1533+ parentId,
1534+ isInsideContainer,
1535+ }
1536+ : undefined ,
1537+ }
1538+ } ,
1539+ [ blocks , determineSourceHandle , addNotification , activeWorkflowId ]
1540+ )
1541+
14471542 /** Gets the appropriate start handle for a container node (loop or parallel). */
14481543 const getContainerStartHandle = useCallback (
14491544 ( containerId : string ) : string => {
@@ -1600,7 +1695,15 @@ const WorkflowContent = React.memo(() => {
16001695 * @param position - Drop position in ReactFlow coordinates.
16011696 */
16021697 const handleToolbarDrop = useCallback (
1603- ( data : { type : string ; enableTriggerMode ?: boolean } , position : { x : number ; y : number } ) => {
1698+ (
1699+ data : {
1700+ type : string
1701+ enableTriggerMode ?: boolean
1702+ presetOperation ?: string
1703+ pendingConnectionContext ?: PendingConnectionContext
1704+ } ,
1705+ position : { x : number ; y : number }
1706+ ) => {
16041707 if ( ! data . type || data . type === 'connectionBlock' ) return
16051708
16061709 try {
@@ -1614,9 +1717,11 @@ const WorkflowContent = React.memo(() => {
16141717 const baseName = data . type === 'loop' ? 'Loop' : 'Parallel'
16151718 const name = getUniqueBlockName ( baseName , blocks )
16161719
1617- const autoConnectEdge = tryCreateAutoConnectEdge ( position , id , {
1618- targetParentId : null ,
1619- } )
1720+ const autoConnectEdge = data . pendingConnectionContext
1721+ ? createPendingConnectionEdge ( data . pendingConnectionContext , id , null )
1722+ : tryCreateAutoConnectEdge ( position , id , {
1723+ targetParentId : null ,
1724+ } )
16201725
16211726 addBlock (
16221727 id ,
@@ -1684,11 +1789,13 @@ const WorkflowContent = React.memo(() => {
16841789 . filter ( ( b ) => b . data ?. parentId === containerInfo . loopId )
16851790 . map ( ( b ) => ( { id : b . id , type : b . type , position : b . position } ) )
16861791
1687- const autoConnectEdge = tryCreateAutoConnectEdge ( relativePosition , id , {
1688- targetParentId : containerInfo . loopId ,
1689- existingChildBlocks,
1690- containerId : containerInfo . loopId ,
1691- } )
1792+ const autoConnectEdge = data . pendingConnectionContext
1793+ ? createPendingConnectionEdge ( data . pendingConnectionContext , id , containerInfo . loopId )
1794+ : tryCreateAutoConnectEdge ( relativePosition , id , {
1795+ targetParentId : containerInfo . loopId ,
1796+ existingChildBlocks,
1797+ containerId : containerInfo . loopId ,
1798+ } )
16921799
16931800 // Add block with parent info AND autoConnectEdge (atomic operation)
16941801 addBlock (
@@ -1702,7 +1809,9 @@ const WorkflowContent = React.memo(() => {
17021809 } ,
17031810 containerInfo . loopId ,
17041811 'parent' ,
1705- autoConnectEdge
1812+ autoConnectEdge ,
1813+ undefined ,
1814+ data . presetOperation ? { operation : data . presetOperation } : undefined
17061815 )
17071816
17081817 // Resize the container node to fit the new block
@@ -1712,9 +1821,11 @@ const WorkflowContent = React.memo(() => {
17121821 // Centralized trigger constraints
17131822 if ( checkTriggerConstraints ( data . type ) ) return
17141823
1715- const autoConnectEdge = tryCreateAutoConnectEdge ( position , id , {
1716- targetParentId : null ,
1717- } )
1824+ const autoConnectEdge = data . pendingConnectionContext
1825+ ? createPendingConnectionEdge ( data . pendingConnectionContext , id , null )
1826+ : tryCreateAutoConnectEdge ( position , id , {
1827+ targetParentId : null ,
1828+ } )
17181829
17191830 // Regular canvas drop with auto-connect edge
17201831 // Use enableTriggerMode from drag data if present (when dragging from Triggers tab)
@@ -1728,7 +1839,8 @@ const WorkflowContent = React.memo(() => {
17281839 undefined ,
17291840 undefined ,
17301841 autoConnectEdge ,
1731- enableTriggerMode
1842+ enableTriggerMode ,
1843+ data . presetOperation ? { operation : data . presetOperation } : undefined
17321844 )
17331845 }
17341846 } catch ( err ) {
@@ -1743,6 +1855,7 @@ const WorkflowContent = React.memo(() => {
17431855 addNotification ,
17441856 activeWorkflowId ,
17451857 tryCreateAutoConnectEdge ,
1858+ createPendingConnectionEdge ,
17461859 checkTriggerConstraints ,
17471860 ]
17481861 )
@@ -1760,6 +1873,20 @@ const WorkflowContent = React.memo(() => {
17601873 if ( ! type ) return
17611874 if ( type === 'connectionBlock' ) return
17621875
1876+ const pendingConnectionContext = consumePendingConnectionContext ( )
1877+ if ( pendingConnectionContext ) {
1878+ handleToolbarDrop (
1879+ {
1880+ type,
1881+ enableTriggerMode,
1882+ presetOperation,
1883+ pendingConnectionContext,
1884+ } ,
1885+ pendingConnectionContext . dropPosition
1886+ )
1887+ return
1888+ }
1889+
17631890 const basePosition = getViewportCenter ( )
17641891
17651892 if ( type === 'loop' || type === 'parallel' ) {
@@ -1829,6 +1956,8 @@ const WorkflowContent = React.memo(() => {
18291956 )
18301957 }
18311958 } , [
1959+ consumePendingConnectionContext ,
1960+ handleToolbarDrop ,
18321961 getViewportCenter ,
18331962 blocks ,
18341963 addBlock ,
@@ -2762,6 +2891,14 @@ const WorkflowContent = React.memo(() => {
27622891 x : clientPos . clientX ,
27632892 y : clientPos . clientY ,
27642893 } )
2894+ const canvasElement = document . querySelector ( '.workflow-container' ) as HTMLElement | null
2895+ const canvasBounds = canvasElement ?. getBoundingClientRect ( )
2896+ const isDropInsideCanvas = canvasBounds
2897+ ? clientPos . clientX >= canvasBounds . left &&
2898+ clientPos . clientX <= canvasBounds . right &&
2899+ clientPos . clientY >= canvasBounds . top &&
2900+ clientPos . clientY <= canvasBounds . bottom
2901+ : false
27652902
27662903 // Find node under cursor
27672904 const targetNode = findNodeAtPosition ( flowPosition )
@@ -2774,11 +2911,19 @@ const WorkflowContent = React.memo(() => {
27742911 target : targetNode . id ,
27752912 targetHandle : 'target' ,
27762913 } )
2914+ } else if ( isDropInsideCanvas ) {
2915+ // Edge dropped on empty canvas: open block search and remember context.
2916+ setPendingConnectionContext ( {
2917+ sourceNodeId : source . nodeId ,
2918+ sourceHandleId : source . handleId ,
2919+ dropPosition : flowPosition ,
2920+ } )
2921+ useSearchModalStore . getState ( ) . open ( )
27772922 }
27782923
27792924 connectionSourceRef . current = null
27802925 } ,
2781- [ screenToFlowPosition , findNodeAtPosition , onConnect ]
2926+ [ screenToFlowPosition , findNodeAtPosition , onConnect , setPendingConnectionContext ]
27822927 )
27832928
27842929 /** Handles node drag to detect container intersections and update highlighting. */
0 commit comments