Skip to content

Commit 2979269

Browse files
authored
fix(sidebar): unify workflow and folder insertion ordering (#3250)
* fix(sidebar): unify workflow and folder insertion ordering * ack comments * ack comments * ack * ack comment * upgrade turbo * fix build
1 parent cf28822 commit 2979269

File tree

13 files changed

+808
-233
lines changed

13 files changed

+808
-233
lines changed

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

Lines changed: 31 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, eq, isNull, min } 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,51 @@ 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({ minSortOrder: min(workflowFolder.sortOrder) })
77+
.from(workflowFolder)
78+
.where(and(eq(workflowFolder.workspaceId, targetWorkspaceId), folderParentCondition)),
79+
tx
80+
.select({ minSortOrder: min(workflow.sortOrder) })
81+
.from(workflow)
82+
.where(and(eq(workflow.workspaceId, targetWorkspaceId), workflowParentCondition)),
83+
])
84+
85+
const minSortOrder = [folderResult?.minSortOrder, workflowResult?.minSortOrder].reduce<
86+
number | null
87+
>((currentMin, candidate) => {
88+
if (candidate == null) return currentMin
89+
if (currentMin == null) return candidate
90+
return Math.min(currentMin, candidate)
91+
}, null)
92+
const sortOrder = minSortOrder != null ? minSortOrder - 1 : 0
6893

69-
// Create the new root folder
7094
await tx.insert(workflowFolder).values({
7195
id: newFolderId,
7296
userId: session.user.id,
7397
workspaceId: targetWorkspaceId,
7498
name,
7599
color: color || sourceFolder.color,
76-
parentId: parentId || sourceFolder.parentId,
77-
sortOrder: sourceFolder.sortOrder,
100+
parentId: targetParentId,
101+
sortOrder,
78102
isExpanded: false,
79103
createdAt: now,
80104
updatedAt: now,
81105
})
82106

83-
// Recursively duplicate child folders
84107
const folderMapping = new Map<string, string>([[sourceFolderId, newFolderId]])
85108
await duplicateFolderStructure(
86109
tx,
@@ -96,7 +119,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
96119
return { newFolderId, folderMapping }
97120
})
98121

99-
// Step 2: Duplicate workflows
100122
const workflowStats = await duplicateWorkflowsInFolderTree(
101123
sourceFolder.workspaceId,
102124
targetWorkspaceId,
@@ -173,7 +195,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
173195
}
174196
}
175197

176-
// Helper to recursively duplicate folder structure
177198
async function duplicateFolderStructure(
178199
tx: any,
179200
sourceFolderId: string,
@@ -184,7 +205,6 @@ async function duplicateFolderStructure(
184205
timestamp: Date,
185206
folderMapping: Map<string, string>
186207
): Promise<void> {
187-
// Get all child folders
188208
const childFolders = await tx
189209
.select()
190210
.from(workflowFolder)
@@ -195,7 +215,6 @@ async function duplicateFolderStructure(
195215
)
196216
)
197217

198-
// Create each child folder and recurse
199218
for (const childFolder of childFolders) {
200219
const newChildFolderId = crypto.randomUUID()
201220
folderMapping.set(childFolder.id, newChildFolderId)
@@ -213,7 +232,6 @@ async function duplicateFolderStructure(
213232
updatedAt: timestamp,
214233
})
215234

216-
// Recurse for this child's children
217235
await duplicateFolderStructure(
218236
tx,
219237
childFolder.id,
@@ -227,7 +245,6 @@ async function duplicateFolderStructure(
227245
}
228246
}
229247

230-
// Helper to duplicate all workflows in a folder tree
231248
async function duplicateWorkflowsInFolderTree(
232249
sourceWorkspaceId: string,
233250
targetWorkspaceId: string,
@@ -237,17 +254,14 @@ async function duplicateWorkflowsInFolderTree(
237254
): Promise<{ total: number; succeeded: number; failed: number }> {
238255
const stats = { total: 0, succeeded: 0, failed: 0 }
239256

240-
// Process each folder in the mapping
241257
for (const [oldFolderId, newFolderId] of folderMapping.entries()) {
242-
// Get workflows in this folder
243258
const workflowsInFolder = await db
244259
.select()
245260
.from(workflow)
246261
.where(and(eq(workflow.folderId, oldFolderId), eq(workflow.workspaceId, sourceWorkspaceId)))
247262

248263
stats.total += workflowsInFolder.length
249264

250-
// Duplicate each workflow
251265
for (const sourceWorkflow of workflowsInFolder) {
252266
try {
253267
await duplicateWorkflow({

0 commit comments

Comments
 (0)