From 8429040921a222f33cb4da251cd9dcad75be222d Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Sat, 24 Jan 2026 11:20:28 -0800 Subject: [PATCH] fix(notes): ghost edges (#2970) * fix(notes): ghost edges * fix deployed state fallback * fallback * remove UI level checks * annotation missing from autoconnect source check --- .../[workspaceId]/w/[workflowId]/workflow.tsx | 61 +++++-------------- .../workspace/providers/socket-provider.tsx | 2 +- apps/sim/stores/workflows/workflow/store.ts | 36 +++++++++-- 3 files changed, 45 insertions(+), 54 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index a1089e0a7..77a1b7953 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -81,6 +81,7 @@ import { useWorkflowDiffStore } from '@/stores/workflow-diff/store' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { getUniqueBlockName, prepareBlockState } from '@/stores/workflows/utils' import { useWorkflowStore } from '@/stores/workflows/workflow/store' +import type { BlockState } from '@/stores/workflows/workflow/types' /** Lazy-loaded components for non-critical UI that can load after initial render */ const LazyChat = lazy(() => @@ -535,8 +536,7 @@ const WorkflowContent = React.memo(() => { return edgesToFilter.filter((edge) => { const sourceBlock = blocks[edge.source] const targetBlock = blocks[edge.target] - if (!sourceBlock || !targetBlock) return false - return !isAnnotationOnlyBlock(sourceBlock.type) && !isAnnotationOnlyBlock(targetBlock.type) + return Boolean(sourceBlock && targetBlock) }) }, [edges, isShowingDiff, isDiffReady, diffAnalysis, blocks]) @@ -1097,6 +1097,13 @@ const WorkflowContent = React.memo(() => { [collaborativeBatchRemoveEdges] ) + const isAutoConnectSourceCandidate = useCallback((block: BlockState): boolean => { + if (!block.enabled) return false + if (block.type === 'response') return false + if (isAnnotationOnlyBlock(block.type)) return false + return true + }, []) + /** Finds the closest block to a position for auto-connect. */ const findClosestOutput = useCallback( (newNodePosition: { x: number; y: number }): BlockData | null => { @@ -1109,8 +1116,7 @@ const WorkflowContent = React.memo(() => { position: { x: number; y: number } distanceSquared: number } | null>((acc, [id, block]) => { - if (!block.enabled) return acc - if (block.type === 'response') return acc + if (!isAutoConnectSourceCandidate(block)) return acc const node = nodeIndex.get(id) if (!node) return acc @@ -1140,7 +1146,7 @@ const WorkflowContent = React.memo(() => { position: closest.position, } }, - [blocks, getNodes, getNodeAnchorPosition, isPointInLoopNode] + [blocks, getNodes, getNodeAnchorPosition, isPointInLoopNode, isAutoConnectSourceCandidate] ) /** Determines the appropriate source handle based on block type. */ @@ -1208,7 +1214,8 @@ const WorkflowContent = React.memo(() => { position: { x: number; y: number } distanceSquared: number } | null>((acc, block) => { - if (block.type === 'response') return acc + const blockState = blocks[block.id] + if (!blockState || !isAutoConnectSourceCandidate(blockState)) return acc const distanceSquared = (block.position.x - targetPosition.x) ** 2 + (block.position.y - targetPosition.y) ** 2 if (!acc || distanceSquared < acc.distanceSquared) { @@ -1225,7 +1232,7 @@ const WorkflowContent = React.memo(() => { } : undefined }, - [] + [blocks, isAutoConnectSourceCandidate] ) /** @@ -1241,8 +1248,6 @@ const WorkflowContent = React.memo(() => { position: { x: number; y: number }, targetBlockId: string, options: { - blockType: string - enableTriggerMode?: boolean targetParentId?: string | null existingChildBlocks?: { id: string; type: string; position: { x: number; y: number } }[] containerId?: string @@ -1250,17 +1255,6 @@ const WorkflowContent = React.memo(() => { ): Edge | undefined => { if (!autoConnectRef.current) return undefined - // Don't auto-connect starter or annotation-only blocks - if (options.blockType === 'starter' || isAnnotationOnlyBlock(options.blockType)) { - return undefined - } - - // Check if target is a trigger block - const targetBlockConfig = getBlock(options.blockType) - const isTargetTrigger = - options.enableTriggerMode || targetBlockConfig?.category === 'triggers' - if (isTargetTrigger) return undefined - // Case 1: Adding block inside a container with existing children if (options.existingChildBlocks && options.existingChildBlocks.length > 0) { const closestBlock = findClosestBlockInSet(options.existingChildBlocks, position) @@ -1368,7 +1362,6 @@ const WorkflowContent = React.memo(() => { const name = getUniqueBlockName(baseName, blocks) const autoConnectEdge = tryCreateAutoConnectEdge(position, id, { - blockType: data.type, targetParentId: null, }) @@ -1439,8 +1432,6 @@ const WorkflowContent = React.memo(() => { .map((b) => ({ id: b.id, type: b.type, position: b.position })) const autoConnectEdge = tryCreateAutoConnectEdge(relativePosition, id, { - blockType: data.type, - enableTriggerMode: data.enableTriggerMode, targetParentId: containerInfo.loopId, existingChildBlocks, containerId: containerInfo.loopId, @@ -1469,8 +1460,6 @@ const WorkflowContent = React.memo(() => { if (checkTriggerConstraints(data.type)) return const autoConnectEdge = tryCreateAutoConnectEdge(position, id, { - blockType: data.type, - enableTriggerMode: data.enableTriggerMode, targetParentId: null, }) @@ -1526,7 +1515,6 @@ const WorkflowContent = React.memo(() => { const name = getUniqueBlockName(baseName, blocks) const autoConnectEdge = tryCreateAutoConnectEdge(basePosition, id, { - blockType: type, targetParentId: null, }) @@ -1562,8 +1550,6 @@ const WorkflowContent = React.memo(() => { const name = getUniqueBlockName(baseName, blocks) const autoConnectEdge = tryCreateAutoConnectEdge(basePosition, id, { - blockType: type, - enableTriggerMode, targetParentId: null, }) @@ -2364,24 +2350,6 @@ const WorkflowContent = React.memo(() => { if (!sourceNode || !targetNode) return - // Prevent connections to/from annotation-only blocks (non-executable) - if ( - isAnnotationOnlyBlock(sourceNode.data?.type) || - isAnnotationOnlyBlock(targetNode.data?.type) - ) { - return - } - - // Prevent incoming connections to trigger blocks (webhook, schedule, etc.) - if (targetNode.data?.config?.category === 'triggers') { - return - } - - // Prevent incoming connections to starter blocks (still keep separate for backward compatibility) - if (targetNode.data?.type === 'starter') { - return - } - // Get parent information (handle container start node case) const sourceParentId = blocks[sourceNode.id]?.data?.parentId || @@ -2787,7 +2755,6 @@ const WorkflowContent = React.memo(() => { .map((b) => ({ id: b.id, type: b.type, position: b.position })) const autoConnectEdge = tryCreateAutoConnectEdge(relativePositionBefore, node.id, { - blockType: node.data?.type || '', targetParentId: potentialParentId, existingChildBlocks, containerId: potentialParentId, diff --git a/apps/sim/app/workspace/providers/socket-provider.tsx b/apps/sim/app/workspace/providers/socket-provider.tsx index c4753b384..4e18d14b1 100644 --- a/apps/sim/app/workspace/providers/socket-provider.tsx +++ b/apps/sim/app/workspace/providers/socket-provider.tsx @@ -352,7 +352,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) { }) }) - useWorkflowStore.setState({ + useWorkflowStore.getState().replaceWorkflowState({ blocks: workflowState.blocks || {}, edges: workflowState.edges || [], loops: workflowState.loops || {}, diff --git a/apps/sim/stores/workflows/workflow/store.ts b/apps/sim/stores/workflows/workflow/store.ts index 3fc3438cf..763fd7a4a 100644 --- a/apps/sim/stores/workflows/workflow/store.ts +++ b/apps/sim/stores/workflows/workflow/store.ts @@ -4,9 +4,10 @@ import { create } from 'zustand' import { devtools } from 'zustand/middleware' import { DEFAULT_DUPLICATE_OFFSET } from '@/lib/workflows/autolayout/constants' import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs' +import { TriggerUtils } from '@/lib/workflows/triggers/triggers' import { getBlock } from '@/blocks' import type { SubBlockConfig } from '@/blocks/types' -import { normalizeName, RESERVED_BLOCK_NAMES } from '@/executor/constants' +import { isAnnotationOnlyBlock, normalizeName, RESERVED_BLOCK_NAMES } from '@/executor/constants' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { filterNewEdges, getUniqueBlockName, mergeSubblockState } from '@/stores/workflows/utils' @@ -90,6 +91,26 @@ function resolveInitialSubblockValue(config: SubBlockConfig): unknown { return null } +function isValidEdge( + edge: Edge, + blocks: Record +): boolean { + const sourceBlock = blocks[edge.source] + const targetBlock = blocks[edge.target] + if (!sourceBlock || !targetBlock) return false + if (isAnnotationOnlyBlock(sourceBlock.type)) return false + if (isAnnotationOnlyBlock(targetBlock.type)) return false + if (TriggerUtils.isTriggerBlock(targetBlock)) return false + return true +} + +function filterValidEdges( + edges: Edge[], + blocks: Record +): Edge[] { + return edges.filter((edge) => isValidEdge(edge, blocks)) +} + const initialState = { blocks: {}, edges: [], @@ -360,8 +381,9 @@ export const useWorkflowStore = create()( } if (edges && edges.length > 0) { + const validEdges = filterValidEdges(edges, newBlocks) const existingEdgeIds = new Set(currentEdges.map((e) => e.id)) - for (const edge of edges) { + for (const edge of validEdges) { if (!existingEdgeIds.has(edge.id)) { newEdges.push({ id: edge.id || crypto.randomUUID(), @@ -495,8 +517,11 @@ export const useWorkflowStore = create()( }, batchAddEdges: (edges: Edge[]) => { + const blocks = get().blocks const currentEdges = get().edges - const filtered = filterNewEdges(edges, currentEdges) + + const validEdges = filterValidEdges(edges, blocks) + const filtered = filterNewEdges(validEdges, currentEdges) const newEdges = [...currentEdges] for (const edge of filtered) { @@ -512,7 +537,6 @@ export const useWorkflowStore = create()( }) } - const blocks = get().blocks set({ blocks: { ...blocks }, edges: newEdges, @@ -572,7 +596,7 @@ export const useWorkflowStore = create()( ) => { set((state) => { const nextBlocks = workflowState.blocks || {} - const nextEdges = workflowState.edges || [] + const nextEdges = filterValidEdges(workflowState.edges || [], nextBlocks) const nextLoops = Object.keys(workflowState.loops || {}).length > 0 ? workflowState.loops @@ -1083,7 +1107,7 @@ export const useWorkflowStore = create()( const newState = { blocks: deployedState.blocks, - edges: deployedState.edges, + edges: filterValidEdges(deployedState.edges ?? [], deployedState.blocks), loops: deployedState.loops || {}, parallels: deployedState.parallels || {}, needsRedeployment: false,