Compare commits

...

4 Commits

Author SHA1 Message Date
waleed
79621d788e fix(change-detection): add condition to check against duplicate edges 2026-01-13 15:51:35 -08:00
Waleed
67686eac07 fix(a2a): removed deployment constraint for redeploying a2a workflows (#2796)
* fix(a2a): removed deployment constraint for redeploying a2a workflows

* updated A2A tab copy state

* consolidated trigger types const
2026-01-13 15:51:35 -08:00
waleed
96a2979bee fix(change-detection): add condition to check against duplicate edges 2026-01-13 15:51:30 -08:00
waleed
c1a92d3be9 fix(dups): onConntext being called twice for a signle edge, once in onConnectEnd and onConnectExtended 2026-01-13 15:51:00 -08:00
4 changed files with 31 additions and 20 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. */ /** Stores source node/handle info when a connection drag starts for drop-on-block detection. */
const connectionSourceRef = useRef<{ nodeId: string; handleId: string } | null>(null) 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. */ /** Stores start positions for multi-node drag undo/redo recording. */
const multiNodeDragStartRef = useRef<Map<string, { x: number; y: number; parentId?: string }>>( const multiNodeDragStartRef = useRef<Map<string, { x: number; y: number; parentId?: string }>>(
new Map() 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 onConnectStart = useCallback((_event: any, params: any) => {
const handleId: string | undefined = params?.handleId const handleId: string | undefined = params?.handleId
@@ -2223,6 +2227,7 @@ const WorkflowContent = React.memo(() => {
nodeId: params?.nodeId, nodeId: params?.nodeId,
handleId: params?.handleId, handleId: params?.handleId,
} }
connectionCompletedRef.current = false
}, []) }, [])
/** Handles new edge connections with container boundary validation. */ /** Handles new edge connections with container boundary validation. */
@@ -2283,6 +2288,7 @@ const WorkflowContent = React.memo(() => {
isInsideContainer: true, isInsideContainer: true,
}, },
}) })
connectionCompletedRef.current = true
return return
} }
@@ -2311,6 +2317,7 @@ const WorkflowContent = React.memo(() => {
} }
: undefined, : undefined,
}) })
connectionCompletedRef.current = true
} }
}, },
[addEdge, getNodes, blocks] [addEdge, getNodes, blocks]
@@ -2319,8 +2326,9 @@ const WorkflowContent = React.memo(() => {
/** /**
* Handles connection drag end. Detects if the edge was dropped over a block * Handles connection drag end. Detects if the edge was dropped over a block
* and automatically creates a connection to that block's target handle. * 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( const onConnectEnd = useCallback(
(event: MouseEvent | TouchEvent) => { (event: MouseEvent | TouchEvent) => {
@@ -2332,6 +2340,12 @@ const WorkflowContent = React.memo(() => {
return 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 // Get cursor position in flow coordinates
const clientPos = 'changedTouches' in event ? event.changedTouches[0] : event const clientPos = 'changedTouches' in event ? event.changedTouches[0] : event
const flowPosition = screenToFlowPosition({ const flowPosition = screenToFlowPosition({
@@ -2342,12 +2356,7 @@ const WorkflowContent = React.memo(() => {
// Find node under cursor // Find node under cursor
const targetNode = findNodeAtPosition(flowPosition) const targetNode = findNodeAtPosition(flowPosition)
// Create connection if valid target found AND edge doesn't already exist // Create connection if valid target found (handle-to-body case)
// 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).
if (targetNode && targetNode.id !== source.nodeId) { if (targetNode && targetNode.id !== source.nodeId) {
const currentEdges = useWorkflowStore.getState().edges const currentEdges = useWorkflowStore.getState().edges
const edgeAlreadyExists = currentEdges.some( const edgeAlreadyExists = currentEdges.some(

View File

@@ -24,7 +24,9 @@ export function hasWorkflowChanged(
deployedState: WorkflowState | null deployedState: WorkflowState | null
): boolean { ): boolean {
// If no deployed state exists, then the workflow has changed // If no deployed state exists, then the workflow has changed
if (!deployedState) return true if (!deployedState) {
return true
}
// 1. Compare edges (connections between blocks) // 1. Compare edges (connections between blocks)
const currentEdges = currentState.edges || [] const currentEdges = currentState.edges || []

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 * @param edges - Array of edges to sort
* @returns Sorted array of normalized edges * @returns Sorted array of unique normalized edges
*/ */
export function sortEdges( export function sortEdges(
edges: Array<{ edges: Array<{
@@ -214,7 +215,13 @@ export function sortEdges(
target: string target: string
targetHandle?: string | null 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( `${a.source}-${a.sourceHandle}-${a.target}-${a.targetHandle}`.localeCompare(
`${b.source}-${b.sourceHandle}-${b.target}-${b.targetHandle}` `${b.source}-${b.sourceHandle}-${b.target}-${b.targetHandle}`
) )

View File

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