Skip to content

Commit 039a972

Browse files
committed
fix(change-detection): add condition to check against duplicate edges
1 parent c35b1c4 commit 039a972

File tree

4 files changed

+41
-47
lines changed

4 files changed

+41
-47
lines changed

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,9 @@ const WorkflowContent = React.memo(() => {
356356
/** Stores source node/handle info when a connection drag starts for drop-on-block detection. */
357357
const connectionSourceRef = useRef<{ nodeId: string; handleId: string } | null>(null)
358358

359+
/** Tracks whether onConnect successfully handled the connection (ReactFlow pattern). */
360+
const connectionCompletedRef = useRef(false)
361+
359362
/** Stores start positions for multi-node drag undo/redo recording. */
360363
const multiNodeDragStartRef = useRef<Map<string, { x: number; y: number; parentId?: string }>>(
361364
new Map()
@@ -2214,7 +2217,8 @@ const WorkflowContent = React.memo(() => {
22142217
)
22152218

22162219
/**
2217-
* Captures the source handle when a connection drag starts
2220+
* Captures the source handle when a connection drag starts.
2221+
* Resets connectionCompletedRef to track if onConnect handles this connection.
22182222
*/
22192223
const onConnectStart = useCallback((_event: any, params: any) => {
22202224
const handleId: string | undefined = params?.handleId
@@ -2223,6 +2227,7 @@ const WorkflowContent = React.memo(() => {
22232227
nodeId: params?.nodeId,
22242228
handleId: params?.handleId,
22252229
}
2230+
connectionCompletedRef.current = false
22262231
}, [])
22272232

22282233
/** Handles new edge connections with container boundary validation. */
@@ -2283,6 +2288,7 @@ const WorkflowContent = React.memo(() => {
22832288
isInsideContainer: true,
22842289
},
22852290
})
2291+
connectionCompletedRef.current = true
22862292
return
22872293
}
22882294

@@ -2311,6 +2317,7 @@ const WorkflowContent = React.memo(() => {
23112317
}
23122318
: undefined,
23132319
})
2320+
connectionCompletedRef.current = true
23142321
}
23152322
},
23162323
[addEdge, getNodes, blocks]
@@ -2319,6 +2326,9 @@ const WorkflowContent = React.memo(() => {
23192326
/**
23202327
* Handles connection drag end. Detects if the edge was dropped over a block
23212328
* and automatically creates a connection to that block's target handle.
2329+
*
2330+
* Uses connectionCompletedRef to check if onConnect already handled this connection
2331+
* (ReactFlow pattern for distinguishing handle-to-handle vs handle-to-body drops).
23222332
*/
23232333
const onConnectEnd = useCallback(
23242334
(event: MouseEvent | TouchEvent) => {
@@ -2330,6 +2340,12 @@ const WorkflowContent = React.memo(() => {
23302340
return
23312341
}
23322342

2343+
// If onConnect already handled this connection, skip (handle-to-handle case)
2344+
if (connectionCompletedRef.current) {
2345+
connectionSourceRef.current = null
2346+
return
2347+
}
2348+
23332349
// Get cursor position in flow coordinates
23342350
const clientPos = 'changedTouches' in event ? event.changedTouches[0] : event
23352351
const flowPosition = screenToFlowPosition({
@@ -2340,7 +2356,7 @@ const WorkflowContent = React.memo(() => {
23402356
// Find node under cursor
23412357
const targetNode = findNodeAtPosition(flowPosition)
23422358

2343-
// Create connection if valid target found
2359+
// Create connection if valid target found (handle-to-body case)
23442360
if (targetNode && targetNode.id !== source.nodeId) {
23452361
onConnect({
23462362
source: source.nodeId,

apps/sim/lib/workflows/comparison/normalize.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,9 +197,10 @@ export function normalizeEdge(edge: Edge): NormalizedEdge {
197197
}
198198

199199
/**
200-
* Sorts edges for consistent comparison
200+
* Sorts and deduplicates edges for consistent comparison.
201+
* Deduplication handles legacy data that may contain duplicate edges.
201202
* @param edges - Array of edges to sort
202-
* @returns Sorted array of normalized edges
203+
* @returns Sorted array of unique normalized edges
203204
*/
204205
export function sortEdges(
205206
edges: Array<{
@@ -214,7 +215,13 @@ export function sortEdges(
214215
target: string
215216
targetHandle?: string | null
216217
}> {
217-
return [...edges].sort((a, b) =>
218+
const uniqueEdges = new Map<string, (typeof edges)[number]>()
219+
for (const edge of edges) {
220+
const key = `${edge.source}-${edge.sourceHandle ?? 'null'}-${edge.target}-${edge.targetHandle ?? 'null'}`
221+
uniqueEdges.set(key, edge)
222+
}
223+
224+
return Array.from(uniqueEdges.values()).sort((a, b) =>
218225
`${a.source}-${a.sourceHandle}-${a.target}-${a.targetHandle}`.localeCompare(
219226
`${b.source}-${b.sourceHandle}-${b.target}-${b.targetHandle}`
220227
)

apps/sim/stores/operation-queue/store.ts

Lines changed: 13 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -95,41 +95,19 @@ export const useOperationQueueStore = create<OperationQueueState>((set, get) =>
9595
return
9696
}
9797

98-
// Enhanced duplicate content check - especially important for block and edge operations
99-
const duplicateContent = state.operations.find((op) => {
100-
if (
101-
op.operation.operation !== operation.operation.operation ||
102-
op.operation.target !== operation.operation.target ||
103-
op.workflowId !== operation.workflowId
104-
) {
105-
return false
106-
}
107-
108-
// For block operations, check the block ID specifically
109-
if (operation.operation.target === 'block') {
110-
return op.operation.payload?.id === operation.operation.payload?.id
111-
}
112-
113-
// For edge operations (batch-add-edges), check by source→target connection
114-
// This prevents duplicate edges with different IDs but same connection
115-
if (
116-
operation.operation.target === 'edges' &&
117-
operation.operation.operation === 'batch-add-edges'
118-
) {
119-
const newEdges = operation.operation.payload?.edges || []
120-
const existingEdges = op.operation.payload?.edges || []
121-
// Check if any new edge has the same source→target as an existing operation's edges
122-
return newEdges.some((newEdge: any) =>
123-
existingEdges.some(
124-
(existingEdge: any) =>
125-
existingEdge.source === newEdge.source && existingEdge.target === newEdge.target
126-
)
127-
)
128-
}
129-
130-
// For other operations, fall back to full payload comparison
131-
return JSON.stringify(op.operation.payload) === JSON.stringify(operation.operation.payload)
132-
})
98+
// Enhanced duplicate content check - especially important for block operations
99+
const duplicateContent = state.operations.find(
100+
(op) =>
101+
op.operation.operation === operation.operation.operation &&
102+
op.operation.target === operation.operation.target &&
103+
op.workflowId === operation.workflowId &&
104+
// For block operations, check the block ID specifically
105+
((operation.operation.target === 'block' &&
106+
op.operation.payload?.id === operation.operation.payload?.id) ||
107+
// For other operations, fall back to full payload comparison
108+
(operation.operation.target !== 'block' &&
109+
JSON.stringify(op.operation.payload) === JSON.stringify(operation.operation.payload)))
110+
)
133111

134112
const isReplaceStateWorkflowOp =
135113
operation.operation.target === 'workflow' && operation.operation.operation === 'replace-state'

apps/sim/stores/workflows/workflow/store.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -498,8 +498,6 @@ export const useWorkflowStore = create<WorkflowStore>()(
498498
const currentEdges = get().edges
499499
const newEdges = [...currentEdges]
500500
const existingEdgeIds = new Set(currentEdges.map((e) => e.id))
501-
// Track existing connections to prevent duplicates (same source->target)
502-
const existingConnections = new Set(currentEdges.map((e) => `${e.source}->${e.target}`))
503501

504502
for (const edge of edges) {
505503
// Skip if edge ID already exists
@@ -508,10 +506,6 @@ export const useWorkflowStore = create<WorkflowStore>()(
508506
// Skip self-referencing edges
509507
if (edge.source === edge.target) continue
510508

511-
// Skip if connection already exists (same source and target)
512-
const connectionKey = `${edge.source}->${edge.target}`
513-
if (existingConnections.has(connectionKey)) continue
514-
515509
// Skip if would create a cycle
516510
if (wouldCreateCycle([...newEdges], edge.source, edge.target)) continue
517511

@@ -525,7 +519,6 @@ export const useWorkflowStore = create<WorkflowStore>()(
525519
data: edge.data || {},
526520
})
527521
existingEdgeIds.add(edge.id)
528-
existingConnections.add(connectionKey)
529522
}
530523

531524
const blocks = get().blocks

0 commit comments

Comments
 (0)