From 5a9e9ab3262b44b90ff15c3d9cf0eef5e089b463 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 14 Nov 2025 15:47:10 -0800 Subject: [PATCH] fix(folders): duplicate --- .../w/hooks/use-duplicate-folder.ts | 46 ++++++++++++++++++- apps/sim/hooks/queries/folders.ts | 12 ++++- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/hooks/use-duplicate-folder.ts b/apps/sim/app/workspace/[workspaceId]/w/hooks/use-duplicate-folder.ts index 23dd08bfd44..b95c4af601c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/hooks/use-duplicate-folder.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/hooks/use-duplicate-folder.ts @@ -42,6 +42,19 @@ export function useDuplicateFolder({ const duplicateFolderMutation = useDuplicateFolderMutation() const [isDuplicating, setIsDuplicating] = useState(false) + const generateDuplicateName = useCallback((baseName: string, siblingNames: Set) => { + const trimmedName = (baseName || 'Untitled Folder').trim() + let candidate = `${trimmedName} Copy` + let counter = 2 + + while (siblingNames.has(candidate)) { + candidate = `${trimmedName} Copy ${counter}` + counter += 1 + } + + return candidate + }, []) + /** * Duplicate the folder(s) */ @@ -62,10 +75,32 @@ export function useDuplicateFolder({ const folderIdsToDuplicate = Array.isArray(folderIdsOrId) ? folderIdsOrId : [folderIdsOrId] const duplicatedIds: string[] = [] + const folderStore = useFolderStore.getState() // Duplicate each folder sequentially for (const folderId of folderIdsToDuplicate) { - const result = await duplicateFolderMutation.mutateAsync({ id: folderId, workspaceId }) + const folder = folderStore.getFolderById(folderId) + + if (!folder) { + logger.warn('Attempted to duplicate folder that no longer exists', { folderId }) + continue + } + + const siblingNames = new Set( + folderStore.getChildFolders(folder.parentId).map((sibling) => sibling.name) + ) + // Avoid colliding with the original folder name + siblingNames.add(folder.name) + + const duplicateName = generateDuplicateName(folder.name, siblingNames) + + const result = await duplicateFolderMutation.mutateAsync({ + id: folderId, + workspaceId, + name: duplicateName, + parentId: folder.parentId, + color: folder.color, + }) const newFolderId = result?.id if (newFolderId) { duplicatedIds.push(newFolderId) @@ -88,7 +123,14 @@ export function useDuplicateFolder({ } finally { setIsDuplicating(false) } - }, [getFolderIds, isDuplicating, duplicateFolderMutation, workspaceId, onSuccess]) + }, [ + getFolderIds, + generateDuplicateName, + isDuplicating, + duplicateFolderMutation, + workspaceId, + onSuccess, + ]) return { isDuplicating, diff --git a/apps/sim/hooks/queries/folders.ts b/apps/sim/hooks/queries/folders.ts index b9869388477..1f1bf056e5e 100644 --- a/apps/sim/hooks/queries/folders.ts +++ b/apps/sim/hooks/queries/folders.ts @@ -79,6 +79,9 @@ interface DeleteFolderVariables { interface DuplicateFolderVariables { workspaceId: string id: string + name: string + parentId?: string | null + color?: string } export function useCreateFolder() { @@ -160,11 +163,16 @@ export function useDuplicateFolderMutation() { const queryClient = useQueryClient() return useMutation({ - mutationFn: async ({ id, workspaceId }: DuplicateFolderVariables) => { + mutationFn: async ({ id, workspaceId, name, parentId, color }: DuplicateFolderVariables) => { const response = await fetch(`/api/folders/${id}/duplicate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ workspaceId }), + body: JSON.stringify({ + workspaceId, + name, + parentId: parentId ?? null, + color, + }), }) if (!response.ok) {