From 37dbfe393a79d9ccbf8686f4b543d1b28572e4aa Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 27 Jan 2026 14:17:16 -0800 Subject: [PATCH] fix(workflow): preserve parent and position when duplicating/pasting nested blocks Three related fixes for blocks inside containers (loop/parallel): 1. regenerateBlockIds now preserves parentId when the parent exists in the current workflow, not just when it's in the copy set. This keeps duplicated blocks inside their container. 2. calculatePasteOffset now uses simple offset for nested blocks instead of viewport-center calculation. Since nested blocks use relative positioning, the viewport-center offset would place them incorrectly. 3. Use CONTAINER_DIMENSIONS constants instead of hardcoded magic numbers in orphan cleanup position calculation. --- .../[workspaceId]/w/[workflowId]/workflow.tsx | 30 +++++++++++++++---- apps/sim/stores/workflows/workflow/store.ts | 6 +++- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 6117a9334..348a34ae4 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -99,19 +99,33 @@ const logger = createLogger('Workflow') const DEFAULT_PASTE_OFFSET = { x: 50, y: 50 } /** - * Calculates the offset to paste blocks at viewport center + * Calculates the offset to paste blocks at viewport center, or simple offset for nested blocks */ function calculatePasteOffset( clipboard: { - blocks: Record + blocks: Record< + string, + { + position: { x: number; y: number } + type: string + height?: number + data?: { parentId?: string } + } + > } | null, - viewportCenter: { x: number; y: number } + viewportCenter: { x: number; y: number }, + existingBlocks: Record = {} ): { x: number; y: number } { if (!clipboard) return DEFAULT_PASTE_OFFSET const clipboardBlocks = Object.values(clipboard.blocks) if (clipboardBlocks.length === 0) return DEFAULT_PASTE_OFFSET + const allBlocksNested = clipboardBlocks.every( + (b) => b.data?.parentId && existingBlocks[b.data.parentId] + ) + if (allBlocksNested) return DEFAULT_PASTE_OFFSET + const minX = Math.min(...clipboardBlocks.map((b) => b.position.x)) const maxX = Math.max( ...clipboardBlocks.map((b) => { @@ -449,7 +463,6 @@ const WorkflowContent = React.memo(() => { /** Re-applies diff markers when blocks change after socket rehydration. */ const diffBlocksRef = useRef(blocks) useEffect(() => { - // Track if blocks actually changed (vs other deps triggering this effect) const blocksChanged = blocks !== diffBlocksRef.current diffBlocksRef.current = blocks @@ -1024,7 +1037,7 @@ const WorkflowContent = React.memo(() => { executePasteOperation( 'paste', - calculatePasteOffset(clipboard, getViewportCenter()), + calculatePasteOffset(clipboard, getViewportCenter(), blocks), targetContainer, flowPosition // Pass the click position so blocks are centered at where user right-clicked ) @@ -1036,6 +1049,7 @@ const WorkflowContent = React.memo(() => { screenToFlowPosition, contextMenuPosition, isPointInLoopNode, + blocks, ]) const handleContextDuplicate = useCallback(() => { @@ -1146,7 +1160,10 @@ const WorkflowContent = React.memo(() => { } else if ((event.ctrlKey || event.metaKey) && event.key === 'v') { if (effectivePermissions.canEdit && hasClipboard()) { event.preventDefault() - executePasteOperation('paste', calculatePasteOffset(clipboard, getViewportCenter())) + executePasteOperation( + 'paste', + calculatePasteOffset(clipboard, getViewportCenter(), blocks) + ) } } } @@ -1168,6 +1185,7 @@ const WorkflowContent = React.memo(() => { clipboard, getViewportCenter, executePasteOperation, + blocks, ]) /** diff --git a/apps/sim/stores/workflows/workflow/store.ts b/apps/sim/stores/workflows/workflow/store.ts index 06025b885..98048c7ab 100644 --- a/apps/sim/stores/workflows/workflow/store.ts +++ b/apps/sim/stores/workflows/workflow/store.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import type { Edge } from 'reactflow' import { create } from 'zustand' import { devtools } from 'zustand/middleware' +import { CONTAINER_DIMENSIONS } from '@/lib/workflows/blocks/block-dimensions' import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs' import { getBlock } from '@/blocks' import type { SubBlockConfig } from '@/blocks/types' @@ -446,7 +447,10 @@ export const useWorkflowStore = create()( // Clean up orphaned nodes - blocks whose parent was removed but weren't descendants // This can happen in edge cases (e.g., data inconsistency, external modifications) const remainingBlockIds = new Set(Object.keys(newBlocks)) - const CONTAINER_OFFSET = { x: 16, y: 50 + 16 } // leftPadding, headerHeight + topPadding + const CONTAINER_OFFSET = { + x: CONTAINER_DIMENSIONS.LEFT_PADDING, + y: CONTAINER_DIMENSIONS.HEADER_HEIGHT + CONTAINER_DIMENSIONS.TOP_PADDING, + } Object.entries(newBlocks).forEach(([blockId, block]) => { const parentId = block.data?.parentId