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.
This commit is contained in:
waleed
2026-01-27 14:17:16 -08:00
parent 503f676910
commit 37dbfe393a
2 changed files with 29 additions and 7 deletions

View File

@@ -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<string, { position: { x: number; y: number }; type: string; height?: number }>
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<string, { id: string }> = {}
): { 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,
])
/**

View File

@@ -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<WorkflowStore>()(
// 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