Skip to content

Commit 0049644

Browse files
icecrasher321Vikhyath Mondretiwaleedlatif1
authored
fix(workspace url id bug): switch workspace bug (#564)
* fix switch workspace bug" * fix(workspaces): resolve workflow selection ordering issue, eliminate race conditions during workspace transitions --------- Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-Air.attlocal.net> Co-authored-by: Waleed Latif <walif6@gmail.com>
1 parent 8764954 commit 0049644

6 files changed

Lines changed: 94 additions & 56 deletions

File tree

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import crypto from 'crypto'
2-
import { and, eq, isNull } from 'drizzle-orm'
2+
import { and, desc, eq, isNull } from 'drizzle-orm'
33
import { NextResponse } from 'next/server'
44
import { getSession } from '@/lib/auth'
55
import { createLogger } from '@/lib/logs/console-logger'
@@ -155,14 +155,22 @@ export async function GET(request: Request) {
155155
if (workspaceId) {
156156
// Filter by workspace ID only, not user ID
157157
// This allows sharing workflows across workspace members
158-
workflows = await db.select().from(workflow).where(eq(workflow.workspaceId, workspaceId))
158+
// Order by createdAt desc to match frontend sorting by lastModified
159+
workflows = await db
160+
.select()
161+
.from(workflow)
162+
.where(eq(workflow.workspaceId, workspaceId))
163+
.orderBy(desc(workflow.createdAt))
159164
} else {
160165
// Filter by user ID only, including workflows without workspace IDs
161-
workflows = await db.select().from(workflow).where(eq(workflow.userId, userId))
166+
// Order by createdAt desc to match frontend sorting by lastModified
167+
workflows = await db
168+
.select()
169+
.from(workflow)
170+
.where(eq(workflow.userId, userId))
171+
.orderBy(desc(workflow.createdAt))
162172
}
163173

164-
const elapsed = Date.now() - startTime
165-
166174
// Return the workflows
167175
return NextResponse.json({ data: workflows }, { status: 200 })
168176
} catch (error: any) {

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -333,23 +333,45 @@ export const WorkspaceHeader = React.memo<WorkspaceHeaderProps>(
333333
}, [sessionData?.user?.id, fetchSubscriptionStatus, fetchWorkspaces])
334334

335335
const switchWorkspace = useCallback(
336-
(workspace: Workspace) => {
336+
async (workspace: Workspace) => {
337337
// If already on this workspace, close dropdown and do nothing else
338338
if (activeWorkspace?.id === workspace.id) {
339339
setWorkspaceDropdownOpen(false)
340340
return
341341
}
342342

343-
setActiveWorkspace(workspace)
343+
// Close dropdown immediately for responsive feel
344344
setWorkspaceDropdownOpen(false)
345345

346-
// Use full workspace switch which now handles localStorage automatically
347-
switchToWorkspace(workspace.id)
348-
349-
// Update URL to include workspace ID
350-
router.push(`/workspace/${workspace.id}/w`)
346+
try {
347+
// Update UI state optimistically
348+
setActiveWorkspace(workspace)
349+
350+
// Switch workspace data first with the explicit workspace ID
351+
// This ensures the data switch happens with the correct ID regardless of URL timing
352+
await switchToWorkspace(workspace.id)
353+
354+
// Then update URL - this will trigger useParams updates in other components
355+
router.push(`/workspace/${workspace.id}/w`)
356+
} catch (error) {
357+
// If workspace switch fails, revert the optimistic UI update
358+
logger.error('Failed to switch workspace:', error)
359+
// Revert to previous workspace if we can identify it
360+
const currentWorkspaces = workspaces
361+
const fallbackWorkspace = currentWorkspaces.find((w) => w.id === currentWorkspaceId)
362+
if (fallbackWorkspace) {
363+
setActiveWorkspace(fallbackWorkspace)
364+
}
365+
}
351366
},
352-
[activeWorkspace?.id, switchToWorkspace, router, setWorkspaceDropdownOpen]
367+
[
368+
activeWorkspace?.id,
369+
switchToWorkspace,
370+
router,
371+
setWorkspaceDropdownOpen,
372+
workspaces,
373+
currentWorkspaceId,
374+
]
353375
)
354376

355377
const handleCreateWorkspace = useCallback(
@@ -372,11 +394,11 @@ export const WorkspaceHeader = React.memo<WorkspaceHeaderProps>(
372394
setWorkspaces((prev) => [...prev, newWorkspace])
373395
setActiveWorkspace(newWorkspace)
374396

375-
// Use switchToWorkspace to properly load workflows for the new workspace
397+
// Switch workspace data first with the explicit workspace ID
376398
// This will clear existing workflows, set loading state, and fetch workflows from DB
377399
switchToWorkspace(newWorkspace.id)
378400

379-
// Update URL to include new workspace ID
401+
// Then update URL to include new workspace ID
380402
router.push(`/workspace/${newWorkspace.id}/w`)
381403
}
382404
} catch (err) {
@@ -465,10 +487,14 @@ export const WorkspaceHeader = React.memo<WorkspaceHeaderProps>(
465487

466488
// If deleted workspace was active, switch to another workspace
467489
if (activeWorkspace?.id === id && updatedWorkspaces.length > 0) {
468-
// Use the specialized method for handling workspace deletion
469-
const newWorkspaceId = updatedWorkspaces[0].id
470-
useWorkflowRegistry.getState().handleWorkspaceDeletion(newWorkspaceId)
471-
setActiveWorkspace(updatedWorkspaces[0])
490+
const newWorkspace = updatedWorkspaces[0]
491+
setActiveWorkspace(newWorkspace)
492+
493+
// Use the specialized method for handling workspace deletion with explicit workspace ID
494+
useWorkflowRegistry.getState().handleWorkspaceDeletion(newWorkspace.id)
495+
496+
// Update URL to the new workspace
497+
router.push(`/workspace/${newWorkspace.id}/w`)
472498
}
473499

474500
setWorkspaceDropdownOpen(false)
@@ -478,7 +504,7 @@ export const WorkspaceHeader = React.memo<WorkspaceHeaderProps>(
478504
setIsDeleting(false)
479505
}
480506
},
481-
[workspaces, activeWorkspace?.id]
507+
[workspaces, activeWorkspace?.id, router]
482508
)
483509

484510
const openEditModal = useCallback(

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx

Lines changed: 19 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
useGlobalShortcuts,
1414
} from '@/app/workspace/[workspaceId]/w/hooks/use-keyboard-shortcuts'
1515
import { useSidebarStore } from '@/stores/sidebar/store'
16-
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
16+
import { isWorkspaceInTransition, useWorkflowRegistry } from '@/stores/workflows/registry/store'
1717
import type { WorkflowMetadata } from '@/stores/workflows/registry/types'
1818
import { useUserPermissionsContext } from '../providers/workspace-permissions-provider'
1919
import { CreateMenu } from './components/create-menu/create-menu'
@@ -32,13 +32,16 @@ const IS_DEV = process.env.NODE_ENV === 'development'
3232
export function Sidebar() {
3333
useGlobalShortcuts()
3434

35-
const { workflows, createWorkflow, isLoading: workflowsLoading } = useWorkflowRegistry()
36-
const { isPending: sessionLoading } = useSession()
37-
const userPermissions = useUserPermissionsContext()
38-
const isLoading = workflowsLoading || sessionLoading
3935
const router = useRouter()
4036
const params = useParams()
4137
const workspaceId = params.workspaceId as string
38+
39+
const { workflows, createWorkflow, isLoading: workflowsLoading } = useWorkflowRegistry()
40+
const { isPending: sessionLoading } = useSession()
41+
const userPermissions = useUserPermissionsContext()
42+
43+
// Simple loading logic: wait for workflows and valid workspaceId, plus workspace transitions
44+
const isLoading = workflowsLoading || sessionLoading || !workspaceId || isWorkspaceInTransition()
4245
const pathname = usePathname()
4346

4447
const [showSettings, setShowSettings] = useState(false)
@@ -57,41 +60,25 @@ export function Sidebar() {
5760
}
5861
}, [showSettings, showHelp, showInviteMembers, setAnyModalOpen])
5962

60-
// Separate regular workflows from temporary marketplace workflows
63+
// Filter workflows for current workspace (database already sorted)
6164
const { regularWorkflows, tempWorkflows } = useMemo(() => {
65+
if (isLoading) return { regularWorkflows: [], tempWorkflows: [] }
66+
6267
const regular: WorkflowMetadata[] = []
6368
const temp: WorkflowMetadata[] = []
6469

65-
if (!isLoading) {
66-
Object.values(workflows).forEach((workflow) => {
67-
if (workflow.workspaceId === workspaceId || !workflow.workspaceId) {
68-
if (workflow.marketplaceData?.status === 'temp') {
69-
temp.push(workflow)
70-
} else {
71-
regular.push(workflow)
72-
}
70+
Object.values(workflows).forEach((workflow) => {
71+
if (workflow.workspaceId === workspaceId || !workflow.workspaceId) {
72+
if (workflow.marketplaceData?.status === 'temp') {
73+
temp.push(workflow)
74+
} else {
75+
regular.push(workflow)
7376
}
74-
})
75-
76-
// Sort by last modified date (newest first)
77-
const sortByLastModified = (a: WorkflowMetadata, b: WorkflowMetadata) => {
78-
const dateA =
79-
a.lastModified instanceof Date
80-
? a.lastModified.getTime()
81-
: new Date(a.lastModified).getTime()
82-
const dateB =
83-
b.lastModified instanceof Date
84-
? b.lastModified.getTime()
85-
: new Date(b.lastModified).getTime()
86-
return dateB - dateA
8777
}
88-
89-
regular.sort(sortByLastModified)
90-
temp.sort(sortByLastModified)
91-
}
78+
})
9279

9380
return { regularWorkflows: regular, tempWorkflows: temp }
94-
}, [workflows, isLoading, workspaceId])
81+
}, [workflows, workspaceId, isLoading])
9582

9683
// Create workflow handler
9784
const handleCreateWorkflow = async (folderId?: string) => {

apps/sim/app/workspace/[workspaceId]/w/page.tsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,35 @@ import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
77

88
export default function WorkflowsPage() {
99
const router = useRouter()
10-
const { workflows, isLoading } = useWorkflowRegistry()
10+
const { workflows, isLoading, loadWorkflows } = useWorkflowRegistry()
1111

1212
const params = useParams()
13-
const workspaceId = params.workspaceId
13+
const workspaceId = params.workspaceId as string
14+
15+
// Load workflows for this specific workspace when component mounts or workspaceId changes
16+
// Only load if we don't already have workflows for this workspace (to prevent duplicate calls during workspace switches)
17+
useEffect(() => {
18+
if (workspaceId) {
19+
// Check if we already have workflows for this workspace
20+
const workflowIds = Object.keys(workflows)
21+
const hasWorkflowsForWorkspace =
22+
workflowIds.length > 0 &&
23+
Object.values(workflows).some((w) => w.workspaceId === workspaceId)
24+
25+
// Only load if we don't have workflows for this workspace and we're not loading
26+
if (!hasWorkflowsForWorkspace && !isLoading) {
27+
loadWorkflows(workspaceId)
28+
}
29+
}
30+
}, [workspaceId, loadWorkflows, isLoading, workflows])
1431

1532
useEffect(() => {
1633
// Wait for workflows to load
1734
if (isLoading) return
1835

1936
const workflowIds = Object.keys(workflows)
2037

21-
// If we have workflows, redirect to the first one
38+
// If we have workflows, redirect to the first one (database already sorted by lastModified desc)
2239
if (workflowIds.length > 0) {
2340
router.replace(`/workspace/${workspaceId}/w/${workflowIds[0]}`)
2441
return

apps/sim/contexts/socket-context.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,6 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
270270
})
271271

272272
socketInstance.on('workflow-state', (state) => {
273-
logger.info('Received workflow state from server:', state)
274273
// This will be used to sync initial state when joining a workflow
275274
})
276275

apps/sim/stores/workflows/registry/store.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ async function fetchWorkflowsFromDB(workspaceId?: string): Promise<void> {
151151
})
152152

153153
// Only set first workflow as active if no active workflow is set and we have workflows
154+
// Database already returns workflows sorted by lastModified desc
154155
const currentState = useWorkflowRegistry.getState()
155156
if (!currentState.activeWorkflowId && Object.keys(registryWorkflows).length > 0) {
156157
const firstWorkflowId = Object.keys(registryWorkflows)[0]

0 commit comments

Comments
 (0)