Skip to content

Commit 612cf8d

Browse files
committed
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.
1 parent f391a03 commit 612cf8d

File tree

3 files changed

+46
-13
lines changed

3 files changed

+46
-13
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,19 +120,33 @@ function getViewportCenter(
120120
}
121121

122122
/**
123-
* Calculates the offset to paste blocks at viewport center
123+
* Calculates the offset to paste blocks at viewport center, or simple offset for nested blocks
124124
*/
125125
function calculatePasteOffset(
126126
clipboard: {
127-
blocks: Record<string, { position: { x: number; y: number }; type: string; height?: number }>
127+
blocks: Record<
128+
string,
129+
{
130+
position: { x: number; y: number }
131+
type: string
132+
height?: number
133+
data?: { parentId?: string }
134+
}
135+
>
128136
} | null,
129-
screenToFlowPosition: (pos: { x: number; y: number }) => { x: number; y: number }
137+
screenToFlowPosition: (pos: { x: number; y: number }) => { x: number; y: number },
138+
existingBlocks: Record<string, { id: string }> = {}
130139
): { x: number; y: number } {
131140
if (!clipboard) return DEFAULT_PASTE_OFFSET
132141

133142
const clipboardBlocks = Object.values(clipboard.blocks)
134143
if (clipboardBlocks.length === 0) return DEFAULT_PASTE_OFFSET
135144

145+
const allBlocksNested = clipboardBlocks.every(
146+
(b) => b.data?.parentId && existingBlocks[b.data.parentId]
147+
)
148+
if (allBlocksNested) return DEFAULT_PASTE_OFFSET
149+
136150
const minX = Math.min(...clipboardBlocks.map((b) => b.position.x))
137151
const maxX = Math.max(
138152
...clipboardBlocks.map((b) => {
@@ -921,8 +935,8 @@ const WorkflowContent = React.memo(() => {
921935

922936
const handleContextPaste = useCallback(() => {
923937
if (!hasClipboard()) return
924-
executePasteOperation('paste', calculatePasteOffset(clipboard, screenToFlowPosition))
925-
}, [hasClipboard, executePasteOperation, clipboard, screenToFlowPosition])
938+
executePasteOperation('paste', calculatePasteOffset(clipboard, screenToFlowPosition, blocks))
939+
}, [hasClipboard, executePasteOperation, clipboard, screenToFlowPosition, blocks])
926940

927941
const handleContextDuplicate = useCallback(() => {
928942
copyBlocks(contextMenuBlocks.map((b) => b.id))
@@ -1036,7 +1050,10 @@ const WorkflowContent = React.memo(() => {
10361050
} else if ((event.ctrlKey || event.metaKey) && event.key === 'v') {
10371051
if (effectivePermissions.canEdit && hasClipboard()) {
10381052
event.preventDefault()
1039-
executePasteOperation('paste', calculatePasteOffset(clipboard, screenToFlowPosition))
1053+
executePasteOperation(
1054+
'paste',
1055+
calculatePasteOffset(clipboard, screenToFlowPosition, blocks)
1056+
)
10401057
}
10411058
}
10421059
}
@@ -1058,6 +1075,7 @@ const WorkflowContent = React.memo(() => {
10581075
clipboard,
10591076
screenToFlowPosition,
10601077
executePasteOperation,
1078+
blocks,
10611079
])
10621080

10631081
/**

apps/sim/stores/workflows/utils.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -574,13 +574,14 @@ export function regenerateBlockIds(
574574
const newNormalizedName = normalizeName(newName)
575575
nameMap.set(oldNormalizedName, newNormalizedName)
576576

577-
// Check if this block has a parent that's also being copied
578-
// If so, it's a nested block and should keep its relative position (no offset)
579-
// Only top-level blocks (no parent in the paste set) get the position offset
577+
// Position handling depends on parent relationship:
578+
// 1. Parent in paste set: both get copied, child keeps relative position (no offset - parent also moves)
579+
// 2. Parent in workflow only: duplicate within same container, relative position + offset
580+
// 3. No parent: absolute position + offset
580581
const hasParentInPasteSet = block.data?.parentId && blocks[block.data.parentId]
581582
const newPosition = hasParentInPasteSet
582-
? { x: block.position.x, y: block.position.y } // Keep relative position
583-
: { x: block.position.x + positionOffset.x, y: block.position.y + positionOffset.y }
583+
? { x: block.position.x, y: block.position.y } // Keep relative position unchanged
584+
: { x: block.position.x + positionOffset.x, y: block.position.y + positionOffset.y } // Add offset
584585

585586
// Placeholder block - we'll update parentId in second pass
586587
const newBlock: BlockState = {
@@ -602,19 +603,29 @@ export function regenerateBlockIds(
602603
})
603604

604605
// Second pass: update parentId references for nested blocks
605-
// If a block's parent is also being pasted, map to new parentId; otherwise clear it
606+
// Priority: 1) Parent in paste set -> use new ID, 2) Parent in workflow -> keep original, 3) Clear
606607
Object.entries(newBlocks).forEach(([, block]) => {
607608
if (block.data?.parentId) {
608609
const oldParentId = block.data.parentId
609610
const newParentId = blockIdMap.get(oldParentId)
610611

611612
if (newParentId) {
613+
// Parent is also being copied - use the new parent ID
612614
block.data = {
613615
...block.data,
614616
parentId: newParentId,
615617
extent: 'parent',
616618
}
619+
} else if (existingBlockNames[oldParentId]) {
620+
// Parent exists in current workflow - keep original parentId
621+
// (e.g., duplicating a block inside a container without copying the container)
622+
block.data = {
623+
...block.data,
624+
parentId: oldParentId,
625+
extent: 'parent',
626+
}
617627
} else {
628+
// Parent doesn't exist anywhere - clear the relationship
618629
block.data = { ...block.data, parentId: undefined, extent: undefined }
619630
}
620631
}

apps/sim/stores/workflows/workflow/store.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
22
import type { Edge } from 'reactflow'
33
import { create } from 'zustand'
44
import { devtools } from 'zustand/middleware'
5+
import { CONTAINER_DIMENSIONS } from '@/lib/workflows/blocks/block-dimensions'
56
import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
67
import { getBlock } from '@/blocks'
78
import type { SubBlockConfig } from '@/blocks/types'
@@ -446,7 +447,10 @@ export const useWorkflowStore = create<WorkflowStore>()(
446447
// Clean up orphaned nodes - blocks whose parent was removed but weren't descendants
447448
// This can happen in edge cases (e.g., data inconsistency, external modifications)
448449
const remainingBlockIds = new Set(Object.keys(newBlocks))
449-
const CONTAINER_OFFSET = { x: 16, y: 50 + 16 } // leftPadding, headerHeight + topPadding
450+
const CONTAINER_OFFSET = {
451+
x: CONTAINER_DIMENSIONS.LEFT_PADDING,
452+
y: CONTAINER_DIMENSIONS.HEADER_HEIGHT + CONTAINER_DIMENSIONS.TOP_PADDING,
453+
}
450454

451455
Object.entries(newBlocks).forEach(([blockId, block]) => {
452456
const parentId = block.data?.parentId

0 commit comments

Comments
 (0)