Skip to content

Commit b8dfb4d

Browse files
authored
fix(copy): preserve block names when pasting into workflows without conflicts (#3315)
1 parent 9166649 commit b8dfb4d

File tree

2 files changed

+262
-2
lines changed

2 files changed

+262
-2
lines changed

apps/sim/stores/workflows/utils.test.ts

Lines changed: 258 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -527,9 +527,266 @@ describe('regenerateBlockIds', () => {
527527
const newBlocks = Object.values(result.blocks)
528528
expect(newBlocks).toHaveLength(2)
529529

530-
// All pasted blocks should be unlocked so users can edit them
531530
for (const block of newBlocks) {
532531
expect(block.locked).toBe(false)
533532
}
534533
})
534+
535+
it('should preserve original name when no conflicting block exists', () => {
536+
const blockId = 'block-1'
537+
538+
const blocksToCopy = {
539+
[blockId]: createAgentBlock({
540+
id: blockId,
541+
name: 'Agent 1',
542+
position: { x: 100, y: 100 },
543+
}),
544+
}
545+
546+
const result = regenerateBlockIds(
547+
blocksToCopy,
548+
[],
549+
{},
550+
{},
551+
{},
552+
positionOffset,
553+
{},
554+
getUniqueBlockName
555+
)
556+
557+
const newBlocks = Object.values(result.blocks)
558+
expect(newBlocks).toHaveLength(1)
559+
expect(newBlocks[0].name).toBe('Agent 1')
560+
})
561+
562+
it('should preserve original name with number suffix when no conflict', () => {
563+
const blocksToCopy = {
564+
'block-1': createAgentBlock({
565+
id: 'block-1',
566+
name: 'Agent 3',
567+
position: { x: 100, y: 100 },
568+
}),
569+
}
570+
571+
const result = regenerateBlockIds(
572+
blocksToCopy,
573+
[],
574+
{},
575+
{},
576+
{},
577+
positionOffset,
578+
{},
579+
getUniqueBlockName
580+
)
581+
582+
expect(Object.values(result.blocks)[0].name).toBe('Agent 3')
583+
})
584+
585+
it('should increment name when an exact match exists in destination', () => {
586+
const existingBlocks = {
587+
existing: createAgentBlock({ id: 'existing', name: 'Agent 1' }),
588+
}
589+
590+
const blocksToCopy = {
591+
'block-1': createAgentBlock({
592+
id: 'block-1',
593+
name: 'Agent 1',
594+
position: { x: 100, y: 100 },
595+
}),
596+
}
597+
598+
const result = regenerateBlockIds(
599+
blocksToCopy,
600+
[],
601+
{},
602+
{},
603+
{},
604+
positionOffset,
605+
existingBlocks,
606+
getUniqueBlockName
607+
)
608+
609+
expect(Object.values(result.blocks)[0].name).toBe('Agent 2')
610+
})
611+
612+
it('should preserve name when only a different-numbered sibling exists', () => {
613+
const existingBlocks = {
614+
existing: createAgentBlock({ id: 'existing', name: 'Agent 2' }),
615+
}
616+
617+
const blocksToCopy = {
618+
'block-1': createAgentBlock({
619+
id: 'block-1',
620+
name: 'Agent 5',
621+
position: { x: 100, y: 100 },
622+
}),
623+
}
624+
625+
const result = regenerateBlockIds(
626+
blocksToCopy,
627+
[],
628+
{},
629+
{},
630+
{},
631+
positionOffset,
632+
existingBlocks,
633+
getUniqueBlockName
634+
)
635+
636+
expect(Object.values(result.blocks)[0].name).toBe('Agent 5')
637+
})
638+
639+
it('should preserve names for multiple blocks when no conflicts', () => {
640+
const blocksToCopy = {
641+
'block-1': createAgentBlock({
642+
id: 'block-1',
643+
name: 'Agent 1',
644+
position: { x: 100, y: 100 },
645+
}),
646+
'block-2': createFunctionBlock({
647+
id: 'block-2',
648+
name: 'Function 3',
649+
position: { x: 200, y: 100 },
650+
}),
651+
}
652+
653+
const result = regenerateBlockIds(
654+
blocksToCopy,
655+
[],
656+
{},
657+
{},
658+
{},
659+
positionOffset,
660+
{},
661+
getUniqueBlockName
662+
)
663+
664+
const newBlocks = Object.values(result.blocks)
665+
const agentBlock = newBlocks.find((b) => b.type === 'agent')
666+
const functionBlock = newBlocks.find((b) => b.type === 'function')
667+
668+
expect(agentBlock!.name).toBe('Agent 1')
669+
expect(functionBlock!.name).toBe('Function 3')
670+
})
671+
672+
it('should handle mixed conflicts: preserve non-conflicting, increment conflicting', () => {
673+
const existingBlocks = {
674+
existing: createAgentBlock({ id: 'existing', name: 'Agent 1' }),
675+
}
676+
677+
const blocksToCopy = {
678+
'block-1': createAgentBlock({
679+
id: 'block-1',
680+
name: 'Agent 1',
681+
position: { x: 100, y: 100 },
682+
}),
683+
'block-2': createFunctionBlock({
684+
id: 'block-2',
685+
name: 'Function 1',
686+
position: { x: 200, y: 100 },
687+
}),
688+
}
689+
690+
const result = regenerateBlockIds(
691+
blocksToCopy,
692+
[],
693+
{},
694+
{},
695+
{},
696+
positionOffset,
697+
existingBlocks,
698+
getUniqueBlockName
699+
)
700+
701+
const newBlocks = Object.values(result.blocks)
702+
const agentBlock = newBlocks.find((b) => b.type === 'agent')
703+
const functionBlock = newBlocks.find((b) => b.type === 'function')
704+
705+
expect(agentBlock!.name).toBe('Agent 2')
706+
expect(functionBlock!.name).toBe('Function 1')
707+
})
708+
709+
it('should detect conflicts case-insensitively', () => {
710+
const existingBlocks = {
711+
existing: createBlock({ id: 'existing', name: 'api 1' }),
712+
}
713+
714+
const blocksToCopy = {
715+
'block-1': createBlock({
716+
id: 'block-1',
717+
name: 'API 1',
718+
position: { x: 100, y: 100 },
719+
}),
720+
}
721+
722+
const result = regenerateBlockIds(
723+
blocksToCopy,
724+
[],
725+
{},
726+
{},
727+
{},
728+
positionOffset,
729+
existingBlocks,
730+
getUniqueBlockName
731+
)
732+
733+
expect(Object.values(result.blocks)[0].name).toBe('API 2')
734+
})
735+
736+
it('should preserve name without number suffix when no conflict', () => {
737+
const blocksToCopy = {
738+
'block-1': createBlock({
739+
id: 'block-1',
740+
name: 'Custom Block',
741+
position: { x: 100, y: 100 },
742+
}),
743+
}
744+
745+
const result = regenerateBlockIds(
746+
blocksToCopy,
747+
[],
748+
{},
749+
{},
750+
{},
751+
positionOffset,
752+
{},
753+
getUniqueBlockName
754+
)
755+
756+
expect(Object.values(result.blocks)[0].name).toBe('Custom Block')
757+
})
758+
759+
it('should avoid collisions between pasted blocks themselves', () => {
760+
const blocksToCopy = {
761+
'block-1': createAgentBlock({
762+
id: 'block-1',
763+
name: 'Agent 1',
764+
position: { x: 100, y: 100 },
765+
}),
766+
'block-2': createAgentBlock({
767+
id: 'block-2',
768+
name: 'Agent 1',
769+
position: { x: 200, y: 100 },
770+
}),
771+
}
772+
773+
const result = regenerateBlockIds(
774+
blocksToCopy,
775+
[],
776+
{},
777+
{},
778+
{},
779+
positionOffset,
780+
{},
781+
getUniqueBlockName
782+
)
783+
784+
const newBlocks = Object.values(result.blocks)
785+
const names = newBlocks.map((b) => b.name)
786+
787+
expect(names).toHaveLength(2)
788+
expect(new Set(names).size).toBe(2)
789+
expect(names).toContain('Agent 1')
790+
expect(names).toContain('Agent 2')
791+
})
535792
})

apps/sim/stores/workflows/utils.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,10 @@ export function regenerateBlockIds(
445445
blockIdMap.set(oldId, newId)
446446

447447
const oldNormalizedName = normalizeName(block.name)
448-
const newName = uniqueNameFn(block.name, allBlocksForNaming)
448+
const nameConflicts = Object.values(allBlocksForNaming).some(
449+
(existing) => normalizeName(existing.name) === oldNormalizedName
450+
)
451+
const newName = nameConflicts ? uniqueNameFn(block.name, allBlocksForNaming) : block.name
449452
const newNormalizedName = normalizeName(newName)
450453
nameMap.set(oldNormalizedName, newNormalizedName)
451454

0 commit comments

Comments
 (0)