Skip to content

Commit 7488b1f

Browse files
authored
feat(sidebar/workspaces) (#312)
* improvement(sidebar): full sidebar UI complete, adding collapsed state styling improvements * improvement(ui): full sidebar state * improvement(ui/ux): finished sidebar styling * improvement(ui): scrollable workflows * improvement(ui/ux): added loading state * improvement(sidebar): adding loading for user and plan * fix: skeleton only on initial load * improvement(control-bar): adjusted styling * feat(workspaces): added schema for workspaces and setup workspace header UI * feat(workspaces): added route to fetch for workspace header * fix: deployment error * fix: deployment error * feat(workspaces): added workspace id to workflow table * feat(workspaces): ran migrations to dev * feat(workspaces): completed connecting registries to workspaces and added migration route * fix(workspaces): N+1 database queries * improvement(ui/ux): sidebar * hotfix(sidebar): hid top nav section * fix(ux): adding workflow over header dropdown
1 parent 47d3cbe commit 7488b1f

29 files changed

Lines changed: 4312 additions & 391 deletions

File tree

sim/app/api/workflows/sync/route.ts

Lines changed: 137 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { NextRequest, NextResponse } from 'next/server'
2-
import { eq } from 'drizzle-orm'
2+
import { and, eq, isNull } from 'drizzle-orm'
33
import { z } from 'zod'
44
import { getSession } from '@/lib/auth'
55
import { createLogger } from '@/lib/logs/console-logger'
66
import { db } from '@/db'
7-
import { workflow } from '@/db/schema'
7+
import { workflow, workspace } from '@/db/schema'
88

99
const logger = createLogger('WorkflowAPI')
1010

@@ -39,14 +39,18 @@ const WorkflowSchema = z.object({
3939
color: z.string().optional(),
4040
state: WorkflowStateSchema,
4141
marketplaceData: MarketplaceDataSchema,
42+
workspaceId: z.string().optional(),
4243
})
4344

4445
const SyncPayloadSchema = z.object({
4546
workflows: z.record(z.string(), WorkflowSchema),
47+
workspaceId: z.string().optional(),
4648
})
4749

4850
export async function GET(request: Request) {
4951
const requestId = crypto.randomUUID().slice(0, 8)
52+
const url = new URL(request.url)
53+
const workspaceId = url.searchParams.get('workspaceId')
5054

5155
try {
5256
// Get the session directly in the API route
@@ -58,8 +62,42 @@ export async function GET(request: Request) {
5862

5963
const userId = session.user.id
6064

61-
// Fetch all workflows for the user
62-
const workflows = await db.select().from(workflow).where(eq(workflow.userId, userId))
65+
// If workspaceId is provided, verify it exists first
66+
if (workspaceId) {
67+
const workspaceExists = await db
68+
.select({ id: workspace.id })
69+
.from(workspace)
70+
.where(eq(workspace.id, workspaceId))
71+
.then(rows => rows.length > 0)
72+
73+
if (!workspaceExists) {
74+
logger.warn(`[${requestId}] Attempt to fetch workflows for non-existent workspace: ${workspaceId}`)
75+
return NextResponse.json({ error: 'Workspace not found', code: 'WORKSPACE_NOT_FOUND' }, { status: 404 })
76+
}
77+
78+
// Migrate any orphaned workflows to this workspace
79+
await migrateOrphanedWorkflows(userId, workspaceId)
80+
}
81+
82+
// Fetch workflows for the user
83+
let workflows
84+
85+
if (workspaceId) {
86+
// Filter by user ID and workspace ID
87+
workflows = await db
88+
.select()
89+
.from(workflow)
90+
.where(and(
91+
eq(workflow.userId, userId),
92+
eq(workflow.workspaceId, workspaceId)
93+
))
94+
} else {
95+
// Filter by user ID only, including workflows without workspace IDs
96+
workflows = await db
97+
.select()
98+
.from(workflow)
99+
.where(eq(workflow.userId, userId))
100+
}
63101

64102
// Return the workflows
65103
return NextResponse.json({ data: workflows }, { status: 200 })
@@ -69,6 +107,40 @@ export async function GET(request: Request) {
69107
}
70108
}
71109

110+
// Helper function to migrate orphaned workflows to a workspace
111+
async function migrateOrphanedWorkflows(userId: string, workspaceId: string) {
112+
try {
113+
// Find workflows without workspace IDs for this user
114+
const orphanedWorkflows = await db
115+
.select({ id: workflow.id })
116+
.from(workflow)
117+
.where(and(
118+
eq(workflow.userId, userId),
119+
isNull(workflow.workspaceId)
120+
))
121+
122+
if (orphanedWorkflows.length === 0) {
123+
return // No orphaned workflows to migrate
124+
}
125+
126+
logger.info(`Migrating ${orphanedWorkflows.length} orphaned workflows to workspace ${workspaceId}`)
127+
128+
// Update each workflow to associate it with the provided workspace
129+
for (const { id } of orphanedWorkflows) {
130+
await db
131+
.update(workflow)
132+
.set({
133+
workspaceId: workspaceId,
134+
updatedAt: new Date()
135+
})
136+
.where(eq(workflow.id, id))
137+
}
138+
} catch (error) {
139+
logger.error('Error migrating orphaned workflows:', error)
140+
// Continue execution even if migration fails
141+
}
142+
}
143+
72144
export async function POST(req: NextRequest) {
73145
const requestId = crypto.randomUUID().slice(0, 8)
74146

@@ -82,20 +154,32 @@ export async function POST(req: NextRequest) {
82154
const body = await req.json()
83155

84156
try {
85-
const { workflows: clientWorkflows } = SyncPayloadSchema.parse(body)
157+
const { workflows: clientWorkflows, workspaceId } = SyncPayloadSchema.parse(body)
86158

87159
// CRITICAL SAFEGUARD: Prevent wiping out existing workflows
88160
// If client is sending empty workflows object, first check if user has existing workflows
89161
if (Object.keys(clientWorkflows).length === 0) {
90-
const existingWorkflows = await db
91-
.select()
92-
.from(workflow)
93-
.where(eq(workflow.userId, session.user.id))
162+
let existingWorkflows;
163+
164+
if (workspaceId) {
165+
existingWorkflows = await db
166+
.select()
167+
.from(workflow)
168+
.where(and(
169+
eq(workflow.userId, session.user.id),
170+
eq(workflow.workspaceId, workspaceId)
171+
));
172+
} else {
173+
existingWorkflows = await db
174+
.select()
175+
.from(workflow)
176+
.where(eq(workflow.userId, session.user.id));
177+
}
94178

95179
// If user has existing workflows, but client sends empty, reject the sync
96180
if (existingWorkflows.length > 0) {
97181
logger.warn(
98-
`[${requestId}] Prevented data loss: Client attempted to sync empty workflows while DB has ${existingWorkflows.length} workflows`
182+
`[${requestId}] Prevented data loss: Client attempted to sync empty workflows while DB has ${existingWorkflows.length} workflows in workspace ${workspaceId || 'default'}`
99183
)
100184
return NextResponse.json(
101185
{
@@ -107,11 +191,41 @@ export async function POST(req: NextRequest) {
107191
}
108192
}
109193

194+
// Validate that the workspace exists if one is specified
195+
if (workspaceId) {
196+
const workspaceExists = await db
197+
.select({ id: workspace.id })
198+
.from(workspace)
199+
.where(eq(workspace.id, workspaceId))
200+
.then(rows => rows.length > 0)
201+
202+
if (!workspaceExists) {
203+
logger.warn(`[${requestId}] Attempt to sync workflows to non-existent workspace: ${workspaceId}`)
204+
return NextResponse.json({
205+
error: 'Workspace not found',
206+
code: 'WORKSPACE_NOT_FOUND'
207+
}, { status: 404 })
208+
}
209+
}
210+
110211
// Get all workflows for the user from the database
111-
const dbWorkflows = await db
112-
.select()
113-
.from(workflow)
114-
.where(eq(workflow.userId, session.user.id))
212+
// If workspaceId is provided, only get workflows for that workspace
213+
let dbWorkflows;
214+
215+
if (workspaceId) {
216+
dbWorkflows = await db
217+
.select()
218+
.from(workflow)
219+
.where(and(
220+
eq(workflow.userId, session.user.id),
221+
eq(workflow.workspaceId, workspaceId)
222+
))
223+
} else {
224+
dbWorkflows = await db
225+
.select()
226+
.from(workflow)
227+
.where(eq(workflow.userId, session.user.id))
228+
}
115229

116230
const now = new Date()
117231
const operations: Promise<any>[] = []
@@ -130,13 +244,17 @@ export async function POST(req: NextRequest) {
130244
if (clientWorkflow.state.isPublished && !clientWorkflow.marketplaceData) {
131245
clientWorkflow.marketplaceData = { id: clientWorkflow.id, status: 'owner' }
132246
}
247+
248+
// Ensure the workflow has the correct workspaceId
249+
const effectiveWorkspaceId = clientWorkflow.workspaceId || workspaceId;
133250

134251
if (!dbWorkflow) {
135252
// New workflow - create
136253
operations.push(
137254
db.insert(workflow).values({
138255
id: clientWorkflow.id,
139256
userId: session.user.id,
257+
workspaceId: effectiveWorkspaceId,
140258
name: clientWorkflow.name,
141259
description: clientWorkflow.description,
142260
color: clientWorkflow.color,
@@ -154,6 +272,7 @@ export async function POST(req: NextRequest) {
154272
dbWorkflow.name !== clientWorkflow.name ||
155273
dbWorkflow.description !== clientWorkflow.description ||
156274
dbWorkflow.color !== clientWorkflow.color ||
275+
dbWorkflow.workspaceId !== effectiveWorkspaceId ||
157276
JSON.stringify(dbWorkflow.marketplaceData) !==
158277
JSON.stringify(clientWorkflow.marketplaceData)
159278

@@ -165,6 +284,7 @@ export async function POST(req: NextRequest) {
165284
name: clientWorkflow.name,
166285
description: clientWorkflow.description,
167286
color: clientWorkflow.color,
287+
workspaceId: effectiveWorkspaceId,
168288
state: clientWorkflow.state,
169289
marketplaceData: clientWorkflow.marketplaceData || null,
170290
lastSynced: now,
@@ -177,8 +297,10 @@ export async function POST(req: NextRequest) {
177297
}
178298

179299
// Handle deletions - workflows in DB but not in client
300+
// Only delete workflows for the current workspace!
180301
for (const dbWorkflow of dbWorkflows) {
181-
if (!processedIds.has(dbWorkflow.id)) {
302+
if (!processedIds.has(dbWorkflow.id) &&
303+
(!workspaceId || dbWorkflow.workspaceId === workspaceId)) {
182304
operations.push(db.delete(workflow).where(eq(workflow.id, dbWorkflow.id)))
183305
}
184306
}

0 commit comments

Comments
 (0)