Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 5 additions & 13 deletions apps/sim/app/api/workflows/sync/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import crypto from 'crypto'
import { and, desc, eq, isNull } from 'drizzle-orm'
import { and, eq, isNull } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
Expand Down Expand Up @@ -155,22 +155,14 @@ export async function GET(request: Request) {
if (workspaceId) {
// Filter by workspace ID only, not user ID
// This allows sharing workflows across workspace members
// Order by createdAt desc to match frontend sorting by lastModified
workflows = await db
.select()
.from(workflow)
.where(eq(workflow.workspaceId, workspaceId))
.orderBy(desc(workflow.createdAt))
workflows = await db.select().from(workflow).where(eq(workflow.workspaceId, workspaceId))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Missing orderBy clause - workflows are no longer sorted by creation date which could affect frontend display order

} else {
// Filter by user ID only, including workflows without workspace IDs
// Order by createdAt desc to match frontend sorting by lastModified
workflows = await db
.select()
.from(workflow)
.where(eq(workflow.userId, userId))
.orderBy(desc(workflow.createdAt))
workflows = await db.select().from(workflow).where(eq(workflow.userId, userId))
}

const elapsed = Date.now() - startTime

// Return the workflows
return NextResponse.json({ data: workflows }, { status: 200 })
} catch (error: any) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,45 +333,23 @@ export const WorkspaceHeader = React.memo<WorkspaceHeaderProps>(
}, [sessionData?.user?.id, fetchSubscriptionStatus, fetchWorkspaces])

const switchWorkspace = useCallback(
async (workspace: Workspace) => {
(workspace: Workspace) => {
// If already on this workspace, close dropdown and do nothing else
if (activeWorkspace?.id === workspace.id) {
setWorkspaceDropdownOpen(false)
return
}

// Close dropdown immediately for responsive feel
setActiveWorkspace(workspace)
setWorkspaceDropdownOpen(false)

try {
// Update UI state optimistically
setActiveWorkspace(workspace)

// Switch workspace data first with the explicit workspace ID
// This ensures the data switch happens with the correct ID regardless of URL timing
await switchToWorkspace(workspace.id)

// Then update URL - this will trigger useParams updates in other components
router.push(`/workspace/${workspace.id}/w`)
} catch (error) {
// If workspace switch fails, revert the optimistic UI update
logger.error('Failed to switch workspace:', error)
// Revert to previous workspace if we can identify it
const currentWorkspaces = workspaces
const fallbackWorkspace = currentWorkspaces.find((w) => w.id === currentWorkspaceId)
if (fallbackWorkspace) {
setActiveWorkspace(fallbackWorkspace)
}
}
// Use full workspace switch which now handles localStorage automatically
switchToWorkspace(workspace.id)

// Update URL to include workspace ID
router.push(`/workspace/${workspace.id}/w`)
},
[
activeWorkspace?.id,
switchToWorkspace,
router,
setWorkspaceDropdownOpen,
workspaces,
currentWorkspaceId,
]
[activeWorkspace?.id, switchToWorkspace, router, setWorkspaceDropdownOpen]
)

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

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

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

// If deleted workspace was active, switch to another workspace
if (activeWorkspace?.id === id && updatedWorkspaces.length > 0) {
const newWorkspace = updatedWorkspaces[0]
setActiveWorkspace(newWorkspace)

// Use the specialized method for handling workspace deletion with explicit workspace ID
useWorkflowRegistry.getState().handleWorkspaceDeletion(newWorkspace.id)

// Update URL to the new workspace
router.push(`/workspace/${newWorkspace.id}/w`)
// Use the specialized method for handling workspace deletion
const newWorkspaceId = updatedWorkspaces[0].id
useWorkflowRegistry.getState().handleWorkspaceDeletion(newWorkspaceId)
setActiveWorkspace(updatedWorkspaces[0])
}
Comment on lines 466 to 472
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Missing router.push after workspace deletion. User may remain on deleted workspace URL.

Suggested change
// If deleted workspace was active, switch to another workspace
if (activeWorkspace?.id === id && updatedWorkspaces.length > 0) {
const newWorkspace = updatedWorkspaces[0]
setActiveWorkspace(newWorkspace)
// Use the specialized method for handling workspace deletion with explicit workspace ID
useWorkflowRegistry.getState().handleWorkspaceDeletion(newWorkspace.id)
// Update URL to the new workspace
router.push(`/workspace/${newWorkspace.id}/w`)
// Use the specialized method for handling workspace deletion
const newWorkspaceId = updatedWorkspaces[0].id
useWorkflowRegistry.getState().handleWorkspaceDeletion(newWorkspaceId)
setActiveWorkspace(updatedWorkspaces[0])
}
// If deleted workspace was active, switch to another workspace
if (activeWorkspace?.id === id && updatedWorkspaces.length > 0) {
// Use the specialized method for handling workspace deletion
const newWorkspaceId = updatedWorkspaces[0].id
useWorkflowRegistry.getState().handleWorkspaceDeletion(newWorkspaceId)
setActiveWorkspace(updatedWorkspaces[0])
// Navigate to the new workspace
router.push(`/workspace/${newWorkspaceId}/w`)
}


setWorkspaceDropdownOpen(false)
Expand All @@ -504,7 +478,7 @@ export const WorkspaceHeader = React.memo<WorkspaceHeaderProps>(
setIsDeleting(false)
}
},
[workspaces, activeWorkspace?.id, router]
[workspaces, activeWorkspace?.id]
)

