Skip to content

Commit 40347c4

Browse files
fix(workflow-editor): wire drag-release edge from the actual source handle via handleToolbarDrop
1 parent 2d5ce1e commit 40347c4

1 file changed

Lines changed: 91 additions & 70 deletions

File tree

  • apps/sim/app/workspace/[workspaceId]/w/[workflowId]

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx

Lines changed: 91 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)