Skip to content

Commit 6c6131e

Browse files
icecrasher321Vikhyath MondretiVikhyath Mondretiwaleedlatif1
authored
feat(folders): folders to manage workflows (#490)
* feat(subworkflows) workflows in workflows * revert sync changes * working output vars * fix greptile comments * add cycle detection * add tests * working tests * works * fix formatting * fix input var handling * fix(tab-sync): sync between tabs on change * feat(folders): folders to organize workflows * address comments * change schema types * fix lint error * fix typing error * fix race cond * delete unused files * improved UI * updated naming conventions * revert unrelated changes to db schema * fixed collapsed sidebar subfolders * add logs filters for folders --------- Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-MacBook-Air.local> Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-Air.attlocal.net> Co-authored-by: Waleed Latif <walif6@gmail.com>
1 parent fe45d0f commit 6c6131e

File tree

26 files changed

+4931
-160
lines changed

26 files changed

+4931
-160
lines changed
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import { and, eq } from 'drizzle-orm'
2+
import { type NextRequest, NextResponse } from 'next/server'
3+
import { getSession } from '@/lib/auth'
4+
import { createLogger } from '@/lib/logs/console-logger'
5+
import { db } from '@/db'
6+
import { workflow, workflowFolder } from '@/db/schema'
7+
8+
const logger = createLogger('FoldersIDAPI')
9+
10+
// PUT - Update a folder
11+
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
12+
try {
13+
const session = await getSession()
14+
if (!session?.user?.id) {
15+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
16+
}
17+
18+
const { id } = await params
19+
const body = await request.json()
20+
const { name, color, isExpanded, parentId } = body
21+
22+
// Verify the folder exists and belongs to the user
23+
const existingFolder = await db
24+
.select()
25+
.from(workflowFolder)
26+
.where(and(eq(workflowFolder.id, id), eq(workflowFolder.userId, session.user.id)))
27+
.then((rows) => rows[0])
28+
29+
if (!existingFolder) {
30+
return NextResponse.json({ error: 'Folder not found' }, { status: 404 })
31+
}
32+
33+
// Prevent setting a folder as its own parent or creating circular references
34+
if (parentId && parentId === id) {
35+
return NextResponse.json({ error: 'Folder cannot be its own parent' }, { status: 400 })
36+
}
37+
38+
// Check for circular references if parentId is provided
39+
if (parentId) {
40+
const wouldCreateCycle = await checkForCircularReference(id, parentId)
41+
if (wouldCreateCycle) {
42+
return NextResponse.json(
43+
{ error: 'Cannot create circular folder reference' },
44+
{ status: 400 }
45+
)
46+
}
47+
}
48+
49+
// Update the folder
50+
const updates: any = { updatedAt: new Date() }
51+
if (name !== undefined) updates.name = name.trim()
52+
if (color !== undefined) updates.color = color
53+
if (isExpanded !== undefined) updates.isExpanded = isExpanded
54+
if (parentId !== undefined) updates.parentId = parentId || null
55+
56+
const [updatedFolder] = await db
57+
.update(workflowFolder)
58+
.set(updates)
59+
.where(eq(workflowFolder.id, id))
60+
.returning()
61+
62+
logger.info('Updated folder:', { id, updates })
63+
64+
return NextResponse.json({ folder: updatedFolder })
65+
} catch (error) {
66+
logger.error('Error updating folder:', { error })
67+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
68+
}
69+
}
70+
71+
// DELETE - Delete a folder
72+
export async function DELETE(
73+
request: NextRequest,
74+
{ params }: { params: Promise<{ id: string }> }
75+
) {
76+
try {
77+
const session = await getSession()
78+
if (!session?.user?.id) {
79+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
80+
}
81+
82+
const { id } = await params
83+
const { searchParams } = new URL(request.url)
84+
const moveWorkflowsTo = searchParams.get('moveWorkflowsTo') // Optional: move workflows to another folder
85+
86+
// Verify the folder exists and belongs to the user
87+
const existingFolder = await db
88+
.select()
89+
.from(workflowFolder)
90+
.where(and(eq(workflowFolder.id, id), eq(workflowFolder.userId, session.user.id)))
91+
.then((rows) => rows[0])
92+
93+
if (!existingFolder) {
94+
return NextResponse.json({ error: 'Folder not found' }, { status: 404 })
95+
}
96+
97+
// Check if folder has child folders
98+
const childFolders = await db
99+
.select({ id: workflowFolder.id })
100+
.from(workflowFolder)
101+
.where(eq(workflowFolder.parentId, id))
102+
103+
// Check if folder has workflows
104+
const workflowsInFolder = await db
105+
.select({ id: workflow.id })
106+
.from(workflow)
107+
.where(eq(workflow.folderId, id))
108+
109+
// Handle child folders - move them to parent or root
110+
if (childFolders.length > 0) {
111+
await db
112+
.update(workflowFolder)
113+
.set({
114+
parentId: existingFolder.parentId, // Move to the parent of the deleted folder
115+
updatedAt: new Date(),
116+
})
117+
.where(eq(workflowFolder.parentId, id))
118+
}
119+
120+
// Handle workflows in the folder
121+
if (workflowsInFolder.length > 0) {
122+
const newFolderId = moveWorkflowsTo || null // Move to specified folder or root
123+
await db
124+
.update(workflow)
125+
.set({
126+
folderId: newFolderId,
127+
updatedAt: new Date(),
128+
})
129+
.where(eq(workflow.folderId, id))
130+
}
131+
132+
// Delete the folder
133+
await db.delete(workflowFolder).where(eq(workflowFolder.id, id))
134+
135+
logger.info('Deleted folder:', {
136+
id,
137+
childFoldersCount: childFolders.length,
138+
workflowsCount: workflowsInFolder.length,
139+
movedWorkflowsTo: moveWorkflowsTo,
140+
})
141+
142+
return NextResponse.json({
143+
success: true,
144+
movedItems: {
145+
childFolders: childFolders.length,
146+
workflows: workflowsInFolder.length,
147+
},
148+
})
149+
} catch (error) {
150+
logger.error('Error deleting folder:', { error })
151+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
152+
}
153+
}
154+
155+
// Helper function to check for circular references
156+
async function checkForCircularReference(folderId: string, parentId: string): Promise<boolean> {
157+
let currentParentId: string | null = parentId
158+
const visited = new Set<string>()
159+
160+
while (currentParentId) {
161+
if (visited.has(currentParentId)) {
162+
return true // Circular reference detected
163+
}
164+
165+
if (currentParentId === folderId) {
166+
return true // Would create a cycle
167+
}
168+
169+
visited.add(currentParentId)
170+
171+
// Get the parent of the current parent
172+
const parent: { parentId: string | null } | undefined = await db
173+
.select({ parentId: workflowFolder.parentId })
174+
.from(workflowFolder)
175+
.where(eq(workflowFolder.id, currentParentId))
176+
.then((rows) => rows[0])
177+
178+
currentParentId = parent?.parentId || null
179+
}
180+
181+
return false
182+
}

