Skip to content

Commit d34664d

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 d34664d

File tree

2 files changed

+29
-6
lines changed

2 files changed

+29
-6
lines changed

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

Lines changed: 24 additions & 5 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) => {
@@ -1024,7 +1038,7 @@ const WorkflowContent = React.memo(() => {
10241038

10251039
executePasteOperation(
10261040
'paste',
1027-
calculatePasteOffset(clipboard, getViewportCenter()),
1041+
calculatePasteOffset(clipboard, getViewportCenter(), blocks),
10281042
targetContainer,
10291043
flowPosition // Pass the click position so blocks are centered at where user right-clicked
10301044
)
@@ -1036,6 +1050,7 @@ const WorkflowContent = React.memo(() => {
10361050
screenToFlowPosition,
10371051
contextMenuPosition,
10381052
isPointInLoopNode,
1053+
blocks,
10391054
])
10401055

10411056
const handleContextDuplicate = useCallback(() => {
@@ -1146,7 +1161,10 @@ const WorkflowContent = React.memo(() => {
11461161
} else if ((event.ctrlKey || event.metaKey) && event.key === 'v') {
11471162
if (effectivePermissions.canEdit && hasClipboard()) {
11481163
event.preventDefault()
1149-
executePasteOperation('paste', calculatePasteOffset(clipboard, getViewportCenter()))
1164+
executePasteOperation(
1165+
'paste',
1166+
calculatePasteOffset(clipboard, getViewportCenter(), blocks)
1167+
)
11501168
}
11511169
}
11521170
}
@@ -1168,6 +1186,7 @@ const WorkflowContent = React.memo(() => {
11681186
clipboard,
11691187
getViewportCenter,
11701188
executePasteOperation,
1189+
blocks,
11711190
])
11721191

11731192
/**

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)