fix(change-detection): add condition to check against duplicate edges

This commit is contained in:
waleed
2026-01-13 15:47:38 -08:00
parent c1a92d3be9
commit 96a2979bee
4 changed files with 41 additions and 54 deletions

View File

@@ -356,6 +356,9 @@ const WorkflowContent = React.memo(() => {
/** Stores source node/handle info when a connection drag starts for drop-on-block detection. */
const connectionSourceRef = useRef<{ nodeId: string; handleId: string } | null>(null)
/** Tracks whether onConnect successfully handled the connection (ReactFlow pattern). */
const connectionCompletedRef = useRef(false)
/** Stores start positions for multi-node drag undo/redo recording. */
const multiNodeDragStartRef = useRef<Map<string, { x: number; y: number; parentId?: string }>>(
new Map()
@@ -2214,7 +2217,8 @@ const WorkflowContent = React.memo(() => {
)
/**
* Captures the source handle when a connection drag starts
* Captures the source handle when a connection drag starts.
* Resets connectionCompletedRef to track if onConnect handles this connection.
*/
const onConnectStart = useCallback((_event: any, params: any) => {
const handleId: string | undefined = params?.handleId
@@ -2223,6 +2227,7 @@ const WorkflowContent = React.memo(() => {
nodeId: params?.nodeId,
handleId: params?.handleId,
}
connectionCompletedRef.current = false
}, [])
/** Handles new edge connections with container boundary validation. */
@@ -2283,6 +2288,7 @@ const WorkflowContent = React.memo(() => {
isInsideContainer: true,
},
})
connectionCompletedRef.current = true
return
}
@@ -2311,6 +2317,7 @@ const WorkflowContent = React.memo(() => {
}
: undefined,
})
connectionCompletedRef.current = true
}
},
[addEdge, getNodes, blocks]
@@ -2319,8 +2326,9 @@ const WorkflowContent = React.memo(() => {
/**
* Handles connection drag end. Detects if the edge was dropped over a block
* and automatically creates a connection to that block's target handle.
* Only creates a connection if ReactFlow didn't already handle it (e.g., when
* dropping on the block body instead of a handle).
*
* Uses connectionCompletedRef to check if onConnect already handled this connection
* (ReactFlow pattern for distinguishing handle-to-handle vs handle-to-body drops).
*/
const onConnectEnd = useCallback(
(event: MouseEvent | TouchEvent) => {
@@ -2332,6 +2340,12 @@ const WorkflowContent = React.memo(() => {
return
}
// If onConnect already handled this connection, skip (handle-to-handle case)
if (connectionCompletedRef.current) {
connectionSourceRef.current = null
return
}
// Get cursor position in flow coordinates
const clientPos = 'changedTouches' in event ? event.changedTouches[0] : event
const flowPosition = screenToFlowPosition({
@@ -2342,12 +2356,7 @@ const WorkflowContent = React.memo(() => {
// Find node under cursor
const targetNode = findNodeAtPosition(flowPosition)
// Create connection if valid target found AND edge doesn't already exist
// ReactFlow's onConnect fires first when dropping on a handle, so we check
// if that connection already exists to avoid creating duplicates.
// IMPORTANT: We must read directly from the store (not React state) because
// the store update from ReactFlow's onConnect may not have triggered a
// React re-render yet when this callback runs (typically 1-2ms later).
// Create connection if valid target found (handle-to-body case)
if (targetNode && targetNode.id !== source.nodeId) {
const currentEdges = useWorkflowStore.getState().edges
const edgeAlreadyExists = currentEdges.some(

View File

@@ -197,9 +197,10 @@ export function normalizeEdge(edge: Edge): NormalizedEdge {
}
/**
* Sorts edges for consistent comparison
* Sorts and deduplicates edges for consistent comparison.
* Deduplication handles legacy data that may contain duplicate edges.
* @param edges - Array of edges to sort
* @returns Sorted array of normalized edges
* @returns Sorted array of unique normalized edges
*/
export function sortEdges(
edges: Array<{
@@ -214,7 +215,13 @@ export function sortEdges(
target: string
targetHandle?: string | null
}> {
return [...edges].sort((a, b) =>
const uniqueEdges = new Map<string, (typeof edges)[number]>()
for (const edge of edges) {
const key = `${edge.source}-${edge.sourceHandle ?? 'null'}-${edge.target}-${edge.targetHandle ?? 'null'}`
uniqueEdges.set(key, edge)
}
return Array.from(uniqueEdges.values()).sort((a, b) =>
`${a.source}-${a.sourceHandle}-${a.target}-${a.targetHandle}`.localeCompare(
`${b.source}-${b.sourceHandle}-${b.target}-${b.targetHandle}`
)

View File

@@ -95,41 +95,19 @@ export const useOperationQueueStore = create<OperationQueueState>((set, get) =>
return
}
// Enhanced duplicate content check - especially important for block and edge operations
const duplicateContent = state.operations.find((op) => {
if (
op.operation.operation !== operation.operation.operation ||
op.operation.target !== operation.operation.target ||
op.workflowId !== operation.workflowId
) {
return false
}
// For block operations, check the block ID specifically
if (operation.operation.target === 'block') {
return op.operation.payload?.id === operation.operation.payload?.id
}
// For edge operations (batch-add-edges), check by source→target connection
// This prevents duplicate edges with different IDs but same connection
if (
operation.operation.target === 'edges' &&
operation.operation.operation === 'batch-add-edges'
) {
const newEdges = operation.operation.payload?.edges || []
const existingEdges = op.operation.payload?.edges || []
// Check if any new edge has the same source→target as an existing operation's edges
return newEdges.some((newEdge: any) =>
existingEdges.some(
(existingEdge: any) =>
existingEdge.source === newEdge.source && existingEdge.target === newEdge.target
)
)
}
// For other operations, fall back to full payload comparison
return JSON.stringify(op.operation.payload) === JSON.stringify(operation.operation.payload)
})
// Enhanced duplicate content check - especially important for block operations
const duplicateContent = state.operations.find(
(op) =>
op.operation.operation === operation.operation.operation &&
op.operation.target === operation.operation.target &&
op.workflowId === operation.workflowId &&
// For block operations, check the block ID specifically
((operation.operation.target === 'block' &&
op.operation.payload?.id === operation.operation.payload?.id) ||
// For other operations, fall back to full payload comparison
(operation.operation.target !== 'block' &&
JSON.stringify(op.operation.payload) === JSON.stringify(operation.operation.payload)))
)
const isReplaceStateWorkflowOp =
operation.operation.target === 'workflow' && operation.operation.operation === 'replace-state'

View File

@@ -498,8 +498,6 @@ export const useWorkflowStore = create<WorkflowStore>()(
const currentEdges = get().edges
const newEdges = [...currentEdges]
const existingEdgeIds = new Set(currentEdges.map((e) => e.id))
// Track existing connections to prevent duplicates (same source->target)
const existingConnections = new Set(currentEdges.map((e) => `${e.source}->${e.target}`))
for (const edge of edges) {
// Skip if edge ID already exists
@@ -508,10 +506,6 @@ export const useWorkflowStore = create<WorkflowStore>()(
// Skip self-referencing edges
if (edge.source === edge.target) continue
// Skip if connection already exists (same source and target)
const connectionKey = `${edge.source}->${edge.target}`
if (existingConnections.has(connectionKey)) continue
// Skip if would create a cycle
if (wouldCreateCycle([...newEdges], edge.source, edge.target)) continue
@@ -525,7 +519,6 @@ export const useWorkflowStore = create<WorkflowStore>()(
data: edge.data || {},
})
existingEdgeIds.add(edge.id)
existingConnections.add(connectionKey)
}
const blocks = get().blocks