Skip to content

Commit 1038e14

Browse files
authored
fix autoconnect (#1127)
1 parent 8b78200 commit 1038e14

File tree

2 files changed

+136
-18
lines changed

2 files changed

+136
-18
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -352,11 +352,13 @@ export const isPointInLoopNode = (
352352
const containingNodes = getNodes()
353353
.filter((n) => isContainerType(n.type))
354354
.filter((n) => {
355+
// Use absolute coordinates for nested containers
356+
const absolutePos = getNodeAbsolutePosition(n.id, getNodes)
355357
const rect = {
356-
left: n.position.x,
357-
right: n.position.x + (n.data?.width || DEFAULT_CONTAINER_WIDTH),
358-
top: n.position.y,
359-
bottom: n.position.y + (n.data?.height || DEFAULT_CONTAINER_HEIGHT),
358+
left: absolutePos.x,
359+
right: absolutePos.x + (n.data?.width || DEFAULT_CONTAINER_WIDTH),
360+
top: absolutePos.y,
361+
bottom: absolutePos.y + (n.data?.height || DEFAULT_CONTAINER_HEIGHT),
360362
}
361363

362364
return (
@@ -368,7 +370,8 @@ export const isPointInLoopNode = (
368370
})
369371
.map((n) => ({
370372
loopId: n.id,
371-
loopPosition: n.position,
373+
// Return absolute position so callers can compute relative placement correctly
374+
loopPosition: getNodeAbsolutePosition(n.id, getNodes),
372375
dimensions: {
373376
width: n.data?.width || DEFAULT_CONTAINER_WIDTH,
374377
height: n.data?.height || DEFAULT_CONTAINER_HEIGHT,

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

Lines changed: 128 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,41 @@ const WorkflowContent = React.memo(() => {
281281
[getNodes]
282282
)
283283

284+
// Compute the absolute position of a node's source anchor (right-middle)
285+
const getNodeAnchorPosition = useCallback(
286+
(nodeId: string): { x: number; y: number } => {
287+
const node = getNodes().find((n) => n.id === nodeId)
288+
const absPos = getNodeAbsolutePositionWrapper(nodeId)
289+
290+
if (!node) {
291+
return absPos
292+
}
293+
294+
// Use known defaults per node type without type casting
295+
const isSubflow = node.type === 'subflowNode'
296+
const width = isSubflow
297+
? typeof node.data?.width === 'number'
298+
? node.data.width
299+
: 500
300+
: typeof node.width === 'number'
301+
? node.width
302+
: 350
303+
const height = isSubflow
304+
? typeof node.data?.height === 'number'
305+
? node.data.height
306+
: 300
307+
: typeof node.height === 'number'
308+
? node.height
309+
: 100
310+
311+
return {
312+
x: absPos.x + width,
313+
y: absPos.y + height / 2,
314+
}
315+
},
316+
[getNodes, getNodeAbsolutePositionWrapper]
317+
)
318+
284319
// Auto-layout handler - now uses frontend auto layout for immediate updates
285320
const handleAutoLayout = useCallback(async () => {
286321
if (Object.keys(blocks).length === 0) return
@@ -373,22 +408,37 @@ const WorkflowContent = React.memo(() => {
373408
// Handle drops
374409
const findClosestOutput = useCallback(
375410
(newNodePosition: { x: number; y: number }): BlockData | null => {
376-
const existingBlocks = Object.entries(blocks)
377-
.filter(([_, block]) => block.enabled)
378-
.map(([id, block]) => ({
379-
id,
380-
type: block.type,
381-
position: block.position,
382-
distance: Math.sqrt(
383-
(block.position.x - newNodePosition.x) ** 2 +
384-
(block.position.y - newNodePosition.y) ** 2
385-
),
386-
}))
411+
// Determine if drop is inside a container; if not, exclude child nodes from candidates
412+
const containerAtPoint = isPointInLoopNodeWrapper(newNodePosition)
413+
const nodeIndex = new Map(getNodes().map((n) => [n.id, n]))
414+
415+
const candidates = Object.entries(blocks)
416+
.filter(([id, block]) => {
417+
if (!block.enabled) return false
418+
const node = nodeIndex.get(id)
419+
if (!node) return false
420+
421+
// If dropping outside containers, ignore blocks that are inside a container
422+
if (!containerAtPoint && node.parentId) return false
423+
return true
424+
})
425+
.map(([id, block]) => {
426+
const anchor = getNodeAnchorPosition(id)
427+
const distance = Math.sqrt(
428+
(anchor.x - newNodePosition.x) ** 2 + (anchor.y - newNodePosition.y) ** 2
429+
)
430+
return {
431+
id,
432+
type: block.type,
433+
position: anchor,
434+
distance,
435+
}
436+
})
387437
.sort((a, b) => a.distance - b.distance)
388438

389-
return existingBlocks[0] || null
439+
return candidates[0] || null
390440
},
391-
[blocks]
441+
[blocks, getNodes, getNodeAnchorPosition, isPointInLoopNodeWrapper]
392442
)
393443

394444
// Determine the appropriate source handle based on block type
@@ -1385,8 +1435,69 @@ const WorkflowContent = React.memo(() => {
13851435

13861436
// Update the node's parent relationship
13871437
if (potentialParentId) {
1438+
// Compute relative position BEFORE updating parent to avoid stale state
1439+
const containerAbsPosBefore = getNodeAbsolutePositionWrapper(potentialParentId)
1440+
const nodeAbsPosBefore = getNodeAbsolutePositionWrapper(node.id)
1441+
const relativePositionBefore = {
1442+
x: nodeAbsPosBefore.x - containerAbsPosBefore.x,
1443+
y: nodeAbsPosBefore.y - containerAbsPosBefore.y,
1444+
}
1445+
13881446
// Moving to a new parent container
13891447
updateNodeParent(node.id, potentialParentId)
1448+
1449+
// Auto-connect when moving an existing block into a container
1450+
const isAutoConnectEnabled = useGeneralStore.getState().isAutoConnectEnabled
1451+
if (isAutoConnectEnabled) {
1452+
// Existing children in the target container (excluding the moved node)
1453+
const existingChildBlocks = Object.values(blocks).filter(
1454+
(b) => b.data?.parentId === potentialParentId && b.id !== node.id
1455+
)
1456+
1457+
if (existingChildBlocks.length > 0) {
1458+
// Connect from nearest existing child inside the container
1459+
const closestBlock = existingChildBlocks
1460+
.map((b) => ({
1461+
block: b,
1462+
distance: Math.sqrt(
1463+
(b.position.x - relativePositionBefore.x) ** 2 +
1464+
(b.position.y - relativePositionBefore.y) ** 2
1465+
),
1466+
}))
1467+
.sort((a, b) => a.distance - b.distance)[0]?.block
1468+
1469+
if (closestBlock) {
1470+
const sourceHandle = determineSourceHandle({
1471+
id: closestBlock.id,
1472+
type: closestBlock.type,
1473+
})
1474+
addEdge({
1475+
id: crypto.randomUUID(),
1476+
source: closestBlock.id,
1477+
target: node.id,
1478+
sourceHandle,
1479+
targetHandle: 'target',
1480+
type: 'workflowEdge',
1481+
})
1482+
}
1483+
} else {
1484+
// No children: connect from the container's start handle to the moved node
1485+
const containerNode = getNodes().find((n) => n.id === potentialParentId)
1486+
const startSourceHandle =
1487+
(containerNode?.data as any)?.kind === 'loop'
1488+
? 'loop-start-source'
1489+
: 'parallel-start-source'
1490+
1491+
addEdge({
1492+
id: crypto.randomUUID(),
1493+
source: potentialParentId,
1494+
target: node.id,
1495+
sourceHandle: startSourceHandle,
1496+
targetHandle: 'target',
1497+
type: 'workflowEdge',
1498+
})
1499+
}
1500+
}
13901501
}
13911502

13921503
// Reset state
@@ -1400,6 +1511,10 @@ const WorkflowContent = React.memo(() => {
14001511
updateNodeParent,
14011512
getNodeHierarchyWrapper,
14021513
collaborativeUpdateBlockPosition,
1514+
addEdge,
1515+
determineSourceHandle,
1516+
blocks,
1517+
getNodeAbsolutePositionWrapper,
14031518
]
14041519
)
14051520

0 commit comments

Comments
 (0)