diff --git a/apps/sim/app/api/workspaces/route.ts b/apps/sim/app/api/workspaces/route.ts index f9172d9c30..492922b79b 100644 --- a/apps/sim/app/api/workspaces/route.ts +++ b/apps/sim/app/api/workspaces/route.ts @@ -13,6 +13,7 @@ const logger = createLogger('Workspaces') const createWorkspaceSchema = z.object({ name: z.string().trim().min(1, 'Name is required'), + skipDefaultWorkflow: z.boolean().optional().default(false), }) // Get all workspaces for the current user @@ -63,9 +64,9 @@ export async function POST(req: Request) { } try { - const { name } = createWorkspaceSchema.parse(await req.json()) + const { name, skipDefaultWorkflow } = createWorkspaceSchema.parse(await req.json()) - const newWorkspace = await createWorkspace(session.user.id, name) + const newWorkspace = await createWorkspace(session.user.id, name, skipDefaultWorkflow) return NextResponse.json({ workspace: newWorkspace }) } catch (error) { @@ -80,7 +81,7 @@ async function createDefaultWorkspace(userId: string, userName?: string | null) return createWorkspace(userId, workspaceName) } -async function createWorkspace(userId: string, name: string) { +async function createWorkspace(userId: string, name: string, skipDefaultWorkflow = false) { const workspaceId = crypto.randomUUID() const workflowId = crypto.randomUUID() const now = new Date() @@ -97,7 +98,6 @@ async function createWorkspace(userId: string, name: string) { updatedAt: now, }) - // Create admin permissions for the workspace owner await tx.insert(permissions).values({ id: crypto.randomUUID(), entityType: 'workspace' as const, @@ -108,37 +108,41 @@ async function createWorkspace(userId: string, name: string) { updatedAt: now, }) - // Create initial workflow for the workspace (empty canvas) - // Create the workflow - await tx.insert(workflow).values({ - id: workflowId, - userId, - workspaceId, - folderId: null, - name: 'default-agent', - description: 'Your first workflow - start building here!', - color: '#3972F6', - lastSynced: now, - createdAt: now, - updatedAt: now, - isDeployed: false, - runCount: 0, - variables: {}, - }) + if (!skipDefaultWorkflow) { + await tx.insert(workflow).values({ + id: workflowId, + userId, + workspaceId, + folderId: null, + name: 'default-agent', + description: 'Your first workflow - start building here!', + color: '#3972F6', + lastSynced: now, + createdAt: now, + updatedAt: now, + isDeployed: false, + runCount: 0, + variables: {}, + }) + } logger.info( - `Created workspace ${workspaceId} with initial workflow ${workflowId} for user ${userId}` + skipDefaultWorkflow + ? `Created workspace ${workspaceId} for user ${userId}` + : `Created workspace ${workspaceId} with initial workflow ${workflowId} for user ${userId}` ) }) - const { workflowState } = buildDefaultWorkflowArtifacts() - const seedResult = await saveWorkflowToNormalizedTables(workflowId, workflowState) + if (!skipDefaultWorkflow) { + const { workflowState } = buildDefaultWorkflowArtifacts() + const seedResult = await saveWorkflowToNormalizedTables(workflowId, workflowState) - if (!seedResult.success) { - throw new Error(seedResult.error || 'Failed to seed default workflow state') + if (!seedResult.success) { + throw new Error(seedResult.error || 'Failed to seed default workflow state') + } } } catch (error) { - logger.error(`Failed to create workspace ${workspaceId} with initial workflow:`, error) + logger.error(`Failed to create workspace ${workspaceId}:`, error) throw error } diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/workflow-list.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/workflow-list.tsx index e6a811817e..ba2a7b6d45 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/workflow-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/workflow-list.tsx @@ -33,7 +33,7 @@ function DropIndicatorLine({ show, level = 0 }: { show: boolean; level?: number className='pointer-events-none absolute left-0 right-0 z-20 flex items-center' style={{ paddingLeft: `${level * TREE_SPACING.INDENT_PER_LEVEL}px` }} > -
+
) } @@ -163,7 +163,7 @@ export function WorkflowList({ active={isWorkflowActive(workflow.id)} level={level} onWorkflowClick={handleWorkflowClick} - onDragStart={() => handleDragStart('workflow')} + onDragStart={() => handleDragStart('workflow', folderId)} onDragEnd={handleDragEnd} />
@@ -224,18 +224,21 @@ export function WorkflowList({ return (
+ {/* Drop target highlight overlay - covers entire folder section */}
+
handleDragStart('folder')} + onDragStart={() => handleDragStart('folder', parentFolderId)} onDragEnd={handleDragEnd} />
@@ -314,13 +317,16 @@ export function WorkflowList({ return (
+ {/* Root drop target highlight overlay */} +
{rootItems.map((item) => item.type === 'folder' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-drag-drop.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-drag-drop.ts index 3ad9736cd6..a4cd6271f6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-drag-drop.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-drag-drop.ts @@ -27,6 +27,7 @@ export function useDragDrop() { const hoverExpandTimerRef = useRef(null) const lastDragYRef = useRef(0) const draggedTypeRef = useRef<'workflow' | 'folder' | null>(null) + const draggedSourceFolderRef = useRef(null) const params = useParams() const workspaceId = params.workspaceId as string | undefined @@ -119,59 +120,163 @@ export function useDragDrop() { [] ) + type SiblingItem = { type: 'folder' | 'workflow'; id: string; sortOrder: number } + + const getDestinationFolderId = useCallback((indicator: DropIndicator): string | null => { + return indicator.position === 'inside' + ? indicator.targetId === 'root' + ? null + : indicator.targetId + : indicator.folderId + }, []) + + const calculateInsertIndex = useCallback( + (remaining: SiblingItem[], indicator: DropIndicator): number => { + return indicator.position === 'inside' + ? remaining.length + : remaining.findIndex((item) => item.id === indicator.targetId) + + (indicator.position === 'after' ? 1 : 0) + }, + [] + ) + + const buildAndSubmitUpdates = useCallback( + async (newOrder: SiblingItem[], destinationFolderId: string | null) => { + const indexed = newOrder.map((item, i) => ({ ...item, sortOrder: i })) + + const folderUpdates = indexed + .filter((item) => item.type === 'folder') + .map((item) => ({ id: item.id, sortOrder: item.sortOrder, parentId: destinationFolderId })) + + const workflowUpdates = indexed + .filter((item) => item.type === 'workflow') + .map((item) => ({ id: item.id, sortOrder: item.sortOrder, folderId: destinationFolderId })) + + await Promise.all([ + folderUpdates.length > 0 && + reorderFoldersMutation.mutateAsync({ workspaceId: workspaceId!, updates: folderUpdates }), + workflowUpdates.length > 0 && + reorderWorkflowsMutation.mutateAsync({ + workspaceId: workspaceId!, + updates: workflowUpdates, + }), + ]) + }, + [workspaceId, reorderFoldersMutation, reorderWorkflowsMutation] + ) + + const isLeavingElement = useCallback((e: React.DragEvent): boolean => { + const relatedTarget = e.relatedTarget as HTMLElement | null + const currentTarget = e.currentTarget as HTMLElement + return !relatedTarget || !currentTarget.contains(relatedTarget) + }, []) + + const initDragOver = useCallback((e: React.DragEvent, stopPropagation = true) => { + e.preventDefault() + if (stopPropagation) e.stopPropagation() + lastDragYRef.current = e.clientY + setIsDragging(true) + }, []) + + const getSiblingItems = useCallback((folderId: string | null): SiblingItem[] => { + const currentFolders = useFolderStore.getState().folders + const currentWorkflows = useWorkflowRegistry.getState().workflows + return [ + ...Object.values(currentFolders) + .filter((f) => f.parentId === folderId) + .map((f) => ({ type: 'folder' as const, id: f.id, sortOrder: f.sortOrder })), + ...Object.values(currentWorkflows) + .filter((w) => w.folderId === folderId) + .map((w) => ({ type: 'workflow' as const, id: w.id, sortOrder: w.sortOrder })), + ].sort((a, b) => a.sortOrder - b.sortOrder) + }, []) + + const setNormalizedDropIndicator = useCallback( + (indicator: DropIndicator | null) => { + if (!indicator || indicator.position !== 'after' || indicator.targetId === 'root') { + setDropIndicator(indicator) + return + } + + const siblings = getSiblingItems(indicator.folderId) + const currentIdx = siblings.findIndex((s) => s.id === indicator.targetId) + const nextSibling = siblings[currentIdx + 1] + + if (nextSibling) { + setDropIndicator({ + targetId: nextSibling.id, + position: 'before', + folderId: indicator.folderId, + }) + } else { + setDropIndicator(indicator) + } + }, + [getSiblingItems] + ) + + const isNoOpMove = useCallback( + ( + indicator: DropIndicator, + draggedIds: string[], + draggedType: 'folder' | 'workflow', + destinationFolderId: string | null, + currentFolderId: string | null | undefined + ): boolean => { + if (indicator.position !== 'inside' && draggedIds.includes(indicator.targetId)) { + return true + } + if (currentFolderId !== destinationFolderId) { + return false + } + const siblingItems = getSiblingItems(destinationFolderId) + const remaining = siblingItems.filter( + (item) => !(item.type === draggedType && draggedIds.includes(item.id)) + ) + const insertAt = calculateInsertIndex(remaining, indicator) + const originalIdx = siblingItems.findIndex( + (item) => item.type === draggedType && item.id === draggedIds[0] + ) + return insertAt === originalIdx + }, + [getSiblingItems, calculateInsertIndex] + ) + const handleWorkflowDrop = useCallback( async (workflowIds: string[], indicator: DropIndicator) => { if (!workflowIds.length || !workspaceId) return try { - const destinationFolderId = - indicator.position === 'inside' - ? indicator.targetId === 'root' - ? null - : indicator.targetId - : indicator.folderId - - type SiblingItem = { type: 'folder' | 'workflow'; id: string; sortOrder: number } - const currentFolders = useFolderStore.getState().folders + const destinationFolderId = getDestinationFolderId(indicator) const currentWorkflows = useWorkflowRegistry.getState().workflows - const siblingFolders = Object.values(currentFolders).filter( - (f) => f.parentId === destinationFolderId - ) - const siblingWorkflows = Object.values(currentWorkflows).filter( - (w) => w.folderId === destinationFolderId - ) + const firstWorkflow = currentWorkflows[workflowIds[0]] - const siblingItems: SiblingItem[] = [ - ...siblingFolders.map((f) => ({ - type: 'folder' as const, - id: f.id, - sortOrder: f.sortOrder, - })), - ...siblingWorkflows.map((w) => ({ - type: 'workflow' as const, - id: w.id, - sortOrder: w.sortOrder, - })), - ].sort((a, b) => a.sortOrder - b.sortOrder) + if ( + isNoOpMove( + indicator, + workflowIds, + 'workflow', + destinationFolderId, + firstWorkflow?.folderId + ) + ) { + return + } + const siblingItems = getSiblingItems(destinationFolderId) const movingSet = new Set(workflowIds) const remaining = siblingItems.filter( (item) => !(item.type === 'workflow' && movingSet.has(item.id)) ) const moving = workflowIds - .map((id) => { - const w = currentWorkflows[id] - return { type: 'workflow' as const, id, sortOrder: w?.sortOrder ?? 0 } - }) + .map((id) => ({ + type: 'workflow' as const, + id, + sortOrder: currentWorkflows[id]?.sortOrder ?? 0, + })) .sort((a, b) => a.sortOrder - b.sortOrder) - let insertAt: number - if (indicator.position === 'inside') { - insertAt = remaining.length - } else { - const targetIdx = remaining.findIndex((item) => item.id === indicator.targetId) - insertAt = indicator.position === 'before' ? targetIdx : targetIdx + 1 - } + const insertAt = calculateInsertIndex(remaining, indicator) const newOrder: SiblingItem[] = [ ...remaining.slice(0, insertAt), @@ -179,35 +284,18 @@ export function useDragDrop() { ...remaining.slice(insertAt), ] - const folderUpdates = newOrder - .map((item, i) => ({ ...item, sortOrder: i })) - .filter((item) => item.type === 'folder') - .map((item) => ({ - id: item.id, - sortOrder: item.sortOrder, - parentId: destinationFolderId, - })) - - const workflowUpdates = newOrder - .map((item, i) => ({ ...item, sortOrder: i })) - .filter((item) => item.type === 'workflow') - .map((item) => ({ - id: item.id, - sortOrder: item.sortOrder, - folderId: destinationFolderId, - })) - - await Promise.all([ - folderUpdates.length > 0 && - reorderFoldersMutation.mutateAsync({ workspaceId, updates: folderUpdates }), - workflowUpdates.length > 0 && - reorderWorkflowsMutation.mutateAsync({ workspaceId, updates: workflowUpdates }), - ]) + await buildAndSubmitUpdates(newOrder, destinationFolderId) } catch (error) { logger.error('Failed to reorder workflows:', error) } }, - [workspaceId, reorderFoldersMutation, reorderWorkflowsMutation] + [ + getDestinationFolderId, + getSiblingItems, + calculateInsertIndex, + isNoOpMove, + buildAndSubmitUpdates, + ] ) const handleFolderDrop = useCallback( @@ -218,12 +306,7 @@ export function useDragDrop() { const folderStore = useFolderStore.getState() const currentFolders = folderStore.folders - const targetParentId = - indicator.position === 'inside' - ? indicator.targetId === 'root' - ? null - : indicator.targetId - : indicator.folderId + const targetParentId = getDestinationFolderId(indicator) if (draggedFolderId === targetParentId) { logger.info('Cannot move folder into itself') @@ -238,39 +321,25 @@ export function useDragDrop() { } } - type SiblingItem = { type: 'folder' | 'workflow'; id: string; sortOrder: number } - const currentWorkflows = useWorkflowRegistry.getState().workflows - const siblingFolders = Object.values(currentFolders).filter( - (f) => f.parentId === targetParentId - ) - const siblingWorkflows = Object.values(currentWorkflows).filter( - (w) => w.folderId === targetParentId - ) - - const siblingItems: SiblingItem[] = [ - ...siblingFolders.map((f) => ({ - type: 'folder' as const, - id: f.id, - sortOrder: f.sortOrder, - })), - ...siblingWorkflows.map((w) => ({ - type: 'workflow' as const, - id: w.id, - sortOrder: w.sortOrder, - })), - ].sort((a, b) => a.sortOrder - b.sortOrder) + const draggedFolder = currentFolders[draggedFolderId] + if ( + isNoOpMove( + indicator, + [draggedFolderId], + 'folder', + targetParentId, + draggedFolder?.parentId + ) + ) { + return + } + const siblingItems = getSiblingItems(targetParentId) const remaining = siblingItems.filter( (item) => !(item.type === 'folder' && item.id === draggedFolderId) ) - let insertAt: number - if (indicator.position === 'inside') { - insertAt = remaining.length - } else { - const targetIdx = remaining.findIndex((item) => item.id === indicator.targetId) - insertAt = indicator.position === 'before' ? targetIdx : targetIdx + 1 - } + const insertAt = calculateInsertIndex(remaining, indicator) const newOrder: SiblingItem[] = [ ...remaining.slice(0, insertAt), @@ -278,27 +347,19 @@ export function useDragDrop() { ...remaining.slice(insertAt), ] - const folderUpdates = newOrder - .map((item, i) => ({ ...item, sortOrder: i })) - .filter((item) => item.type === 'folder') - .map((item) => ({ id: item.id, sortOrder: item.sortOrder, parentId: targetParentId })) - - const workflowUpdates = newOrder - .map((item, i) => ({ ...item, sortOrder: i })) - .filter((item) => item.type === 'workflow') - .map((item) => ({ id: item.id, sortOrder: item.sortOrder, folderId: targetParentId })) - - await Promise.all([ - folderUpdates.length > 0 && - reorderFoldersMutation.mutateAsync({ workspaceId, updates: folderUpdates }), - workflowUpdates.length > 0 && - reorderWorkflowsMutation.mutateAsync({ workspaceId, updates: workflowUpdates }), - ]) + await buildAndSubmitUpdates(newOrder, targetParentId) } catch (error) { logger.error('Failed to reorder folder:', error) } }, - [workspaceId, reorderFoldersMutation, reorderWorkflowsMutation] + [ + workspaceId, + getDestinationFolderId, + getSiblingItems, + calculateInsertIndex, + isNoOpMove, + buildAndSubmitUpdates, + ] ) const handleDrop = useCallback( @@ -334,90 +395,98 @@ export function useDragDrop() { const createWorkflowDragHandlers = useCallback( (workflowId: string, folderId: string | null) => ({ onDragOver: (e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - lastDragYRef.current = e.clientY - setIsDragging(true) - - const position = calculateDropPosition(e, e.currentTarget) - setDropIndicator({ targetId: workflowId, position, folderId }) + initDragOver(e) + const isSameFolder = draggedSourceFolderRef.current === folderId + if (isSameFolder) { + const position = calculateDropPosition(e, e.currentTarget) + setNormalizedDropIndicator({ targetId: workflowId, position, folderId }) + } else { + setNormalizedDropIndicator({ + targetId: folderId || 'root', + position: 'inside', + folderId: null, + }) + } }, onDrop: handleDrop, }), - [calculateDropPosition, handleDrop] + [initDragOver, calculateDropPosition, setNormalizedDropIndicator, handleDrop] ) const createFolderDragHandlers = useCallback( (folderId: string, parentFolderId: string | null) => ({ onDragOver: (e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - lastDragYRef.current = e.clientY - setIsDragging(true) - + initDragOver(e) if (draggedTypeRef.current === 'folder') { - const position = calculateDropPosition(e, e.currentTarget) - setDropIndicator({ targetId: folderId, position, folderId: parentFolderId }) + const isSameParent = draggedSourceFolderRef.current === parentFolderId + if (isSameParent) { + const position = calculateDropPosition(e, e.currentTarget) + setNormalizedDropIndicator({ targetId: folderId, position, folderId: parentFolderId }) + } else { + // Cross-container: highlight this folder (drop into it) + setNormalizedDropIndicator({ + targetId: folderId, + position: 'inside', + folderId: parentFolderId, + }) + setHoverFolderId(folderId) + } } else { - setDropIndicator({ targetId: folderId, position: 'inside', folderId: parentFolderId }) + setNormalizedDropIndicator({ + targetId: folderId, + position: 'inside', + folderId: parentFolderId, + }) setHoverFolderId(folderId) } }, onDragLeave: (e: React.DragEvent) => { - const relatedTarget = e.relatedTarget as HTMLElement | null - const currentTarget = e.currentTarget as HTMLElement - if (!relatedTarget || !currentTarget.contains(relatedTarget)) { - setHoverFolderId(null) - } + if (isLeavingElement(e)) setHoverFolderId(null) }, onDrop: handleDrop, }), - [calculateDropPosition, handleDrop] + [initDragOver, calculateDropPosition, setNormalizedDropIndicator, isLeavingElement, handleDrop] ) const createEmptyFolderDropZone = useCallback( (folderId: string) => ({ onDragOver: (e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - lastDragYRef.current = e.clientY - setIsDragging(true) - setDropIndicator({ targetId: folderId, position: 'inside', folderId }) + initDragOver(e) + setNormalizedDropIndicator({ targetId: folderId, position: 'inside', folderId }) }, onDrop: handleDrop, }), - [handleDrop] + [initDragOver, setNormalizedDropIndicator, handleDrop] ) const createRootDropZone = useCallback( () => ({ onDragOver: (e: React.DragEvent) => { - e.preventDefault() - lastDragYRef.current = e.clientY - setIsDragging(true) - setDropIndicator({ targetId: 'root', position: 'inside', folderId: null }) + initDragOver(e, false) + setNormalizedDropIndicator({ targetId: 'root', position: 'inside', folderId: null }) }, onDragLeave: (e: React.DragEvent) => { - const relatedTarget = e.relatedTarget as HTMLElement | null - const currentTarget = e.currentTarget as HTMLElement - if (!relatedTarget || !currentTarget.contains(relatedTarget)) { - setDropIndicator(null) - } + if (isLeavingElement(e)) setNormalizedDropIndicator(null) }, onDrop: handleDrop, }), - [handleDrop] + [initDragOver, setNormalizedDropIndicator, isLeavingElement, handleDrop] ) - const handleDragStart = useCallback((type: 'workflow' | 'folder') => { - draggedTypeRef.current = type - setIsDragging(true) - }, []) + const handleDragStart = useCallback( + (type: 'workflow' | 'folder', sourceFolderId: string | null) => { + draggedTypeRef.current = type + draggedSourceFolderRef.current = sourceFolderId + setIsDragging(true) + }, + [] + ) const handleDragEnd = useCallback(() => { setIsDragging(false) setDropIndicator(null) draggedTypeRef.current = null + draggedSourceFolderRef.current = null setHoverFolderId(null) }, []) diff --git a/apps/sim/app/workspace/[workspaceId]/w/hooks/use-import-workflow.ts b/apps/sim/app/workspace/[workspaceId]/w/hooks/use-import-workflow.ts index 00bdf33f87..5309ade1a5 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/hooks/use-import-workflow.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/hooks/use-import-workflow.ts @@ -140,9 +140,9 @@ export function useImportWorkflow({ workspaceId }: UseImportWorkflowProps) { workspaceId, }) const folderMap = new Map() + const sanitizeName = (name: string) => name.replace(/[^a-z0-9-_]/gi, '-') - const exportedFoldersByPath = new Map() - if (metadata?.folders) { + if (metadata?.folders && metadata.folders.length > 0) { type ExportedFolder = { id: string name: string @@ -152,7 +152,7 @@ export function useImportWorkflow({ workspaceId }: UseImportWorkflowProps) { const foldersById = new Map( metadata.folders.map((f) => [f.id, f]) ) - const sanitizeName = (name: string) => name.replace(/[^a-z0-9-_]/gi, '-') + const oldIdToNewId = new Map() const buildPath = (folderId: string): string => { const pathParts: string[] = [] @@ -165,9 +165,29 @@ export function useImportWorkflow({ workspaceId }: UseImportWorkflowProps) { return pathParts.join('/') } - for (const f of metadata.folders) { - const path = buildPath(f.id) - exportedFoldersByPath.set(path, { sortOrder: f.sortOrder }) + const createFolderRecursive = async (folder: ExportedFolder): Promise => { + if (oldIdToNewId.has(folder.id)) { + return oldIdToNewId.get(folder.id)! + } + + let parentId = importFolder.id + if (folder.parentId && foldersById.has(folder.parentId)) { + parentId = await createFolderRecursive(foldersById.get(folder.parentId)!) + } + + const newFolder = await createFolderMutation.mutateAsync({ + name: folder.name, + workspaceId, + parentId, + sortOrder: folder.sortOrder, + }) + oldIdToNewId.set(folder.id, newFolder.id) + folderMap.set(buildPath(folder.id), newFolder.id) + return newFolder.id + } + + for (const folder of metadata.folders) { + await createFolderRecursive(folder) } } @@ -178,20 +198,19 @@ export function useImportWorkflow({ workspaceId }: UseImportWorkflowProps) { if (workflow.folderPath.length > 0) { const folderPathKey = workflow.folderPath.join('/') - if (!folderMap.has(folderPathKey)) { + if (folderMap.has(folderPathKey)) { + targetFolderId = folderMap.get(folderPathKey)! + } else { let parentId = importFolder.id - for (let i = 0; i < workflow.folderPath.length; i++) { const pathSegment = workflow.folderPath.slice(0, i + 1).join('/') const folderNameForSegment = workflow.folderPath[i] if (!folderMap.has(pathSegment)) { - const exportedFolder = exportedFoldersByPath.get(pathSegment) const subFolder = await createFolderMutation.mutateAsync({ name: folderNameForSegment, workspaceId, parentId, - sortOrder: exportedFolder?.sortOrder, }) folderMap.set(pathSegment, subFolder.id) parentId = subFolder.id @@ -199,9 +218,8 @@ export function useImportWorkflow({ workspaceId }: UseImportWorkflowProps) { parentId = folderMap.get(pathSegment)! } } + targetFolderId = folderMap.get(folderPathKey)! } - - targetFolderId = folderMap.get(folderPathKey)! } const workflowId = await importSingleWorkflow( diff --git a/apps/sim/app/workspace/[workspaceId]/w/hooks/use-import-workspace.ts b/apps/sim/app/workspace/[workspaceId]/w/hooks/use-import-workspace.ts index d79add0c23..c99a1a477c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/hooks/use-import-workspace.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/hooks/use-import-workspace.ts @@ -59,7 +59,7 @@ export function useImportWorkspace({ onSuccess }: UseImportWorkspaceProps = {}) const createResponse = await fetch('/api/workspaces', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name: workspaceName }), + body: JSON.stringify({ name: workspaceName, skipDefaultWorkflow: true }), }) if (!createResponse.ok) { @@ -70,6 +70,56 @@ export function useImportWorkspace({ onSuccess }: UseImportWorkspaceProps = {}) logger.info('Created new workspace:', newWorkspace) const folderMap = new Map() + const sanitizeName = (name: string) => name.replace(/[^a-z0-9-_]/gi, '-') + + if (metadata?.folders && metadata.folders.length > 0) { + type ExportedFolder = { + id: string + name: string + parentId: string | null + sortOrder?: number + } + const foldersById = new Map( + metadata.folders.map((f) => [f.id, f]) + ) + const oldIdToNewId = new Map() + + const buildPath = (folderId: string): string => { + const pathParts: string[] = [] + let currentId: string | null = folderId + while (currentId && foldersById.has(currentId)) { + const folder: ExportedFolder = foldersById.get(currentId)! + pathParts.unshift(sanitizeName(folder.name)) + currentId = folder.parentId + } + return pathParts.join('/') + } + + const createFolderRecursive = async (folder: ExportedFolder): Promise => { + if (oldIdToNewId.has(folder.id)) { + return oldIdToNewId.get(folder.id)! + } + + let parentId: string | undefined + if (folder.parentId && foldersById.has(folder.parentId)) { + parentId = await createFolderRecursive(foldersById.get(folder.parentId)!) + } + + const newFolder = await createFolderMutation.mutateAsync({ + name: folder.name, + workspaceId: newWorkspace.id, + parentId, + sortOrder: folder.sortOrder, + }) + oldIdToNewId.set(folder.id, newFolder.id) + folderMap.set(buildPath(folder.id), newFolder.id) + return newFolder.id + } + + for (const folder of metadata.folders) { + await createFolderRecursive(folder) + } + } for (const workflow of extractedWorkflows) { try { @@ -84,9 +134,10 @@ export function useImportWorkspace({ onSuccess }: UseImportWorkspaceProps = {}) if (workflow.folderPath.length > 0) { const folderPathKey = workflow.folderPath.join('/') - if (!folderMap.has(folderPathKey)) { - let parentId: string | null = null - + if (folderMap.has(folderPathKey)) { + targetFolderId = folderMap.get(folderPathKey)! + } else { + let parentId: string | undefined for (let i = 0; i < workflow.folderPath.length; i++) { const pathSegment = workflow.folderPath.slice(0, i + 1).join('/') @@ -94,7 +145,7 @@ export function useImportWorkspace({ onSuccess }: UseImportWorkspaceProps = {}) const subFolder = await createFolderMutation.mutateAsync({ name: workflow.folderPath[i], workspaceId: newWorkspace.id, - parentId: parentId || undefined, + parentId, }) folderMap.set(pathSegment, subFolder.id) parentId = subFolder.id @@ -102,9 +153,8 @@ export function useImportWorkspace({ onSuccess }: UseImportWorkspaceProps = {}) parentId = folderMap.get(pathSegment)! } } + targetFolderId = folderMap.get(folderPathKey) || null } - - targetFolderId = folderMap.get(folderPathKey) || null } const workflowName = extractWorkflowName(workflow.content, workflow.name)