@@ -1794,7 +1794,12 @@ const WorkflowContent = React.memo(
17941794 */
17951795 const handleToolbarDrop = useCallback (
17961796 (
1797- data : { type : string ; enableTriggerMode ?: boolean ; presetOperation ?: string } ,
1797+ data : {
1798+ type : string
1799+ enableTriggerMode ?: boolean
1800+ presetOperation ?: string
1801+ forcedSource ?: { nodeId : string ; handleId : string }
1802+ } ,
17981803 position : { x : number ; y : number }
17991804 ) => {
18001805 if ( ! data . type || data . type === 'connectionBlock' ) return
@@ -1803,6 +1808,40 @@ const WorkflowContent = React.memo(
18031808 ? { operation : data . presetOperation }
18041809 : undefined
18051810
1811+ const { forcedSource } = data
1812+
1813+ /**
1814+ * Edge for the new block. With a `forcedSource` (a drag-release from a
1815+ * handle), wire from that exact handle — but only when it stays within the
1816+ * resolved container context, matching onConnect's boundary rules; a
1817+ * cross-boundary source yields no edge. Otherwise fall back to normal
1818+ * proximity auto-connect.
1819+ */
1820+ const resolveEdge = (
1821+ targetId : string ,
1822+ targetParentId : string | null ,
1823+ fallback : ( ) => Edge | undefined
1824+ ) : Edge | undefined => {
1825+ if ( ! forcedSource ) return fallback ( )
1826+
1827+ const isContainerStartHandle =
1828+ forcedSource . handleId === 'loop-start-source' ||
1829+ forcedSource . handleId === 'parallel-start-source'
1830+ if ( isContainerStartHandle ) {
1831+ // A container-start handle may only wire to a child of that container.
1832+ return forcedSource . nodeId === targetParentId
1833+ ? createEdgeObject ( forcedSource . nodeId , targetId , forcedSource . handleId )
1834+ : undefined
1835+ }
1836+
1837+ const sourceBlock = blocks [ forcedSource . nodeId ]
1838+ if ( ! sourceBlock ) return undefined
1839+ const sourceParentId = sourceBlock . data ?. parentId ?? null
1840+ return sourceParentId === targetParentId
1841+ ? createEdgeObject ( forcedSource . nodeId , targetId , forcedSource . handleId )
1842+ : undefined
1843+ }
1844+
18061845 try {
18071846 const containerInfo = isPointInLoopNode ( position )
18081847
@@ -1832,11 +1871,13 @@ const WorkflowContent = React.memo(
18321871 . filter ( ( b ) => b . data ?. parentId === containerInfo . loopId )
18331872 . map ( ( b ) => ( { id : b . id , type : b . type , position : b . position } ) )
18341873
1835- const autoConnectEdge = tryCreateAutoConnectEdge ( relativePosition , id , {
1836- targetParentId : containerInfo . loopId ,
1837- existingChildBlocks,
1838- containerId : containerInfo . loopId ,
1839- } )
1874+ const autoConnectEdge = resolveEdge ( id , containerInfo . loopId , ( ) =>
1875+ tryCreateAutoConnectEdge ( relativePosition , id , {
1876+ targetParentId : containerInfo . loopId ,
1877+ existingChildBlocks,
1878+ containerId : containerInfo . loopId ,
1879+ } )
1880+ )
18401881
18411882 addBlock (
18421883 id ,
@@ -1857,9 +1898,11 @@ const WorkflowContent = React.memo(
18571898
18581899 resizeLoopNodesWrapper ( )
18591900 } else {
1860- const autoConnectEdge = tryCreateAutoConnectEdge ( position , id , {
1861- targetParentId : null ,
1862- } )
1901+ const autoConnectEdge = resolveEdge ( id , null , ( ) =>
1902+ tryCreateAutoConnectEdge ( position , id , {
1903+ targetParentId : null ,
1904+ } )
1905+ )
18631906
18641907 addBlock (
18651908 id ,
@@ -1924,11 +1967,13 @@ const WorkflowContent = React.memo(
19241967 . filter ( ( b ) => b . data ?. parentId === containerInfo . loopId )
19251968 . map ( ( b ) => ( { id : b . id , type : b . type , position : b . position } ) )
19261969
1927- const autoConnectEdge = tryCreateAutoConnectEdge ( relativePosition , id , {
1928- targetParentId : containerInfo . loopId ,
1929- existingChildBlocks,
1930- containerId : containerInfo . loopId ,
1931- } )
1970+ const autoConnectEdge = resolveEdge ( id , containerInfo . loopId , ( ) =>
1971+ tryCreateAutoConnectEdge ( relativePosition , id , {
1972+ targetParentId : containerInfo . loopId ,
1973+ existingChildBlocks,
1974+ containerId : containerInfo . loopId ,
1975+ } )
1976+ )
19321977
19331978 // Add block with parent info AND autoConnectEdge (atomic operation)
19341979 addBlock (
@@ -1954,9 +1999,11 @@ const WorkflowContent = React.memo(
19541999 // Centralized trigger constraints
19552000 if ( checkTriggerConstraints ( data . type ) ) return
19562001
1957- const autoConnectEdge = tryCreateAutoConnectEdge ( position , id , {
1958- targetParentId : null ,
1959- } )
2002+ const autoConnectEdge = resolveEdge ( id , null , ( ) =>
2003+ tryCreateAutoConnectEdge ( position , id , {
2004+ targetParentId : null ,
2005+ } )
2006+ )
19602007
19612008 // Regular canvas drop with auto-connect edge
19622009 // Use enableTriggerMode from drag data if present (when dragging from Triggers tab)
@@ -1985,6 +2032,7 @@ const WorkflowContent = React.memo(
19852032 addBlock ,
19862033 tryCreateAutoConnectEdge ,
19872034 checkTriggerConstraints ,
2035+ createEdgeObject ,
19882036 ]
19892037 )
19902038
@@ -2001,65 +2049,38 @@ const WorkflowContent = React.memo(
20012049 if ( typeof type !== 'string' || ! type ) return
20022050 if ( type === 'connectionBlock' ) return
20032051
2004- // Consume a pending drag-release only when THIS event is the palette
2005- // selection it opened, matched by correlation token. Any other
2006- // add-block event (toolbar, sidebar, command list) carries no matching
2007- // token and cannot inherit the pending position/source.
2052+ // Complete a pending drag-release only when THIS event is the palette
2053+ // selection it opened (matched by correlation token). Any other add-block
2054+ // event (toolbar, sidebar, command list) carries no matching token and
2055+ // cannot inherit the pending position/source. Delegating to handleToolbarDrop
2056+ // with the drag source gives container-aware placement AND an edge from the
2057+ // released handle that respects container boundaries, exactly like onConnect.
20082058 const pendingRef = pendingConnectRef . current
2009- const pending =
2010- pendingRef && typeof connectToken === 'string' && connectToken === pendingRef . token
2011- ? pendingRef
2012- : null
2013- if ( pending ) pendingConnectRef . current = null
2014-
2015- let basePosition = getViewportCenter ( )
2016- if ( pending ) {
2017- // screenToFlowPosition already subtracts the pane rect internally — pass
2018- // raw client coords, do NOT pre-subtract container bounds.
2019- basePosition = screenToFlowPosition ( { x : pending . screenX , y : pending . screenY } )
2020-
2021- // Released inside a loop/parallel container: delegate to the container-aware
2022- // drop path for correct parenting, clamping, and in-container auto-connect.
2023- // A forced source→target edge would cross the container boundary, so it is
2024- // intentionally not applied here.
2025- if ( isPointInLoopNode ( basePosition ) ) {
2026- handleToolbarDrop (
2027- {
2028- type,
2029- enableTriggerMode : enableTriggerMode === true ,
2030- presetOperation : typeof presetOperation === 'string' ? presetOperation : undefined ,
2031- } ,
2032- basePosition
2033- )
2034- return
2035- }
2059+ if ( pendingRef && typeof connectToken === 'string' && connectToken === pendingRef . token ) {
2060+ pendingConnectRef . current = null
2061+ // screenToFlowPosition subtracts the pane rect internally — pass raw client coords.
2062+ handleToolbarDrop (
2063+ {
2064+ type,
2065+ enableTriggerMode : enableTriggerMode === true ,
2066+ presetOperation : typeof presetOperation === 'string' ? presetOperation : undefined ,
2067+ forcedSource : pendingRef . source ,
2068+ } ,
2069+ screenToFlowPosition ( { x : pendingRef . screenX , y : pendingRef . screenY } )
2070+ )
2071+ return
20362072 }
20372073
2038- // Force the source→new-block edge only when it stays root-to-root, matching
2039- // the root-level placement — a source inside a container (or a container-start
2040- // handle) would cross the boundary and is rejected, as in onConnect.
2041- const sourceBlock = pending ? blocks [ pending . source . nodeId ] : undefined
2042- const isContainerStartHandle =
2043- pending ?. source . handleId === 'loop-start-source' ||
2044- pending ?. source . handleId === 'parallel-start-source'
2045- const canForceEdge =
2046- Boolean ( sourceBlock ) && ! sourceBlock ?. data ?. parentId && ! isContainerStartHandle
2047-
2048- /**
2049- * Edge for the new block: forced from the drag source when the palette was
2050- * opened by a connection release, otherwise the normal auto-connect edge.
2051- */
2052- const resolveAutoConnectEdge = ( targetId : string ) : Edge | undefined =>
2053- pending && canForceEdge
2054- ? createEdgeObject ( pending . source . nodeId , targetId , pending . source . handleId )
2055- : tryCreateAutoConnectEdge ( basePosition , targetId , { targetParentId : null } )
2074+ const basePosition = getViewportCenter ( )
20562075
20572076 if ( type === 'loop' || type === 'parallel' ) {
20582077 const id = generateId ( )
20592078 const baseName = type === 'loop' ? 'Loop' : 'Parallel'
20602079 const name = getUniqueBlockName ( baseName , blocks )
20612080
2062- const autoConnectEdge = resolveAutoConnectEdge ( id )
2081+ const autoConnectEdge = tryCreateAutoConnectEdge ( basePosition , id , {
2082+ targetParentId : null ,
2083+ } )
20632084
20642085 addBlock (
20652086 id ,
@@ -2092,7 +2113,9 @@ const WorkflowContent = React.memo(
20922113 const baseName = defaultTriggerName || blockConfig . name
20932114 const name = getUniqueBlockName ( baseName , blocks )
20942115
2095- const autoConnectEdge = resolveAutoConnectEdge ( id )
2116+ const autoConnectEdge = tryCreateAutoConnectEdge ( basePosition , id , {
2117+ targetParentId : null ,
2118+ } )
20962119
20972120 addBlock (
20982121 id ,
@@ -2126,9 +2149,7 @@ const WorkflowContent = React.memo(
21262149 checkTriggerConstraints ,
21272150 tryCreateAutoConnectEdge ,
21282151 screenToFlowPosition ,
2129- createEdgeObject ,
21302152 handleToolbarDrop ,
2131- isPointInLoopNode ,
21322153 ] )
21332154
21342155 /**
0 commit comments