From 63eba0f6fb4c89b38972f65ed6ed5550656f0608 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 31 Jan 2026 18:40:21 -0800 Subject: [PATCH] fix(duplicate): place duplicate outside locked container When duplicating a block that's inside a locked loop/parallel, the duplicate is now placed outside the container since nothing should be added to a locked container. Co-Authored-By: Claude Opus 4.5 --- .../stores/workflows/workflow/store.test.ts | 68 +++++++++++++++++++ apps/sim/stores/workflows/workflow/store.ts | 31 ++++++++- 2 files changed, 96 insertions(+), 3 deletions(-) diff --git a/apps/sim/stores/workflows/workflow/store.test.ts b/apps/sim/stores/workflows/workflow/store.test.ts index 3cc98cbd2..d4814dcfd 100644 --- a/apps/sim/stores/workflows/workflow/store.test.ts +++ b/apps/sim/stores/workflows/workflow/store.test.ts @@ -1291,6 +1291,74 @@ describe('workflow store', () => { expect(blocks[duplicatedId].locked).toBeFalsy() } }) + + it('should place duplicate outside locked container when duplicating block inside locked loop', () => { + const { batchToggleLocked, duplicateBlock } = useWorkflowStore.getState() + + // Create a loop with a child block + addBlock('loop-1', 'loop', 'My Loop', { x: 0, y: 0 }, { loopType: 'for', count: 3 }) + addBlock( + 'child-1', + 'function', + 'Child', + { x: 50, y: 50 }, + { parentId: 'loop-1' }, + 'loop-1', + 'parent' + ) + + // Lock the loop (which cascades to the child) + batchToggleLocked(['loop-1']) + expect(useWorkflowStore.getState().blocks['child-1'].locked).toBe(true) + + // Duplicate the child block + duplicateBlock('child-1') + + const { blocks } = useWorkflowStore.getState() + const blockIds = Object.keys(blocks) + + expect(blockIds.length).toBe(3) // loop, original child, duplicate + + const duplicatedId = blockIds.find((id) => id !== 'loop-1' && id !== 'child-1') + expect(duplicatedId).toBeDefined() + + if (duplicatedId) { + // Duplicate should be unlocked + expect(blocks[duplicatedId].locked).toBe(false) + // Duplicate should NOT have a parentId (placed outside the locked container) + expect(blocks[duplicatedId].data?.parentId).toBeUndefined() + // Original should still be inside the loop + expect(blocks['child-1'].data?.parentId).toBe('loop-1') + } + }) + + it('should keep duplicate inside unlocked container when duplicating block inside unlocked loop', () => { + const { duplicateBlock } = useWorkflowStore.getState() + + // Create a loop with a child block (not locked) + addBlock('loop-1', 'loop', 'My Loop', { x: 0, y: 0 }, { loopType: 'for', count: 3 }) + addBlock( + 'child-1', + 'function', + 'Child', + { x: 50, y: 50 }, + { parentId: 'loop-1' }, + 'loop-1', + 'parent' + ) + + // Duplicate the child block (loop is NOT locked) + duplicateBlock('child-1') + + const { blocks } = useWorkflowStore.getState() + const blockIds = Object.keys(blocks) + const duplicatedId = blockIds.find((id) => id !== 'loop-1' && id !== 'child-1') + + if (duplicatedId) { + // Duplicate should still be inside the loop since it's not locked + expect(blocks[duplicatedId].data?.parentId).toBe('loop-1') + } + }) }) describe('updateBlockName', () => { diff --git a/apps/sim/stores/workflows/workflow/store.ts b/apps/sim/stores/workflows/workflow/store.ts index 45f8d23f2..3e28d717b 100644 --- a/apps/sim/stores/workflows/workflow/store.ts +++ b/apps/sim/stores/workflows/workflow/store.ts @@ -563,9 +563,33 @@ export const useWorkflowStore = create()( if (!block) return const newId = crypto.randomUUID() - const offsetPosition = { - x: block.position.x + DEFAULT_DUPLICATE_OFFSET.x, - y: block.position.y + DEFAULT_DUPLICATE_OFFSET.y, + + // Check if block is inside a locked container - if so, place duplicate outside + const parentId = block.data?.parentId + const parentBlock = parentId ? get().blocks[parentId] : undefined + const isParentLocked = parentBlock?.locked ?? false + + // If parent is locked, calculate position outside the container + let offsetPosition: Position + const newData = block.data ? { ...block.data } : undefined + + if (isParentLocked && parentBlock) { + // Place duplicate outside the locked container (to the right of it) + const containerWidth = parentBlock.data?.width ?? 400 + offsetPosition = { + x: parentBlock.position.x + containerWidth + 50, + y: parentBlock.position.y, + } + // Remove parent relationship since we're placing outside + if (newData) { + newData.parentId = undefined + newData.extent = undefined + } + } else { + offsetPosition = { + x: block.position.x + DEFAULT_DUPLICATE_OFFSET.x, + y: block.position.y + DEFAULT_DUPLICATE_OFFSET.y, + } } const newName = getUniqueBlockName(block.name, get().blocks) @@ -594,6 +618,7 @@ export const useWorkflowStore = create()( position: offsetPosition, subBlocks: newSubBlocks, locked: false, + data: newData, }, }, edges: [...get().edges],