Skip to content

Commit 4bbe0b6

Browse files
committed
fix(sidebar): unify workflow and folder insertion ordering
1 parent 86ca984 commit 4bbe0b6

File tree

9 files changed

+629
-213
lines changed

9 files changed

+629
-213
lines changed

apps/sim/app/api/folders/[id]/duplicate/route.ts

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { db } from '@sim/db'
22
import { workflow, workflowFolder } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
4-
import { and, eq } from 'drizzle-orm'
4+
import { and, asc, eq, isNull } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
66
import { z } from 'zod'
77
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
@@ -37,7 +37,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
3737

3838
logger.info(`[${requestId}] Duplicating folder ${sourceFolderId} for user ${session.user.id}`)
3939

40-
// Verify the source folder exists
4140
const sourceFolder = await db
4241
.select()
4342
.from(workflowFolder)
@@ -48,7 +47,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
4847
throw new Error('Source folder not found')
4948
}
5049

51-
// Check if user has permission to access the source folder
5250
const userPermission = await getUserEntityPermissions(
5351
session.user.id,
5452
'workspace',
@@ -61,26 +59,55 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
6159

6260
const targetWorkspaceId = workspaceId || sourceFolder.workspaceId
6361

64-
// Step 1: Duplicate folder structure
6562
const { newFolderId, folderMapping } = await db.transaction(async (tx) => {
6663
const newFolderId = crypto.randomUUID()
6764
const now = new Date()
65+
const targetParentId = parentId ?? sourceFolder.parentId
66+
67+
const folderParentCondition = targetParentId
68+
? eq(workflowFolder.parentId, targetParentId)
69+
: isNull(workflowFolder.parentId)
70+
const workflowParentCondition = targetParentId
71+
? eq(workflow.folderId, targetParentId)
72+
: isNull(workflow.folderId)
73+
74+
const [[folderResult], [workflowResult]] = await Promise.all([
75+
tx
76+
.select({ sortOrder: workflowFolder.sortOrder })
77+
.from(workflowFolder)
78+
.where(and(eq(workflowFolder.workspaceId, targetWorkspaceId), folderParentCondition))
79+
.orderBy(asc(workflowFolder.sortOrder))
80+
.limit(1),
81+
tx
82+
.select({ sortOrder: workflow.sortOrder })
83+
.from(workflow)
84+
.where(and(eq(workflow.workspaceId, targetWorkspaceId), workflowParentCondition))
85+
.orderBy(asc(workflow.sortOrder))
86+
.limit(1),
87+
])
88+
89+
const minSortOrder = [folderResult?.sortOrder, workflowResult?.sortOrder].reduce<
90+
number | null
91+
>((currentMin, candidate) => {
92+
if (candidate == null) return currentMin
93+
if (currentMin == null) return candidate
94+
return Math.min(currentMin, candidate)
95+
}, null)
96+
const sortOrder = minSortOrder != null ? minSortOrder - 1 : 0
6897

69-
// Create the new root folder
7098
await tx.insert(workflowFolder).values({
7199
id: newFolderId,
72100
userId: session.user.id,
73101
workspaceId: targetWorkspaceId,
74102
name,
75103
color: color || sourceFolder.color,
76-
parentId: parentId || sourceFolder.parentId,
77-
sortOrder: sourceFolder.sortOrder,
104+
parentId: targetParentId,
105+
sortOrder,
78106
isExpanded: false,
79107
createdAt: now,
80108
updatedAt: now,
81109
})
82110

83-
// Recursively duplicate child folders
84111
const folderMapping = new Map<string, string>([[sourceFolderId, newFolderId]])
85112
await duplicateFolderStructure(
86113
tx,
@@ -96,7 +123,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
96123
return { newFolderId, folderMapping }
97124
})
98125

99-
// Step 2: Duplicate workflows
100126
const workflowStats = await duplicateWorkflowsInFolderTree(
101127
sourceFolder.workspaceId,
102128
targetWorkspaceId,
@@ -173,7 +199,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
173199
}
174200
}
175201

176-
// Helper to recursively duplicate folder structure
177202
async function duplicateFolderStructure(
178203
tx: any,
179204
sourceFolderId: string,
@@ -184,7 +209,6 @@ async function duplicateFolderStructure(
184209
timestamp: Date,
185210
folderMapping: Map<string, string>
186211
): Promise<void> {
187-
// Get all child folders
188212
const childFolders = await tx
189213
.select()
190214
.from(workflowFolder)
@@ -195,7 +219,6 @@ async function duplicateFolderStructure(
195219
)
196220
)
197221

198-
// Create each child folder and recurse
199222
for (const childFolder of childFolders) {
200223
const newChildFolderId = crypto.randomUUID()
201224
folderMapping.set(childFolder.id, newChildFolderId)
@@ -213,7 +236,6 @@ async function duplicateFolderStructure(
213236
updatedAt: timestamp,
214237
})
215238

216-
// Recurse for this child's children
217239
await duplicateFolderStructure(
218240
tx,
219241
childFolder.id,
@@ -227,7 +249,6 @@ async function duplicateFolderStructure(
227249
}
228250
}
229251

230-
// Helper to duplicate all workflows in a folder tree
231252
async function duplicateWorkflowsInFolderTree(
232253
sourceWorkspaceId: string,
233254
targetWorkspaceId: string,
@@ -237,17 +258,14 @@ async function duplicateWorkflowsInFolderTree(
237258
): Promise<{ total: number; succeeded: number; failed: number }> {
238259
const stats = { total: 0, succeeded: 0, failed: 0 }
239260

240-
// Process each folder in the mapping
241261
for (const [oldFolderId, newFolderId] of folderMapping.entries()) {
242-
// Get workflows in this folder
243262
const workflowsInFolder = await db
244263
.select()
245264
.from(workflow)
246265
.where(and(eq(workflow.folderId, oldFolderId), eq(workflow.workspaceId, sourceWorkspaceId)))
247266

248267
stats.total += workflowsInFolder.length
249268

250-
// Duplicate each workflow
251269
for (const sourceWorkflow of workflowsInFolder) {
252270
try {
253271
await duplicateWorkflow({

0 commit comments

Comments
 (0)