Skip to content

Commit fca5f10

Browse files
feat(workflow-editor): open block palette on edge drag-release with auto-connect (#5586)
* feat(workflow-editor): open block palette on edge drag-release with auto-connect * fix(workflow-editor): correct drag-release drop coords, scoping, and container placement * fix(workflow-editor): correlate drag-release palette selection with a token * fix(workflow-editor): preserve tool operation preset on in-container drag-release * fix(workflow-editor): wire drag-release edge from the actual source handle via handleToolbarDrop * refactor(workflow-editor): collapse drag-release correlation into one store field
1 parent 3d02bbb commit fca5f10

4 files changed

Lines changed: 289 additions & 99 deletions

File tree

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

Lines changed: 112 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ import { useCanvasModeStore } from '@/stores/canvas-mode'
9595
import { useChatStore } from '@/stores/chat/store'
9696
import { defaultWorkflowExecutionState, useExecutionStore } from '@/stores/execution'
9797
import { useSearchModalStore } from '@/stores/modals/search/store'
98+
import type { PendingConnect } from '@/stores/modals/search/types'
9899
import { usePanelEditorStore } from '@/stores/panel'
99100
import { useUndoRedoStore } from '@/stores/undo-redo'
100101
import { useVariablesModalStore } from '@/stores/variables/modal'
@@ -209,6 +210,7 @@ interface AddBlockFromToolbarDetail {
209210
type?: unknown
210211
enableTriggerMode?: unknown
211212
presetOperation?: unknown
213+
pendingConnect?: PendingConnect
212214
}
213215

214216
/**
@@ -1779,9 +1781,55 @@ const WorkflowContent = React.memo(
17791781
* @param position - Drop position in ReactFlow coordinates.
17801782
*/
17811783
const handleToolbarDrop = useCallback(
1782-
(data: { type: string; enableTriggerMode?: boolean }, position: { x: number; y: number }) => {
1784+
(
1785+
data: {
1786+
type: string
1787+
enableTriggerMode?: boolean
1788+
presetOperation?: string
1789+
forcedSource?: { nodeId: string; handleId: string }
1790+
},
1791+
position: { x: number; y: number }
1792+
) => {
17831793
if (!data.type || data.type === 'connectionBlock') return
17841794

1795+
const operationConfig = data.presetOperation
1796+
? { operation: data.presetOperation }
1797+
: undefined
1798+
1799+
const { forcedSource } = data
1800+
1801+
/**
1802+
* Edge for the new block. With a `forcedSource` (a drag-release from a
1803+
* handle), wire from that exact handle — but only when it stays within the
1804+
* resolved container context, matching onConnect's boundary rules; a
1805+
* cross-boundary source yields no edge. Otherwise fall back to normal
1806+
* proximity auto-connect.
1807+
*/
1808+
const resolveEdge = (
1809+
targetId: string,
1810+
targetParentId: string | null,
1811+
fallback: () => Edge | undefined
1812+
): Edge | undefined => {
1813+
if (!forcedSource) return fallback()
1814+
1815+
const isContainerStartHandle =
1816+
forcedSource.handleId === 'loop-start-source' ||
1817+
forcedSource.handleId === 'parallel-start-source'
1818+
if (isContainerStartHandle) {
1819+
// A container-start handle may only wire to a child of that container.
1820+
return forcedSource.nodeId === targetParentId
1821+
? createEdgeObject(forcedSource.nodeId, targetId, forcedSource.handleId)
1822+
: undefined
1823+
}
1824+
1825+
const sourceBlock = blocks[forcedSource.nodeId]
1826+
if (!sourceBlock) return undefined
1827+
const sourceParentId = sourceBlock.data?.parentId ?? null
1828+
return sourceParentId === targetParentId
1829+
? createEdgeObject(forcedSource.nodeId, targetId, forcedSource.handleId)
1830+
: undefined
1831+
}
1832+
17851833
try {
17861834
const containerInfo = isPointInLoopNode(position)
17871835

@@ -1811,11 +1859,13 @@ const WorkflowContent = React.memo(
18111859
.filter((b) => b.data?.parentId === containerInfo.loopId)
18121860
.map((b) => ({ id: b.id, type: b.type, position: b.position }))
18131861

1814-
const autoConnectEdge = tryCreateAutoConnectEdge(relativePosition, id, {
1815-
targetParentId: containerInfo.loopId,
1816-
existingChildBlocks,
1817-
containerId: containerInfo.loopId,
1818-
})
1862+
const autoConnectEdge = resolveEdge(id, containerInfo.loopId, () =>
1863+
tryCreateAutoConnectEdge(relativePosition, id, {
1864+
targetParentId: containerInfo.loopId,
1865+
existingChildBlocks,
1866+
containerId: containerInfo.loopId,
1867+
})
1868+
)
18191869

18201870
addBlock(
18211871
id,
@@ -1836,9 +1886,11 @@ const WorkflowContent = React.memo(
18361886

18371887
resizeLoopNodesWrapper()
18381888
} else {
1839-
const autoConnectEdge = tryCreateAutoConnectEdge(position, id, {
1840-
targetParentId: null,
1841-
})
1889+
const autoConnectEdge = resolveEdge(id, null, () =>
1890+
tryCreateAutoConnectEdge(position, id, {
1891+
targetParentId: null,
1892+
})
1893+
)
18421894

18431895
addBlock(
18441896
id,
@@ -1903,11 +1955,13 @@ const WorkflowContent = React.memo(
19031955
.filter((b) => b.data?.parentId === containerInfo.loopId)
19041956
.map((b) => ({ id: b.id, type: b.type, position: b.position }))
19051957

1906-
const autoConnectEdge = tryCreateAutoConnectEdge(relativePosition, id, {
1907-
targetParentId: containerInfo.loopId,
1908-
existingChildBlocks,
1909-
containerId: containerInfo.loopId,
1910-
})
1958+
const autoConnectEdge = resolveEdge(id, containerInfo.loopId, () =>
1959+
tryCreateAutoConnectEdge(relativePosition, id, {
1960+
targetParentId: containerInfo.loopId,
1961+
existingChildBlocks,
1962+
containerId: containerInfo.loopId,
1963+
})
1964+
)
19111965

19121966
// Add block with parent info AND autoConnectEdge (atomic operation)
19131967
addBlock(
@@ -1921,7 +1975,9 @@ const WorkflowContent = React.memo(
19211975
},
19221976
containerInfo.loopId,
19231977
'parent',
1924-
autoConnectEdge
1978+
autoConnectEdge,
1979+
undefined,
1980+
operationConfig
19251981
)
19261982

19271983
// Resize the container node to fit the new block
@@ -1931,9 +1987,11 @@ const WorkflowContent = React.memo(
19311987
// Centralized trigger constraints
19321988
if (checkTriggerConstraints(data.type)) return
19331989

1934-
const autoConnectEdge = tryCreateAutoConnectEdge(position, id, {
1935-
targetParentId: null,
1936-
})
1990+
const autoConnectEdge = resolveEdge(id, null, () =>
1991+
tryCreateAutoConnectEdge(position, id, {
1992+
targetParentId: null,
1993+
})
1994+
)
19371995

19381996
// Regular canvas drop with auto-connect edge
19391997
// Use enableTriggerMode from drag data if present (when dragging from Triggers tab)
@@ -1947,7 +2005,8 @@ const WorkflowContent = React.memo(
19472005
undefined,
19482006
undefined,
19492007
autoConnectEdge,
1950-
enableTriggerMode
2008+
enableTriggerMode,
2009+
operationConfig
19512010
)
19522011
}
19532012
} catch (err) {
@@ -1961,6 +2020,7 @@ const WorkflowContent = React.memo(
19612020
addBlock,
19622021
tryCreateAutoConnectEdge,
19632022
checkTriggerConstraints,
2023+
createEdgeObject,
19642024
]
19652025
)
19662026

@@ -1972,11 +2032,30 @@ const WorkflowContent = React.memo(
19722032
return
19732033
}
19742034

1975-
const { type, enableTriggerMode, presetOperation } = event.detail
2035+
const { type, enableTriggerMode, presetOperation, pendingConnect } = event.detail
19762036

19772037
if (typeof type !== 'string' || !type) return
19782038
if (type === 'connectionBlock') return
19792039

2040+
// Complete an edge drag-release: only a genuine palette selection carries
2041+
// `pendingConnect` (other add-block dispatchers — toolbar, sidebar, command
2042+
// list — don't), so its presence is the signal. Delegating to
2043+
// handleToolbarDrop with the drag source gives container-aware placement AND
2044+
// an edge from the released handle that respects container boundaries.
2045+
if (pendingConnect) {
2046+
// screenToFlowPosition subtracts the pane rect internally — pass raw client coords.
2047+
handleToolbarDrop(
2048+
{
2049+
type,
2050+
enableTriggerMode: enableTriggerMode === true,
2051+
presetOperation: typeof presetOperation === 'string' ? presetOperation : undefined,
2052+
forcedSource: pendingConnect.source,
2053+
},
2054+
screenToFlowPosition({ x: pendingConnect.screenX, y: pendingConnect.screenY })
2055+
)
2056+
return
2057+
}
2058+
19802059
const basePosition = getViewportCenter()
19812060

19822061
if (type === 'loop' || type === 'parallel') {
@@ -2054,6 +2133,8 @@ const WorkflowContent = React.memo(
20542133
effectivePermissions.canEdit,
20552134
checkTriggerConstraints,
20562135
tryCreateAutoConnectEdge,
2136+
screenToFlowPosition,
2137+
handleToolbarDrop,
20572138
])
20582139

20592140
/**
@@ -3132,6 +3213,17 @@ const WorkflowContent = React.memo(
31323213
target: targetNode.id,
31333214
targetHandle: 'target',
31343215
})
3216+
} else if (!targetNode) {
3217+
// Released on empty canvas: open the command palette with the drag origin
3218+
// + drop point, so the chosen block lands here wired from this handle.
3219+
useSearchModalStore.getState().open({
3220+
sections: ['blocks', 'tools', 'toolOperations'],
3221+
pendingConnect: {
3222+
source: { nodeId: source.nodeId, handleId: source.handleId },
3223+
screenX: clientPos.clientX,
3224+
screenY: clientPos.clientY,
3225+
},
3226+
})
31353227
}
31363228

31373229
connectionSourceRef.current = null

0 commit comments

Comments
 (0)