Skip to content

Commit 37dbfe3

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 503f676 commit 37dbfe3

File tree

2 files changed

+29
-7
lines changed

2 files changed

+29
-7
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
@@ -99,19 +99,33 @@ const logger = createLogger('Workflow')
9999
const DEFAULT_PASTE_OFFSET = { x: 50, y: 50 }
100100

101101
/**
102-
* Calculates the offset to paste blocks at viewport center
102+
* Calculates the offset to paste blocks at viewport center, or simple offset for nested blocks
103103
*/
104104
function calculatePasteOffset(
105105
clipboard: {
106-
blocks: Record<string, { position: { x: number; y: number }; type: string; height?: number }>
106+
blocks: Record<
107+
string,
108+
{
109+
position: { x: number; y: number }
110+
type: string
111+
height?: number
112+
data?: { parentId?: string }
113+
}
114+
>
107115
} | null,
108-
viewportCenter: { x: number; y: number }
116+
viewportCenter: { x: number; y: number },
117+
existingBlocks: Record<string, { id: string }> = {}
109118
): { x: number; y: number } {
110119
if (!clipboard) return DEFAULT_PASTE_OFFSET
111120

112121
const clipboardBlocks = Object.values(clipboard.blocks)
113122
if (clipboardBlocks.length === 0) return DEFAULT_PASTE_OFFSET
114123

124+
const allBlocksNested = clipboardBlocks.every(
125+
(b) => b.data?.parentId && existingBlocks[b.data.parentId]
126+
)
127+
if (allBlocksNested) return DEFAULT_PASTE_OFFSET
128+
115129
const minX = Math.min(...clipboardBlocks.map((b) => b.position.x))
116130
const maxX = Math.max(
117131
...clipboardBlocks.map((b) => {
@@ -449,7 +463,6 @@ const WorkflowContent = React.memo(() => {
449463
/** Re-applies diff markers when blocks change after socket rehydration. */
450464
const diffBlocksRef = useRef(blocks)
451465
useEffect(() => {
452-
// Track if blocks actually changed (vs other deps triggering this effect)
453466
const blocksChanged = blocks !== diffBlocksRef.current
454467
diffBlocksRef.current = blocks
455468

@@ -1024,7 +1037,7 @@ const WorkflowContent = React.memo(() => {
10241037

10251038
executePasteOperation(
10261039
'paste',
1027-
calculatePasteOffset(clipboard, getViewportCenter()),
1040+
calculatePasteOffset(clipboard, getViewportCenter(), blocks),
10281041
targetContainer,
10291042
flowPosition // Pass the click position so blocks are centered at where user right-clicked
10301043
)
@@ -1036,6 +1049,7 @@ const WorkflowContent = React.memo(() => {
10361049
screenToFlowPosition,
10371050
contextMenuPosition,
10381051
isPointInLoopNode,
1052+
blocks,
10391053
])
10401054

10411055
const handleContextDuplicate = useCallback(() => {
@@ -1146,7 +1160,10 @@ const WorkflowContent = React.memo(() => {
11461160
} else if ((event.ctrlKey || event.metaKey) && event.key === 'v') {
11471161
if (effectivePermissions.canEdit && hasClipboard()) {
11481162
event.preventDefault()
1149-
executePasteOperation('paste', calculatePasteOffset(clipboard, getViewportCenter()))
1163+
executePasteOperation(
1164+
'paste',
1165+
calculatePasteOffset(clipboard, getViewportCenter(), blocks)
1166+
)
11501167
}
11511168
}
11521169
}
@@ -1168,6 +1185,7 @@ const WorkflowContent = React.memo(() => {
11681185
clipboard,
11691186
getViewportCenter,
11701187
executePasteOperation,
1188+
blocks,
11711189
])
11721190

11731191
/**

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)