From 5e1c83883bd53e6d6d078bc998d638ae56c1657e Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 10 Jan 2026 21:54:59 -0800 Subject: [PATCH 1/4] fix(resize): fix subflow resize on drag, children deselected in subflow on drag --- .../w/[workflowId]/hooks/index.ts | 3 +- .../[workflowId]/hooks/use-node-utilities.ts | 79 +++++++++++------- .../utils/workflow-canvas-helpers.ts | 34 +++++++- .../[workspaceId]/w/[workflowId]/workflow.tsx | 83 ++++++++++--------- 4 files changed, 127 insertions(+), 72 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/index.ts index 3af268aa37..2e1b90a6a6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/index.ts @@ -4,6 +4,7 @@ export { computeParentUpdateEntries, getClampedPositionForNode, isInEditableElement, + resolveParentChildSelectionConflicts, selectNodesDeferred, validateTriggerPaste, } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-canvas-helpers' @@ -12,7 +13,7 @@ export { useAutoLayout } from './use-auto-layout' export { BLOCK_DIMENSIONS, useBlockDimensions } from './use-block-dimensions' export { useBlockVisual } from './use-block-visual' export { type CurrentWorkflow, useCurrentWorkflow } from './use-current-workflow' -export { useNodeUtilities } from './use-node-utilities' +export { calculateContainerDimensions, useNodeUtilities } from './use-node-utilities' export { usePreventZoom } from './use-prevent-zoom' export { useScrollManagement } from './use-scroll-management' export { useWorkflowExecution } from './use-workflow-execution' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities.ts index d90317ea7e..e137506618 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities.ts @@ -62,6 +62,47 @@ export function clampPositionToContainer( } } +/** + * Calculates container dimensions based on child block positions. + * Single source of truth for container sizing - ensures consistency between + * live drag updates and final dimension calculations. + * + * @param childPositions - Array of child positions with their dimensions + * @returns Calculated width and height for the container + */ +export function calculateContainerDimensions( + childPositions: Array<{ x: number; y: number; width: number; height: number }> +): { width: number; height: number } { + if (childPositions.length === 0) { + return { + width: CONTAINER_DIMENSIONS.DEFAULT_WIDTH, + height: CONTAINER_DIMENSIONS.DEFAULT_HEIGHT, + } + } + + let maxRight = 0 + let maxBottom = 0 + + for (const child of childPositions) { + maxRight = Math.max(maxRight, child.x + child.width) + maxBottom = Math.max(maxBottom, child.y + child.height) + } + + const width = Math.max( + CONTAINER_DIMENSIONS.DEFAULT_WIDTH, + CONTAINER_DIMENSIONS.LEFT_PADDING + maxRight + CONTAINER_DIMENSIONS.RIGHT_PADDING + ) + const height = Math.max( + CONTAINER_DIMENSIONS.DEFAULT_HEIGHT, + CONTAINER_DIMENSIONS.HEADER_HEIGHT + + CONTAINER_DIMENSIONS.TOP_PADDING + + maxBottom + + CONTAINER_DIMENSIONS.BOTTOM_PADDING + ) + + return { width, height } +} + /** * Hook providing utilities for node position, hierarchy, and dimension calculations */ @@ -306,36 +347,16 @@ export function useNodeUtilities(blocks: Record) { (id) => currentBlocks[id]?.data?.parentId === nodeId ) - if (childBlockIds.length === 0) { - return { - width: CONTAINER_DIMENSIONS.DEFAULT_WIDTH, - height: CONTAINER_DIMENSIONS.DEFAULT_HEIGHT, - } - } - - let maxRight = 0 - let maxBottom = 0 - - for (const childId of childBlockIds) { - const child = currentBlocks[childId] - if (!child?.position) continue - - const { width: childWidth, height: childHeight } = getBlockDimensions(childId) - - maxRight = Math.max(maxRight, child.position.x + childWidth) - maxBottom = Math.max(maxBottom, child.position.y + childHeight) - } - - const width = Math.max( - CONTAINER_DIMENSIONS.DEFAULT_WIDTH, - maxRight + CONTAINER_DIMENSIONS.RIGHT_PADDING - ) - const height = Math.max( - CONTAINER_DIMENSIONS.DEFAULT_HEIGHT, - maxBottom + CONTAINER_DIMENSIONS.BOTTOM_PADDING - ) + const childPositions = childBlockIds + .map((childId) => { + const child = currentBlocks[childId] + if (!child?.position) return null + const { width, height } = getBlockDimensions(childId) + return { x: child.position.x, y: child.position.y, width, height } + }) + .filter((p): p is NonNullable => p !== null) - return { width, height } + return calculateContainerDimensions(childPositions) }, [getBlockDimensions] ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-canvas-helpers.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-canvas-helpers.ts index 4f968febca..4a04dc4878 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-canvas-helpers.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-canvas-helpers.ts @@ -70,19 +70,22 @@ export function clearDragHighlights(): void { * Defers selection to next animation frame to allow displayNodes to sync from store first. * This is necessary because the component uses controlled state (nodes={displayNodes}) * and newly added blocks need time to propagate through the store → derivedNodes → displayNodes cycle. + * Automatically resolves parent-child selection conflicts to prevent wiggle during drag. */ export function selectNodesDeferred( nodeIds: string[], - setDisplayNodes: (updater: (nodes: Node[]) => Node[]) => void + setDisplayNodes: (updater: (nodes: Node[]) => Node[]) => void, + blocks: Record ): void { const idsSet = new Set(nodeIds) requestAnimationFrame(() => { - setDisplayNodes((nodes) => - nodes.map((node) => ({ + setDisplayNodes((nodes) => { + const withSelection = nodes.map((node) => ({ ...node, selected: idsSet.has(node.id), })) - ) + return resolveParentChildSelectionConflicts(withSelection, blocks) + }) }) } @@ -186,3 +189,26 @@ export function computeParentUpdateEntries( } }) } + +/** + * Resolves parent-child selection conflicts by deselecting children whose parent is also selected. + */ +export function resolveParentChildSelectionConflicts( + nodes: Node[], + blocks: Record +): Node[] { + const selectedIds = new Set(nodes.filter((n) => n.selected).map((n) => n.id)) + + let hasConflict = false + const resolved = nodes.map((n) => { + if (!n.selected) return n + const parentId = n.parentId || n.data?.parentId || blocks[n.id]?.data?.parentId + if (parentId && selectedIds.has(parentId)) { + hasConflict = true + return { ...n, selected: false } + } + return n + }) + + return hasConflict ? resolved : nodes +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 03764beace..af28eecfce 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -47,6 +47,7 @@ import { computeClampedPositionUpdates, getClampedPositionForNode, isInEditableElement, + resolveParentChildSelectionConflicts, selectNodesDeferred, useAutoLayout, useCurrentWorkflow, @@ -55,6 +56,7 @@ import { } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks' import { useCanvasContextMenu } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-canvas-context-menu' import { + calculateContainerDimensions, clampPositionToContainer, estimateBlockDimensions, } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities' @@ -697,7 +699,8 @@ const WorkflowContent = React.memo(() => { selectNodesDeferred( pastedBlocksArray.map((b) => b.id), - setDisplayNodes + setDisplayNodes, + blocks ) }, [ hasClipboard, @@ -745,7 +748,8 @@ const WorkflowContent = React.memo(() => { selectNodesDeferred( pastedBlocksArray.map((b) => b.id), - setDisplayNodes + setDisplayNodes, + blocks ) }, [ contextMenuBlocks, @@ -890,7 +894,8 @@ const WorkflowContent = React.memo(() => { selectNodesDeferred( pastedBlocks.map((b) => b.id), - setDisplayNodes + setDisplayNodes, + blocks ) } } @@ -2037,10 +2042,19 @@ const WorkflowContent = React.memo(() => { window.removeEventListener('remove-from-subflow', handleRemoveFromSubflow as EventListener) }, [blocks, edgesForDisplay, getNodeAbsolutePosition, collaborativeBatchUpdateParent]) - /** Handles node position changes - updates local state for smooth drag, syncs to store only on drag end. */ - const onNodesChange = useCallback((changes: NodeChange[]) => { - setDisplayNodes((nds) => applyNodeChanges(changes, nds)) - }, []) + /** Handles node changes - applies changes and resolves parent-child selection conflicts. */ + const onNodesChange = useCallback( + (changes: NodeChange[]) => { + setDisplayNodes((nds) => { + const updated = applyNodeChanges(changes, nds) + const hasSelectionChange = changes.some( + (c) => c.type === 'select' && (c as { selected?: boolean }).selected + ) + return hasSelectionChange ? resolveParentChildSelectionConflicts(updated, blocks) : updated + }) + }, + [blocks] + ) /** * Updates container dimensions in displayNodes during drag. @@ -2055,28 +2069,13 @@ const WorkflowContent = React.memo(() => { const childNodes = currentNodes.filter((n) => n.parentId === parentId) if (childNodes.length === 0) return currentNodes - let maxRight = 0 - let maxBottom = 0 - - childNodes.forEach((node) => { + const childPositions = childNodes.map((node) => { const nodePosition = node.id === draggedNodeId ? draggedNodePosition : node.position - const { width: nodeWidth, height: nodeHeight } = getBlockDimensions(node.id) - - maxRight = Math.max(maxRight, nodePosition.x + nodeWidth) - maxBottom = Math.max(maxBottom, nodePosition.y + nodeHeight) + const { width, height } = getBlockDimensions(node.id) + return { x: nodePosition.x, y: nodePosition.y, width, height } }) - const newWidth = Math.max( - CONTAINER_DIMENSIONS.DEFAULT_WIDTH, - CONTAINER_DIMENSIONS.LEFT_PADDING + maxRight + CONTAINER_DIMENSIONS.RIGHT_PADDING - ) - const newHeight = Math.max( - CONTAINER_DIMENSIONS.DEFAULT_HEIGHT, - CONTAINER_DIMENSIONS.HEADER_HEIGHT + - CONTAINER_DIMENSIONS.TOP_PADDING + - maxBottom + - CONTAINER_DIMENSIONS.BOTTOM_PADDING - ) + const { width: newWidth, height: newHeight } = calculateContainerDimensions(childPositions) return currentNodes.map((node) => { if (node.id === parentId) { @@ -2844,27 +2843,38 @@ const WorkflowContent = React.memo(() => { }, [isShiftPressed]) const onSelectionEnd = useCallback(() => { - requestAnimationFrame(() => setIsSelectionDragActive(false)) - }, []) + requestAnimationFrame(() => { + setIsSelectionDragActive(false) + setDisplayNodes((nodes) => resolveParentChildSelectionConflicts(nodes, blocks)) + }) + }, [blocks]) /** Captures initial positions when selection drag starts (for marquee-selected nodes). */ const onSelectionDragStart = useCallback( (_event: React.MouseEvent, nodes: Node[]) => { - // Capture the parent ID of the first node as reference (they should all be in the same context) if (nodes.length > 0) { const firstNodeParentId = blocks[nodes[0].id]?.data?.parentId || null setDragStartParentId(firstNodeParentId) } - // Capture all selected nodes' positions for undo/redo + // Resolve parent-child conflicts and capture positions for undo/redo + setDisplayNodes((allNodes) => resolveParentChildSelectionConflicts(allNodes, blocks)) + + // Filter to nodes that won't be deselected (exclude children whose parent is selected) + const nodeIds = new Set(nodes.map((n) => n.id)) + const effectiveNodes = nodes.filter((n) => { + const parentId = blocks[n.id]?.data?.parentId + return !parentId || !nodeIds.has(parentId) + }) + multiNodeDragStartRef.current.clear() - nodes.forEach((n) => { - const block = blocks[n.id] - if (block) { + effectiveNodes.forEach((n) => { + const blk = blocks[n.id] + if (blk) { multiNodeDragStartRef.current.set(n.id, { x: n.position.x, y: n.position.y, - parentId: block.data?.parentId, + parentId: blk.data?.parentId, }) } }) @@ -2903,7 +2913,6 @@ const WorkflowContent = React.memo(() => { eligibleNodes.forEach((node) => { const absolutePos = getNodeAbsolutePosition(node.id) - const block = blocks[node.id] const width = BLOCK_DIMENSIONS.FIXED_WIDTH const height = Math.max( node.height || BLOCK_DIMENSIONS.MIN_HEIGHT, @@ -3129,13 +3138,11 @@ const WorkflowContent = React.memo(() => { /** * Handles node click to select the node in ReactFlow. - * This ensures clicking anywhere on a block (not just the drag handle) - * selects it for delete/backspace and multi-select operations. + * Parent-child conflict resolution happens automatically in onNodesChange. */ const handleNodeClick = useCallback( (event: React.MouseEvent, node: Node) => { const isMultiSelect = event.shiftKey || event.metaKey || event.ctrlKey - setNodes((nodes) => nodes.map((n) => ({ ...n, From 28943eb11d52f589d391c2118ac81cf377198a0e Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 10 Jan 2026 22:04:41 -0800 Subject: [PATCH 2/4] ack PR comments --- .../w/[workflowId]/utils/workflow-canvas-helpers.ts | 2 +- .../[workspaceId]/w/[workflowId]/workflow.tsx | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-canvas-helpers.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-canvas-helpers.ts index 4a04dc4878..4fd6efd512 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-canvas-helpers.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-canvas-helpers.ts @@ -202,7 +202,7 @@ export function resolveParentChildSelectionConflicts( let hasConflict = false const resolved = nodes.map((n) => { if (!n.selected) return n - const parentId = n.parentId || n.data?.parentId || blocks[n.id]?.data?.parentId + const parentId = n.parentId || blocks[n.id]?.data?.parentId if (parentId && selectedIds.has(parentId)) { hasConflict = true return { ...n, selected: false } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index af28eecfce..3ec5875b59 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -2047,9 +2047,7 @@ const WorkflowContent = React.memo(() => { (changes: NodeChange[]) => { setDisplayNodes((nds) => { const updated = applyNodeChanges(changes, nds) - const hasSelectionChange = changes.some( - (c) => c.type === 'select' && (c as { selected?: boolean }).selected - ) + const hasSelectionChange = changes.some((c) => c.type === 'select') return hasSelectionChange ? resolveParentChildSelectionConflicts(updated, blocks) : updated }) }, @@ -2857,9 +2855,6 @@ const WorkflowContent = React.memo(() => { setDragStartParentId(firstNodeParentId) } - // Resolve parent-child conflicts and capture positions for undo/redo - setDisplayNodes((allNodes) => resolveParentChildSelectionConflicts(allNodes, blocks)) - // Filter to nodes that won't be deselected (exclude children whose parent is selected) const nodeIds = new Set(nodes.map((n) => n.id)) const effectiveNodes = nodes.filter((n) => { @@ -2867,6 +2862,7 @@ const WorkflowContent = React.memo(() => { return !parentId || !nodeIds.has(parentId) }) + // Capture positions for undo/redo before applying display changes multiNodeDragStartRef.current.clear() effectiveNodes.forEach((n) => { const blk = blocks[n.id] @@ -2878,6 +2874,9 @@ const WorkflowContent = React.memo(() => { }) } }) + + // Apply visual deselection of children + setDisplayNodes((allNodes) => resolveParentChildSelectionConflicts(allNodes, blocks)) }, [blocks] ) From 80ede82ee19521a1298eda52e01c61deac916141 Mon Sep 17 00:00:00 2001 From: waleed Date: Sun, 11 Jan 2026 11:18:40 -0800 Subject: [PATCH 3/4] fix copy-paste subflows deselecting children --- .../w/[workflowId]/hooks/index.ts | 1 - .../utils/workflow-canvas-helpers.ts | 24 ---------- .../[workspaceId]/w/[workflowId]/workflow.tsx | 47 ++++++++++--------- 3 files changed, 26 insertions(+), 46 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/index.ts index 2e1b90a6a6..b80f7749a8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/index.ts @@ -5,7 +5,6 @@ export { getClampedPositionForNode, isInEditableElement, resolveParentChildSelectionConflicts, - selectNodesDeferred, validateTriggerPaste, } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-canvas-helpers' export { useFloatBoundarySync, useFloatDrag, useFloatResize } from './float' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-canvas-helpers.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-canvas-helpers.ts index 4fd6efd512..a6d6ea9136 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-canvas-helpers.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-canvas-helpers.ts @@ -65,30 +65,6 @@ export function clearDragHighlights(): void { document.body.style.cursor = '' } -/** - * Selects nodes by their IDs after paste/duplicate operations. - * Defers selection to next animation frame to allow displayNodes to sync from store first. - * This is necessary because the component uses controlled state (nodes={displayNodes}) - * and newly added blocks need time to propagate through the store → derivedNodes → displayNodes cycle. - * Automatically resolves parent-child selection conflicts to prevent wiggle during drag. - */ -export function selectNodesDeferred( - nodeIds: string[], - setDisplayNodes: (updater: (nodes: Node[]) => Node[]) => void, - blocks: Record -): void { - const idsSet = new Set(nodeIds) - requestAnimationFrame(() => { - setDisplayNodes((nodes) => { - const withSelection = nodes.map((node) => ({ - ...node, - selected: idsSet.has(node.id), - })) - return resolveParentChildSelectionConflicts(withSelection, blocks) - }) - }) -} - interface BlockData { height?: number data?: { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 3ec5875b59..08f635ce15 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -48,7 +48,6 @@ import { getClampedPositionForNode, isInEditableElement, resolveParentChildSelectionConflicts, - selectNodesDeferred, useAutoLayout, useCurrentWorkflow, useNodeUtilities, @@ -358,6 +357,9 @@ const WorkflowContent = React.memo(() => { new Map() ) + /** Stores node IDs to select on next derivedNodes sync (for paste/duplicate operations). */ + const pendingSelectionRef = useRef | null>(null) + /** Re-applies diff markers when blocks change after socket rehydration. */ const blocksRef = useRef(blocks) useEffect(() => { @@ -689,6 +691,9 @@ const WorkflowContent = React.memo(() => { return } + // Set pending selection before adding blocks - sync effect will apply it + pendingSelectionRef.current = new Set(pastedBlocksArray.map((b) => b.id)) + collaborativeBatchAddBlocks( pastedBlocksArray, pastedEdges, @@ -696,12 +701,6 @@ const WorkflowContent = React.memo(() => { pastedParallels, pastedSubBlockValues ) - - selectNodesDeferred( - pastedBlocksArray.map((b) => b.id), - setDisplayNodes, - blocks - ) }, [ hasClipboard, clipboard, @@ -738,6 +737,9 @@ const WorkflowContent = React.memo(() => { return } + // Set pending selection before adding blocks - sync effect will apply it + pendingSelectionRef.current = new Set(pastedBlocksArray.map((b) => b.id)) + collaborativeBatchAddBlocks( pastedBlocksArray, pastedEdges, @@ -745,12 +747,6 @@ const WorkflowContent = React.memo(() => { pastedParallels, pastedSubBlockValues ) - - selectNodesDeferred( - pastedBlocksArray.map((b) => b.id), - setDisplayNodes, - blocks - ) }, [ contextMenuBlocks, copyBlocks, @@ -884,6 +880,9 @@ const WorkflowContent = React.memo(() => { return } + // Set pending selection before adding blocks - sync effect will apply it + pendingSelectionRef.current = new Set(pastedBlocks.map((b) => b.id)) + collaborativeBatchAddBlocks( pastedBlocks, pasteData.edges, @@ -891,12 +890,6 @@ const WorkflowContent = React.memo(() => { pasteData.parallels, pasteData.subBlockValues ) - - selectNodesDeferred( - pastedBlocks.map((b) => b.id), - setDisplayNodes, - blocks - ) } } } @@ -1959,15 +1952,27 @@ const WorkflowContent = React.memo(() => { }, [isShiftPressed]) useEffect(() => { - // Preserve selection state when syncing from derivedNodes + // Check for pending selection (from paste/duplicate), otherwise preserve existing selection + const pendingSelection = pendingSelectionRef.current + pendingSelectionRef.current = null + setDisplayNodes((currentNodes) => { + if (pendingSelection) { + // Apply pending selection and resolve parent-child conflicts + const withSelection = derivedNodes.map((node) => ({ + ...node, + selected: pendingSelection.has(node.id), + })) + return resolveParentChildSelectionConflicts(withSelection, blocks) + } + // Preserve existing selection state const selectedIds = new Set(currentNodes.filter((n) => n.selected).map((n) => n.id)) return derivedNodes.map((node) => ({ ...node, selected: selectedIds.has(node.id), })) }) - }, [derivedNodes]) + }, [derivedNodes, blocks]) /** Handles ActionBar remove-from-subflow events. */ useEffect(() => { From c738a71b74561bb2c93396721da916a9c2cce43a Mon Sep 17 00:00:00 2001 From: waleed Date: Sun, 11 Jan 2026 11:27:47 -0800 Subject: [PATCH 4/4] ack comments --- .../[workspaceId]/w/[workflowId]/workflow.tsx | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 08f635ce15..bbaad51aa8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -691,8 +691,11 @@ const WorkflowContent = React.memo(() => { return } - // Set pending selection before adding blocks - sync effect will apply it - pendingSelectionRef.current = new Set(pastedBlocksArray.map((b) => b.id)) + // Set pending selection before adding blocks - sync effect will apply it (accumulates for rapid pastes) + pendingSelectionRef.current = new Set([ + ...(pendingSelectionRef.current ?? []), + ...pastedBlocksArray.map((b) => b.id), + ]) collaborativeBatchAddBlocks( pastedBlocksArray, @@ -737,8 +740,11 @@ const WorkflowContent = React.memo(() => { return } - // Set pending selection before adding blocks - sync effect will apply it - pendingSelectionRef.current = new Set(pastedBlocksArray.map((b) => b.id)) + // Set pending selection before adding blocks - sync effect will apply it (accumulates for rapid pastes) + pendingSelectionRef.current = new Set([ + ...(pendingSelectionRef.current ?? []), + ...pastedBlocksArray.map((b) => b.id), + ]) collaborativeBatchAddBlocks( pastedBlocksArray, @@ -880,8 +886,11 @@ const WorkflowContent = React.memo(() => { return } - // Set pending selection before adding blocks - sync effect will apply it - pendingSelectionRef.current = new Set(pastedBlocks.map((b) => b.id)) + // Set pending selection before adding blocks - sync effect will apply it (accumulates for rapid pastes) + pendingSelectionRef.current = new Set([ + ...(pendingSelectionRef.current ?? []), + ...pastedBlocks.map((b) => b.id), + ]) collaborativeBatchAddBlocks( pastedBlocks,