apps/sim/app/api/folders/route.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { and, asc, desc, eq, isNull } from 'drizzle-orm'
2+
import { type NextRequest, NextResponse } from 'next/server'
3+
import { getSession } from '@/lib/auth'
4+
import { createLogger } from '@/lib/logs/console-logger'
5+
import { db } from '@/db'
6+
import { workflowFolder } from '@/db/schema'
7+
8+
const logger = createLogger('FoldersAPI')
9+
10+
// GET - Fetch folders for a workspace
11+
export async function GET(request: NextRequest) {
12+
try {
13+
const session = await getSession()
14+
if (!session?.user?.id) {
15+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
16+
}
17+
18+
const { searchParams } = new URL(request.url)
19+
const workspaceId = searchParams.get('workspaceId')
20+
21+
if (!workspaceId) {
22+
return NextResponse.json({ error: 'Workspace ID is required' }, { status: 400 })
23+
}
24+
25+
// Fetch all folders for the workspace, ordered by sortOrder and createdAt
26+
const folders = await db
27+
.select()
28+
.from(workflowFolder)
29+
.where(
30+
and(eq(workflowFolder.workspaceId, workspaceId), eq(workflowFolder.userId, session.user.id))
31+
)
32+
.orderBy(asc(workflowFolder.sortOrder), asc(workflowFolder.createdAt))
33+
34+
return NextResponse.json({ folders })
35+
} catch (error) {
36+
logger.error('Error fetching folders:', { error })
37+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
38+
}
39+
}
40+
41+
// POST - Create a new folder
42+
export async function POST(request: NextRequest) {
43+
try {
44+
const session = await getSession()
45+
if (!session?.user?.id) {
46+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
47+
}
48+
49+
const body = await request.json()
50+
const { name, workspaceId, parentId, color } = body
51+
52+
if (!name || !workspaceId) {
53+
return NextResponse.json({ error: 'Name and workspace ID are required' }, { status: 400 })
54+
}
55+
56+
// Generate a new ID
57+
const id = crypto.randomUUID()
58+
59+
// Use transaction to ensure sortOrder consistency
60+
const newFolder = await db.transaction(async (tx) => {
61+
// Get the next sort order for the parent (or root level)
62+
const existingFolders = await tx
63+
.select({ sortOrder: workflowFolder.sortOrder })
64+
.from(workflowFolder)
65+
.where(
66+
and(
67+
eq(workflowFolder.workspaceId, workspaceId),
68+
eq(workflowFolder.userId, session.user.id),
69+
parentId ? eq(workflowFolder.parentId, parentId) : isNull(workflowFolder.parentId)
70+
)
71+
)
72+
.orderBy(desc(workflowFolder.sortOrder))
73+
.limit(1)
74+
75+
const nextSortOrder = existingFolders.length > 0 ? existingFolders[0].sortOrder + 1 : 0
76+
77+
// Insert the new folder within the same transaction
78+
const [folder] = await tx
79+
.insert(workflowFolder)
80+
.values({
81+
id,
82+
name: name.trim(),
83+
userId: session.user.id,
84+
workspaceId,
85+
parentId: parentId || null,
86+
color: color || '#6B7280',
87+
sortOrder: nextSortOrder,
88+
})
89+
.returning()
90+
91+
return folder
92+
})
93+
94+
logger.info('Created new folder:', { id, name, workspaceId, parentId })
95+
96+
return NextResponse.json({ folder: newFolder })
97+
} catch (error) {
98+
logger.error('Error creating folder:', { error })
99+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
100+
}
101+
}

0 commit comments

Comments
 (0)