const openEditModal = useCallback(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
useGlobalShortcuts,
} from '@/app/workspace/[workspaceId]/w/hooks/use-keyboard-shortcuts'
import { useSidebarStore } from '@/stores/sidebar/store'
import { isWorkspaceInTransition, useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import type { WorkflowMetadata } from '@/stores/workflows/registry/types'
import { useUserPermissionsContext } from '../providers/workspace-permissions-provider'
import { CreateMenu } from './components/create-menu/create-menu'
Expand All @@ -32,16 +32,13 @@ const IS_DEV = process.env.NODE_ENV === 'development'
export function Sidebar() {
useGlobalShortcuts()

const router = useRouter()
const params = useParams()
const workspaceId = params.workspaceId as string

const { workflows, createWorkflow, isLoading: workflowsLoading } = useWorkflowRegistry()
const { isPending: sessionLoading } = useSession()
const userPermissions = useUserPermissionsContext()

// Simple loading logic: wait for workflows and valid workspaceId, plus workspace transitions
const isLoading = workflowsLoading || sessionLoading || !workspaceId || isWorkspaceInTransition()
const isLoading = workflowsLoading || sessionLoading
const router = useRouter()
const params = useParams()
const workspaceId = params.workspaceId as string
const pathname = usePathname()

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

// Filter workflows for current workspace (database already sorted)
// Separate regular workflows from temporary marketplace workflows
const { regularWorkflows, tempWorkflows } = useMemo(() => {
if (isLoading) return { regularWorkflows: [], tempWorkflows: [] }

const regular: WorkflowMetadata[] = []
const temp: WorkflowMetadata[] = []

Object.values(workflows).forEach((workflow) => {
if (workflow.workspaceId === workspaceId || !workflow.workspaceId) {
if (workflow.marketplaceData?.status === 'temp') {
temp.push(workflow)
} else {
regular.push(workflow)
if (!isLoading) {
Object.values(workflows).forEach((workflow) => {
if (workflow.workspaceId === workspaceId || !workflow.workspaceId) {
if (workflow.marketplaceData?.status === 'temp') {
temp.push(workflow)
} else {
regular.push(workflow)
}
}
})

// Sort by last modified date (newest first)
const sortByLastModified = (a: WorkflowMetadata, b: WorkflowMetadata) => {
const dateA =
a.lastModified instanceof Date
? a.lastModified.getTime()
: new Date(a.lastModified).getTime()
const dateB =
b.lastModified instanceof Date
? b.lastModified.getTime()
: new Date(b.lastModified).getTime()
return dateB - dateA
}
})

regular.sort(sortByLastModified)
temp.sort(sortByLastModified)
}

return { regularWorkflows: regular, tempWorkflows: temp }
}, [workflows, workspaceId, isLoading])
}, [workflows, isLoading, workspaceId])

// Create workflow handler
const handleCreateWorkflow = async (folderId?: string) => {
Expand Down
23 changes: 3 additions & 20 deletions apps/sim/app/workspace/[workspaceId]/w/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,18 @@ import { useWorkflowRegistry } from '@/stores/workflows/registry/store'

export default function WorkflowsPage() {
const router = useRouter()
const { workflows, isLoading, loadWorkflows } = useWorkflowRegistry()
const { workflows, isLoading } = useWorkflowRegistry()

const params = useParams()
const workspaceId = params.workspaceId as string

// Load workflows for this specific workspace when component mounts or workspaceId changes
// Only load if we don't already have workflows for this workspace (to prevent duplicate calls during workspace switches)
useEffect(() => {
if (workspaceId) {
// Check if we already have workflows for this workspace
const workflowIds = Object.keys(workflows)
const hasWorkflowsForWorkspace =
workflowIds.length > 0 &&
Object.values(workflows).some((w) => w.workspaceId === workspaceId)

// Only load if we don't have workflows for this workspace and we're not loading
if (!hasWorkflowsForWorkspace && !isLoading) {
loadWorkflows(workspaceId)
}
}
}, [workspaceId, loadWorkflows, isLoading, workflows])
const workspaceId = params.workspaceId

useEffect(() => {
// Wait for workflows to load
if (isLoading) return

const workflowIds = Object.keys(workflows)

// If we have workflows, redirect to the first one (database already sorted by lastModified desc)
// If we have workflows, redirect to the first one
if (workflowIds.length > 0) {
router.replace(`/workspace/${workspaceId}/w/${workflowIds[0]}`)
return
Comment on lines +21 to 24
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: No verification that workflows belong to current workspace before redirect. Could cause incorrect routing

Expand Down
1 change: 1 addition & 0 deletions apps/sim/contexts/socket-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
})

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

Expand Down
1 change: 0 additions & 1 deletion apps/sim/stores/workflows/registry/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,6 @@ async function fetchWorkflowsFromDB(workspaceId?: string): Promise<void> {
})

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