From 756d07a8709151e775d65ed8718a1717a227273c Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Sat, 14 Jun 2025 16:32:19 -0700 Subject: [PATCH 01/27] remove local storage usage --- apps/sim/app/api/user/workspace/route.ts | 138 + apps/sim/app/api/workflows/[id]/route.ts | 64 +- apps/sim/app/api/workflows/sync/route.ts | 94 +- .../components/oauth-required-modal.tsx | 103 +- .../credential-selector.tsx | 33 +- .../components/confluence-file-selector.tsx | 37 +- .../components/google-drive-picker.tsx | 33 +- .../components/jira-issue-selector.tsx | 33 +- .../components/microsoft-file-selector.tsx | 33 +- .../components/teams-message-selector.tsx | 33 +- .../folder-selector/folder-selector.tsx | 37 +- .../components/jira-project-selector.tsx | 33 +- apps/sim/app/w/[id]/workflow.tsx | 18 +- .../components/credentials/credentials.tsx | 73 +- .../workspace-header/workspace-header.tsx | 36 +- apps/sim/db/migrations/0043_fair_roulette.sql | 2 + .../sim/db/migrations/meta/0043_snapshot.json | 3241 +++++++++++++++++ apps/sim/db/migrations/meta/_journal.json | 7 + apps/sim/db/schema.ts | 5 + apps/sim/stores/constants.ts | 6 +- apps/sim/stores/index.ts | 188 +- apps/sim/stores/workflows/index.ts | 132 +- apps/sim/stores/workflows/middleware.ts | 49 - apps/sim/stores/workflows/persistence.ts | 224 +- apps/sim/stores/workflows/registry/store.ts | 603 +-- apps/sim/stores/workflows/registry/types.ts | 6 + apps/sim/stores/workflows/subblock/store.ts | 479 ++- apps/sim/stores/workflows/sync.ts | 51 +- apps/sim/stores/workflows/workflow/store.ts | 29 +- 29 files changed, 4421 insertions(+), 1399 deletions(-) create mode 100644 apps/sim/app/api/user/workspace/route.ts create mode 100644 apps/sim/db/migrations/0043_fair_roulette.sql create mode 100644 apps/sim/db/migrations/meta/0043_snapshot.json diff --git a/apps/sim/app/api/user/workspace/route.ts b/apps/sim/app/api/user/workspace/route.ts new file mode 100644 index 00000000000..cf5b53d69b0 --- /dev/null +++ b/apps/sim/app/api/user/workspace/route.ts @@ -0,0 +1,138 @@ +import { and, eq } from 'drizzle-orm' +import { NextResponse } from 'next/server' +import { z } from 'zod' +import { getSession } from '@/lib/auth' +import { createLogger } from '@/lib/logs/console-logger' +import { db } from '@/db' +import { settings, workspaceMember } from '@/db/schema' + +const logger = createLogger('WorkspaceAPI') + +const WorkspaceRequestSchema = z.object({ + workspaceId: z.string(), +}) + +/** + * GET /api/user/workspace + * Retrieve user's last active workspace + */ +export async function GET() { + const requestId = crypto.randomUUID().slice(0, 8) + + try { + const session = await getSession() + + if (!session?.user?.id) { + logger.warn(`[${requestId}] Unauthenticated workspace request rejected`) + return NextResponse.json({ error: 'User not authenticated' }, { status: 401 }) + } + + const userId = session.user.id + + // Get user's last active workspace from settings + const userSettings = await db + .select({ lastActiveWorkspaceId: settings.lastActiveWorkspaceId }) + .from(settings) + .where(eq(settings.userId, userId)) + .limit(1) + + if (!userSettings.length || !userSettings[0].lastActiveWorkspaceId) { + // No workspace preference stored + return NextResponse.json({ workspaceId: null }, { status: 200 }) + } + + const workspaceId = userSettings[0].lastActiveWorkspaceId + + // Verify user still has access to this workspace + const hasAccess = await db + .select({ workspaceId: workspaceMember.workspaceId }) + .from(workspaceMember) + .where(and(eq(workspaceMember.userId, userId), eq(workspaceMember.workspaceId, workspaceId))) + .limit(1) + + if (!hasAccess.length) { + // User no longer has access to this workspace, clear the preference + await db + .update(settings) + .set({ lastActiveWorkspaceId: null }) + .where(eq(settings.userId, userId)) + + return NextResponse.json({ workspaceId: null }, { status: 200 }) + } + + return NextResponse.json({ workspaceId }, { status: 200 }) + } catch (error) { + logger.error(`[${requestId}] Error retrieving workspace preference`, error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} + +/** + * POST /api/user/workspace + * Store user's active workspace preference + */ +export async function POST(request: Request) { + const requestId = crypto.randomUUID().slice(0, 8) + + try { + const session = await getSession() + + if (!session?.user?.id) { + logger.warn(`[${requestId}] Unauthenticated workspace update rejected`) + return NextResponse.json({ error: 'User not authenticated' }, { status: 401 }) + } + + const userId = session.user.id + const body = await request.json() + + // Validate request body + const validationResult = WorkspaceRequestSchema.safeParse(body) + if (!validationResult.success) { + logger.warn(`[${requestId}] Invalid workspace request data`, { + errors: validationResult.error.errors, + }) + return NextResponse.json( + { error: 'Invalid request data', details: validationResult.error.errors }, + { status: 400 } + ) + } + + const { workspaceId } = validationResult.data + + // Verify workspace exists and user has access + const hasAccess = await db + .select({ workspaceId: workspaceMember.workspaceId }) + .from(workspaceMember) + .where(and(eq(workspaceMember.userId, userId), eq(workspaceMember.workspaceId, workspaceId))) + .limit(1) + + if (!hasAccess.length) { + logger.warn( + `[${requestId}] User ${userId} attempted to set workspace ${workspaceId} without access` + ) + return NextResponse.json({ error: 'Access denied to workspace' }, { status: 403 }) + } + + // Update or create user settings with workspace preference + await db + .insert(settings) + .values({ + id: userId, // Use user ID as settings ID + userId, + lastActiveWorkspaceId: workspaceId, + updatedAt: new Date(), + }) + .onConflictDoUpdate({ + target: [settings.userId], + set: { + lastActiveWorkspaceId: workspaceId, + updatedAt: new Date(), + }, + }) + + return NextResponse.json({ success: true }, { status: 200 }) + } catch (error) { + logger.error(`[${requestId}] Error updating workspace preference`, error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/workflows/[id]/route.ts b/apps/sim/app/api/workflows/[id]/route.ts index ac317fb6faf..34dc9aed639 100644 --- a/apps/sim/app/api/workflows/[id]/route.ts +++ b/apps/sim/app/api/workflows/[id]/route.ts @@ -1,31 +1,32 @@ import { and, eq } from 'drizzle-orm' -import { NextResponse } from 'next/server' +import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { createLogger } from '@/lib/logs/console-logger' import { db } from '@/db' import { workflow, workspaceMember } from '@/db/schema' -const logger = createLogger('WorkflowDetailAPI') +const logger = createLogger('WorkflowByIdAPI') -export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) { +/** + * GET /api/workflows/[id] + * Fetch a single workflow by ID + */ +export async function GET(request: NextRequest, { params }: { params: { id: string } }) { const requestId = crypto.randomUUID().slice(0, 8) const startTime = Date.now() + const workflowId = params.id try { // Get the session const session = await getSession() if (!session?.user?.id) { - logger.warn(`[${requestId}] Unauthorized workflow access attempt`) + logger.warn(`[${requestId}] Unauthorized access attempt for workflow ${workflowId}`) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } - const { id: workflowId } = await params + const userId = session.user.id - if (!workflowId) { - return NextResponse.json({ error: 'Workflow ID is required' }, { status: 400 }) - } - - // Fetch the workflow from database + // Fetch the workflow const workflowData = await db .select() .from(workflow) @@ -38,42 +39,43 @@ export async function GET(request: Request, { params }: { params: Promise<{ id: } // Check if user has access to this workflow - // User can access if they own it OR if it's in a workspace they're part of - const canAccess = workflowData.userId === session.user.id + let hasAccess = false + + // Case 1: User owns the workflow + if (workflowData.userId === userId) { + hasAccess = true + } - if (!canAccess && workflowData.workspaceId) { - // Check workspace membership + // Case 2: Workflow belongs to a workspace the user is a member of + if (!hasAccess && workflowData.workspaceId) { const membership = await db - .select() + .select({ role: workspaceMember.role }) .from(workspaceMember) .where( and( eq(workspaceMember.workspaceId, workflowData.workspaceId), - eq(workspaceMember.userId, session.user.id) + eq(workspaceMember.userId, userId) ) ) .then((rows) => rows[0]) if (membership) { - // User is a member of the workspace, allow access - const elapsed = Date.now() - startTime - logger.info(`[${requestId}] Workflow ${workflowId} fetched in ${elapsed}ms`) - return NextResponse.json({ data: workflowData }, { status: 200 }) + hasAccess = true } - } else if (canAccess) { - // User owns the workflow, allow access - const elapsed = Date.now() - startTime - logger.info(`[${requestId}] Workflow ${workflowId} fetched in ${elapsed}ms`) - return NextResponse.json({ data: workflowData }, { status: 200 }) } - logger.warn( - `[${requestId}] User ${session.user.id} attempted to access workflow ${workflowId} without permission` - ) - return NextResponse.json({ error: 'Access denied' }, { status: 403 }) + if (!hasAccess) { + logger.warn(`[${requestId}] User ${userId} denied access to workflow ${workflowId}`) + return NextResponse.json({ error: 'Access denied' }, { status: 403 }) + } + + const elapsed = Date.now() - startTime + logger.info(`[${requestId}] Successfully fetched workflow ${workflowId} in ${elapsed}ms`) + + return NextResponse.json({ data: workflowData }, { status: 200 }) } catch (error: any) { const elapsed = Date.now() - startTime - logger.error(`[${requestId}] Error fetching workflow after ${elapsed}ms:`, error) - return NextResponse.json({ error: 'Failed to fetch workflow' }, { status: 500 }) + logger.error(`[${requestId}] Error fetching workflow ${workflowId} after ${elapsed}ms`, error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } } diff --git a/apps/sim/app/api/workflows/sync/route.ts b/apps/sim/app/api/workflows/sync/route.ts index 7b72967ce6e..de6c8fad76d 100644 --- a/apps/sim/app/api/workflows/sync/route.ts +++ b/apps/sim/app/api/workflows/sync/route.ts @@ -377,9 +377,9 @@ export async function POST(req: NextRequest) { processedIds.add(id) const dbWorkflow = dbWorkflowMap.get(id) - // Handle legacy published workflows migration + // Handle legacy published workflows migration (only if state is provided) // If client workflow has isPublished but no marketplaceData, create marketplaceData with owner status - if (clientWorkflow.state.isPublished && !clientWorkflow.marketplaceData) { + if (clientWorkflow.state?.isPublished && !clientWorkflow.marketplaceData) { clientWorkflow.marketplaceData = { id: clientWorkflow.id, status: 'owner' } } @@ -387,7 +387,7 @@ export async function POST(req: NextRequest) { const effectiveWorkspaceId = clientWorkflow.workspaceId || workspaceId if (!dbWorkflow) { - // New workflow - create + // New workflow - create (state is required by schema) operations.push( db.insert(workflow).values({ id: clientWorkflow.id, @@ -417,61 +417,57 @@ export async function POST(req: NextRequest) { continue // Skip this workflow update and move to the next one } - // Existing workflow - update if needed - const needsUpdate = - JSON.stringify(dbWorkflow.state) !== JSON.stringify(clientWorkflow.state) || - dbWorkflow.name !== clientWorkflow.name || - dbWorkflow.description !== clientWorkflow.description || - dbWorkflow.color !== clientWorkflow.color || - dbWorkflow.workspaceId !== effectiveWorkspaceId || - dbWorkflow.folderId !== (clientWorkflow.folderId || null) || + // For existing workflows, determine what needs updating + let needsUpdate = false + const updateData: any = {} + + // Check metadata changes + if (dbWorkflow.name !== clientWorkflow.name) { + updateData.name = clientWorkflow.name + needsUpdate = true + } + if (dbWorkflow.description !== clientWorkflow.description) { + updateData.description = clientWorkflow.description + needsUpdate = true + } + if (dbWorkflow.color !== clientWorkflow.color) { + updateData.color = clientWorkflow.color + needsUpdate = true + } + if (dbWorkflow.workspaceId !== effectiveWorkspaceId) { + updateData.workspaceId = effectiveWorkspaceId + needsUpdate = true + } + if (dbWorkflow.folderId !== (clientWorkflow.folderId || null)) { + updateData.folderId = clientWorkflow.folderId || null + needsUpdate = true + } + if ( JSON.stringify(dbWorkflow.marketplaceData) !== - JSON.stringify(clientWorkflow.marketplaceData) + JSON.stringify(clientWorkflow.marketplaceData) + ) { + updateData.marketplaceData = clientWorkflow.marketplaceData || null + needsUpdate = true + } - if (needsUpdate) { - operations.push( - db - .update(workflow) - .set({ - name: clientWorkflow.name, - description: clientWorkflow.description, - color: clientWorkflow.color, - workspaceId: effectiveWorkspaceId, - folderId: clientWorkflow.folderId || null, - state: clientWorkflow.state, - marketplaceData: clientWorkflow.marketplaceData || null, - lastSynced: now, - updatedAt: now, - }) - .where(eq(workflow.id, id)) - ) + // Always update state since we only sync the active workflow with valid state + if (JSON.stringify(dbWorkflow.state) !== JSON.stringify(clientWorkflow.state)) { + updateData.state = clientWorkflow.state + needsUpdate = true } - } - } - // Handle deletions - workflows in DB but not in client - // Only delete workflows for the current workspace and only those the user can modify - for (const dbWorkflow of dbWorkflows) { - if ( - !processedIds.has(dbWorkflow.id) && - (!workspaceId || dbWorkflow.workspaceId === workspaceId) - ) { - // Check if the user has permission to delete this workflow - // Users can delete their own workflows, or any workflow if they're a workspace owner/admin - const canDelete = - dbWorkflow.userId === session.user.id || - (workspaceId && (userRole === 'owner' || userRole === 'admin' || userRole === 'member')) + if (needsUpdate) { + updateData.lastSynced = now + updateData.updatedAt = now - if (canDelete) { - operations.push(db.delete(workflow).where(eq(workflow.id, dbWorkflow.id))) - } else { - logger.warn( - `[${requestId}] User ${session.user.id} attempted to delete workflow ${dbWorkflow.id} without permission` - ) + operations.push(db.update(workflow).set(updateData).where(eq(workflow.id, id))) } } } + // NOTE: We don't delete workflows here since we're only syncing the active workflow + // Other workflows remain untouched in the database + // Execute all operations in parallel await Promise.all(operations) diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx index 8c06924f53e..a8361515d8a 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx @@ -10,7 +10,6 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog' -import { client } from '@/lib/auth-client' import { createLogger } from '@/lib/logs/console-logger' import { getProviderIdFromServiceId, @@ -19,7 +18,6 @@ import { type OAuthProvider, parseProvider, } from '@/lib/oauth' -import { saveToStorage } from '@/stores/workflows/persistence' const logger = createLogger('OAuthRequiredModal') @@ -157,57 +155,78 @@ export function OAuthRequiredModal({ (scope) => !scope.includes('userinfo.email') && !scope.includes('userinfo.profile') ) - const handleRedirectToSettings = () => { - try { - // Determine the appropriate serviceId and providerId - const providerId = getProviderIdFromServiceId(effectiveServiceId) + const handleConnectAccount = () => { + // Calculate the provider ID from service ID + const effectiveProviderId = getProviderIdFromServiceId(effectiveServiceId) + + // Store OAuth state in localStorage before redirect + const oauthState = { + providerId: effectiveProviderId, + serviceId: effectiveServiceId, + requiredScopes, + returnUrl: window.location.href, + context: 'oauth-required-modal', + timestamp: Date.now(), + } - // Store information about the required connection - saveToStorage('pending_service_id', effectiveServiceId) - saveToStorage('pending_oauth_scopes', requiredScopes) - saveToStorage('pending_oauth_return_url', window.location.href) - saveToStorage('pending_oauth_provider_id', providerId) - saveToStorage('from_oauth_modal', true) + // Use localStorage for OAuth state management + try { + localStorage.setItem('pending_oauth_state', JSON.stringify(oauthState)) - // Close the modal - onClose() + // Navigate to OAuth URL + const authUrl = `/api/auth/oauth?provider=${effectiveProviderId}&service=${effectiveServiceId}&scopes=${encodeURIComponent( + requiredScopes.join(',') + )}&return_url=${encodeURIComponent(window.location.href)}` - // Open the settings modal with the credentials tab - const event = new CustomEvent('open-settings', { - detail: { tab: 'credentials' }, - }) - window.dispatchEvent(event) + window.location.href = authUrl } catch (error) { - logger.error('Error redirecting to settings:', { error }) + logger.error('Failed to store OAuth state:', error) + // Fallback: redirect without state + const authUrl = `/api/auth/oauth?provider=${effectiveProviderId}&service=${effectiveServiceId}&scopes=${encodeURIComponent( + requiredScopes.join(',') + )}&return_url=${encodeURIComponent(window.location.href)}` + + window.location.href = authUrl } + + onClose() } - const handleConnectDirectly = async () => { - try { - // Determine the appropriate serviceId and providerId - const providerId = getProviderIdFromServiceId(effectiveServiceId) + const handleConnectExisting = () => { + // Calculate the provider ID from service ID + const effectiveProviderId = getProviderIdFromServiceId(effectiveServiceId) - // Store information about the required connection - saveToStorage('pending_service_id', effectiveServiceId) - saveToStorage('pending_oauth_scopes', requiredScopes) - saveToStorage('pending_oauth_return_url', window.location.href) - saveToStorage('pending_oauth_provider_id', providerId) + // Store OAuth state in localStorage before redirect + const oauthState = { + providerId: effectiveProviderId, + serviceId: effectiveServiceId, + requiredScopes, + returnUrl: window.location.href, + context: 'oauth-required-modal-existing', + timestamp: Date.now(), + } - // Close the modal - onClose() + // Use localStorage for OAuth state management + try { + localStorage.setItem('pending_oauth_state', JSON.stringify(oauthState)) - logger.info('Linking OAuth2:', { - providerId, - requiredScopes, - }) + // Navigate to OAuth URL + const authUrl = `/api/auth/oauth?provider=${effectiveProviderId}&service=${effectiveServiceId}&scopes=${encodeURIComponent( + requiredScopes.join(',') + )}&return_url=${encodeURIComponent(window.location.href)}` - await client.oauth2.link({ - providerId, - callbackURL: window.location.href, - }) + window.location.href = authUrl } catch (error) { - logger.error('Error initiating OAuth flow:', { error }) + logger.error('Failed to store OAuth state:', error) + // Fallback: redirect without state + const authUrl = `/api/auth/oauth?provider=${effectiveProviderId}&service=${effectiveServiceId}&scopes=${encodeURIComponent( + requiredScopes.join(',') + )}&return_url=${encodeURIComponent(window.location.href)}` + + window.location.href = authUrl } + + onClose() } return ( @@ -255,13 +274,13 @@ export function OAuthRequiredModal({ - - + + {/* History Button */} + + + {/* Notifications Button */} + + + {/* Duplicate Button */} + + + {/* Auto Layout Button */} + + + {/* Debug Mode Button */} + + + {/* Deploy Button */} + + + {/* Run Button with Dropdown */} +
+ {/* Main Run Button */} + + + {/* Dropdown Trigger */} + +
+ + + ) +} + +const SkeletonPanelComponent = () => { + return ( +
+ {/* Panel skeleton */} +
+ {/* Tab headers skeleton */} +
+ {Array.from({ length: 3 }).map((_, i) => ( + + ))} +
+ + {/* Content skeleton */} +
+ {Array.from({ length: 6 }).map((_, i) => ( + + ))} +
+
+
+ ) +} + +const SkeletonNodes = () => { + return [ + // Starter node skeleton + { + id: 'skeleton-starter', + type: 'workflowBlock', + position: { x: 100, y: 100 }, + data: { + type: 'skeleton', + config: { name: '', description: '', bgColor: '#9CA3AF' }, + name: '', + isActive: false, + isPending: false, + isSkeleton: true, + }, + dragHandle: '.workflow-drag-handle', + }, + // Additional skeleton nodes + { + id: 'skeleton-node-1', + type: 'workflowBlock', + position: { x: 500, y: 100 }, + data: { + type: 'skeleton', + config: { name: '', description: '', bgColor: '#9CA3AF' }, + name: '', + isActive: false, + isPending: false, + isSkeleton: true, + }, + dragHandle: '.workflow-drag-handle', + }, + { + id: 'skeleton-node-2', + type: 'workflowBlock', + position: { x: 300, y: 300 }, + data: { + type: 'skeleton', + config: { name: '', description: '', bgColor: '#9CA3AF' }, + name: '', + isActive: false, + isPending: false, + isSkeleton: true, + }, + dragHandle: '.workflow-drag-handle', + }, + ] +} + +interface SkeletonLoadingProps { + showSkeleton: boolean + isSidebarCollapsed: boolean + children: React.ReactNode +} + +export function SkeletonLoading({ + showSkeleton, + isSidebarCollapsed, + children, +}: SkeletonLoadingProps) { + const { mode, isExpanded } = useSidebarStore() + + return ( +
+
+ {/* Skeleton Control Bar */} +
+ +
+ + {/* Real Control Bar */} +
+ {children} +
+
+ + {/* Real content will be rendered by children - sidebar will show its own loading state */} +
+ ) +} + +export function SkeletonPanelWrapper({ showSkeleton }: { showSkeleton: boolean }) { + return ( +
+ +
+ ) +} + +export { SkeletonNodes, SkeletonPanelComponent } diff --git a/apps/sim/app/w/[id]/workflow.tsx b/apps/sim/app/w/[id]/workflow.tsx index fb1781d113c..3aa89114b09 100644 --- a/apps/sim/app/w/[id]/workflow.tsx +++ b/apps/sim/app/w/[id]/workflow.tsx @@ -12,7 +12,6 @@ import ReactFlow, { } from 'reactflow' import 'reactflow/dist/style.css' -import { LoadingAgent } from '@/components/ui/loading-agent' import { createLogger } from '@/lib/logs/console-logger' import { LoopNodeComponent } from '@/app/w/[id]/components/loop-node/loop-node' import { NotificationList } from '@/app/w/[id]/components/notifications/notifications' @@ -30,6 +29,7 @@ import { useWorkflowStore } from '@/stores/workflows/workflow/store' import { ControlBar } from './components/control-bar/control-bar' import { ErrorBoundary } from './components/error/index' import { Panel } from './components/panel/panel' +import { SkeletonLoading } from './components/skeleton-loading/skeleton-loading' import { Toolbar } from './components/toolbar/toolbar' import { WorkflowBlock } from './components/workflow-block/workflow-block' import { WorkflowEdge } from './components/workflow-edge/workflow-edge' @@ -56,7 +56,7 @@ const edgeTypes: EdgeTypes = { workflowEdge: WorkflowEdge } function WorkflowContent() { // State - const [isInitialized, setIsInitialized] = useState(false) + const [isWorkflowReady, setIsWorkflowReady] = useState(false) const { mode, isExpanded } = useSidebarStore() // In hover mode, act as if sidebar is always collapsed for layout purposes const isSidebarCollapsed = @@ -78,6 +78,7 @@ function WorkflowContent() { // Store access const { workflows, + activeWorkflowId, setActiveWorkflow, createWorkflow, isLoading: workflowsLoading, @@ -252,13 +253,13 @@ function WorkflowContent() { } }, [debouncedAutoLayout]) - // Initialize workflow + // Initialize workflow system useEffect(() => { if (typeof window !== 'undefined') { const initSync = async () => { // Initialize sync system if not already initialized await initializeSyncManagers() - setIsInitialized(true) + // Note: setIsWorkflowReady is handled in the workflow data tracking effect below } // Initialize sync system @@ -727,10 +728,38 @@ function WorkflowContent() { [project, isPointInLoopNodeWrapper, getNodes] ) - // Init workflow + // Track when workflow is fully ready for rendering useEffect(() => { - if (!isInitialized) return + const currentId = params.id as string + + // Reset workflow ready state when workflow changes + if (activeWorkflowId !== currentId) { + setIsWorkflowReady(false) + return + } + + // Check if we have the necessary data to render the workflow + const hasActiveWorkflow = activeWorkflowId === currentId + const hasWorkflowInRegistry = Boolean(workflows[currentId]) + const isNotLoading = !workflowsLoading + + // Workflow is ready when: + // 1. We have an active workflow that matches the URL + // 2. The workflow exists in the registry + // 3. Workflows are not currently loading + if (hasActiveWorkflow && hasWorkflowInRegistry && isNotLoading) { + // Add a small delay to ensure blocks state has settled + const timeoutId = setTimeout(() => { + setIsWorkflowReady(true) + }, 100) + + return () => clearTimeout(timeoutId) + } + setIsWorkflowReady(false) + }, [activeWorkflowId, params.id, workflows, workflowsLoading]) + // Init workflow + useEffect(() => { const validateAndNavigate = async () => { const workflowIds = Object.keys(workflows) const currentId = params.id as string @@ -789,7 +818,6 @@ function WorkflowContent() { setActiveWorkflow, createWorkflow, router, - isInitialized, markAllAsRead, resetVariablesLoaded, ]) @@ -1339,10 +1367,27 @@ function WorkflowContent() { } }, [setSubBlockValue]) - if (!isInitialized) { + // Show skeleton UI while loading, then smoothly transition to real content + const showSkeletonUI = !isWorkflowReady + + if (showSkeletonUI) { return ( -
- +
+ + + + +
+
+ + +
+
+ +
+
) } @@ -1383,7 +1428,6 @@ function WorkflowContent() { }} connectionLineType={ConnectionLineType.SmoothStep} onNodeClick={(e, node) => { - // Allow selecting nodes, but stop propagation to prevent triggering other events e.stopPropagation() }} onPaneClick={onPaneClick} From 65747c596dab0b39fcc8d5f57033593a1ccafd24 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 17 Jun 2025 12:39:55 -0700 Subject: [PATCH 26/27] remove unused utils --- apps/sim/lib/urls/utils.ts | 61 -------------------------------------- 1 file changed, 61 deletions(-) diff --git a/apps/sim/lib/urls/utils.ts b/apps/sim/lib/urls/utils.ts index 587fd6cecbd..c38c32a139a 100644 --- a/apps/sim/lib/urls/utils.ts +++ b/apps/sim/lib/urls/utils.ts @@ -53,64 +53,3 @@ export function getEmailDomain(): string { return isProd ? 'simstudio.ai' : 'localhost:3000' } } - -/** - * OAuth URL construction utilities - */ - -export interface OAuthUrlParams { - provider: string - service: string - scopes: string[] - returnUrl?: string -} - -/** - * Constructs a standardized OAuth URL for authentication - * @param params OAuth URL parameters - * @returns The complete OAuth URL - */ -export function buildOAuthUrl({ provider, service, scopes, returnUrl }: OAuthUrlParams): string { - const currentReturnUrl = returnUrl || (typeof window !== 'undefined' ? window.location.href : '') - - return `/api/auth/oauth?provider=${provider}&service=${service}&scopes=${encodeURIComponent( - scopes.join(',') - )}&return_url=${encodeURIComponent(currentReturnUrl)}` -} - -/** - * Microsoft Teams URL construction utilities - */ - -/** - * Constructs a Microsoft Teams team URL - * @param teamId The team ID - * @returns The complete Teams team URL - */ -export function buildTeamsTeamUrl(teamId: string): string { - return `https://teams.microsoft.com/l/team/${teamId}` -} - -/** - * Constructs a Microsoft Teams channel URL - * @param teamId The team ID - * @param channelDisplayName The channel display name - * @param channelId The channel ID - * @returns The complete Teams channel URL - */ -export function buildTeamsChannelUrl( - teamId: string, - channelDisplayName: string, - channelId: string -): string { - return `https://teams.microsoft.com/l/channel/${teamId}/${encodeURIComponent(channelDisplayName)}/${channelId}` -} - -/** - * Constructs a Microsoft Teams chat URL - * @param chatId The chat ID - * @returns The complete Teams chat URL - */ -export function buildTeamsChatUrl(chatId: string): string { - return `https://teams.microsoft.com/l/chat/${chatId}` -} From 597cc07e63e10e52745513de8325f0af674395e6 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 17 Jun 2025 13:03:21 -0700 Subject: [PATCH 27/27] remove console logs --- apps/sim/app/w/hooks/use-registry-loading.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/sim/app/w/hooks/use-registry-loading.ts b/apps/sim/app/w/hooks/use-registry-loading.ts index 5f4870eb7b7..207514e18fd 100644 --- a/apps/sim/app/w/hooks/use-registry-loading.ts +++ b/apps/sim/app/w/hooks/use-registry-loading.ts @@ -2,8 +2,11 @@ import { useEffect } from 'react' import { usePathname, useRouter } from 'next/navigation' +import { createLogger } from '@/lib/logs/console-logger' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' +const logger = createLogger('UseRegistryLoading') + /** * Extract workflow ID from pathname * @param pathname - Current pathname @@ -22,7 +25,7 @@ function extractWorkflowIdFromPathname(pathname: string): string | null { } return null } catch (error) { - console.warn('Failed to extract workflow ID from pathname:', error) + logger.warn('Failed to extract workflow ID from pathname:', error) return null } } @@ -46,7 +49,7 @@ export function useRegistryLoading() { const workflowIdFromUrl = extractWorkflowIdFromPathname(pathname) if (workflowIdFromUrl) { loadWorkspaceFromWorkflowId(workflowIdFromUrl).catch((error) => { - console.warn('Failed to load workspace from workflow ID:', error) + logger.warn('Failed to load workspace from workflow ID:', error) }) } } @@ -64,7 +67,7 @@ export function useRegistryLoading() { (pathname === '/w' || pathname === '/w/' || pathname === `/w/${activeWorkspaceId}`) ) { const firstWorkflowId = Object.keys(workflows)[0] - console.log('First-time navigation: redirecting to first workflow:', firstWorkflowId) + logger.info('First-time navigation: redirecting to first workflow:', firstWorkflowId) router.replace(`/w/${firstWorkflowId}`) } }