diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 59e56a16b6..374340af82 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -201,8 +201,6 @@ const edgeTypes: EdgeTypes = { const defaultEdgeOptions = { type: 'custom' } const reactFlowStyles = [ - '[&_.react-flow__edges]:!z-0', - '[&_.react-flow__node]:z-[21]', '[&_.react-flow__handle]:!z-[30]', '[&_.react-flow__edge-labels]:!z-[1001]', '[&_.react-flow__pane]:select-none', @@ -2478,6 +2476,7 @@ const WorkflowContent = React.memo( // Local state for nodes - allows smooth drag without store updates on every frame const [displayNodes, setDisplayNodes] = useState([]) + const [lastInteractedNodeId, setLastInteractedNodeId] = useState(null) const selectedNodeIds = useMemo( () => displayNodes.filter((node) => node.selected).map((node) => node.id), @@ -2489,6 +2488,14 @@ const WorkflowContent = React.memo( syncPanelWithSelection(selectedNodeIds) }, [selectedNodeIdsKey]) + // Keep the most recently selected block on top even after deselection, so a + // dragged block doesn't suddenly drop behind other overlapping blocks. + useEffect(() => { + if (selectedNodeIds.length > 0) { + setLastInteractedNodeId(selectedNodeIds[selectedNodeIds.length - 1]) + } + }, [selectedNodeIdsKey]) + useEffect(() => { // Check for pending selection (from paste/duplicate), otherwise preserve existing selection if (pendingSelection && pendingSelection.length > 0) { @@ -3723,18 +3730,58 @@ const WorkflowContent = React.memo( [removeEdge, edges, blocks, addNotification, activeWorkflowId] ) + // Elevate nodes using React Flow's native zIndex so selected/recent blocks + // always sit above edges and other blocks. + // + // Z-index layers (regular blocks): + // 21 — default + // 22 — last interacted (dragged/selected, now deselected) so it stays on + // top of siblings until another block is touched + // 31 — currently selected (above connected edges at z-22 and handles at z-30) + // + // Subflow container nodes are skipped — they use depth-based zIndex for + // correct parent/child layering and must not be bumped. + // Child blocks inside containers already carry zIndex 1000 and are bumped by + // +10 when selected so they stay above their sibling child blocks. + const nodesForRender = useMemo(() => { + return displayNodes.map((node) => { + if (node.type === 'subflowNode') return node + const base = node.zIndex ?? 21 + const target = node.selected + ? base + 10 + : node.id === lastInteractedNodeId + ? Math.max(base + 1, 22) + : base + if (target === (node.zIndex ?? 21)) return node + return { ...node, zIndex: target } + }) + }, [displayNodes, lastInteractedNodeId]) + /** Transforms edges to include selection state and delete handlers. Memoized to prevent re-renders. */ const edgesWithSelection = useMemo(() => { const nodeMap = new Map(displayNodes.map((n) => [n.id, n])) + const elevatedNodeIdSet = new Set( + lastInteractedNodeId ? [...selectedNodeIds, lastInteractedNodeId] : selectedNodeIds + ) return edgesForDisplay.map((edge) => { const sourceNode = nodeMap.get(edge.source) const targetNode = nodeMap.get(edge.target) const parentLoopId = sourceNode?.parentId || targetNode?.parentId const edgeContextId = `${edge.id}${parentLoopId ? `-${parentLoopId}` : ''}` + const connectedToElevated = + elevatedNodeIdSet.has(edge.source) || elevatedNodeIdSet.has(edge.target) + // Derive elevated z-index from connected nodes so edges inside subflows + // (child nodes at z-1000) stay above their sibling child blocks. + const elevatedZIndex = Math.max( + 22, + (sourceNode?.zIndex ?? 21) + 1, + (targetNode?.zIndex ?? 21) + 1 + ) return { ...edge, + zIndex: connectedToElevated ? elevatedZIndex : 0, data: { ...edge.data, isSelected: selectedEdges.has(edgeContextId), @@ -3745,7 +3792,14 @@ const WorkflowContent = React.memo( }, } }) - }, [edgesForDisplay, displayNodes, selectedEdges, handleEdgeDelete]) + }, [ + edgesForDisplay, + displayNodes, + selectedNodeIds, + selectedEdges, + handleEdgeDelete, + lastInteractedNodeId, + ]) /** Handles Delete/Backspace to remove selected edges or blocks. */ useEffect(() => { @@ -3885,7 +3939,7 @@ const WorkflowContent = React.memo( {showTrainingModal && }