mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-14 09:27:58 -05:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79621d788e | ||
|
|
67686eac07 | ||
|
|
96a2979bee | ||
|
|
c1a92d3be9 |
@@ -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(
|
||||||
|
|||||||
@@ -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 || []
|
||||||
|
|||||||
@@ -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}`
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user