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
19 changes: 19 additions & 0 deletions apps/sim/app/api/workflows/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ vi.mock('@sim/db', () => ({
db: {
select: (...args: unknown[]) => mockDbSelect(...args),
insert: (...args: unknown[]) => mockDbInsert(...args),
transaction: vi.fn(async (fn: (tx: Record<string, unknown>) => Promise<void>) => {
const tx = {
select: (...args: unknown[]) => mockDbSelect(...args),
insert: (...args: unknown[]) => mockDbInsert(...args),
}
await fn(tx)
}),
},
}))

Expand Down Expand Up @@ -87,6 +94,18 @@ vi.mock('@/lib/core/telemetry', () => ({
},
}))

vi.mock('@/lib/workflows/defaults', () => ({
buildDefaultWorkflowArtifacts: vi.fn().mockReturnValue({
workflowState: { blocks: {}, edges: [], loops: {}, parallels: {} },
subBlockValues: {},
startBlockId: 'start-block-id',
}),
}))

vi.mock('@/lib/workflows/persistence/utils', () => ({
saveWorkflowToNormalizedTables: vi.fn().mockResolvedValue({ success: true }),
}))

import { POST } from '@/app/api/workflows/route'

describe('Workflows API Route - POST ordering', () => {
Expand Down
42 changes: 26 additions & 16 deletions apps/sim/app/api/workflows/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { getNextWorkflowColor } from '@/lib/workflows/colors'
import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults'
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils'
import { deduplicateWorkflowName, listWorkflows, type WorkflowScope } from '@/lib/workflows/utils'
import { getUserEntityPermissions, workspaceExists } from '@/lib/workspaces/permissions/utils'
import { verifyWorkspaceMembership } from '@/app/api/workflows/utils'
Expand Down Expand Up @@ -247,24 +249,30 @@ export async function POST(req: NextRequest) {
// Silently fail
})

await db.insert(workflow).values({
id: workflowId,
userId,
workspaceId,
folderId: folderId || null,
sortOrder,
name,
description,
color,
lastSynced: now,
createdAt: now,
updatedAt: now,
isDeployed: false,
runCount: 0,
variables: {},
const { workflowState, subBlockValues, startBlockId } = buildDefaultWorkflowArtifacts()

await db.transaction(async (tx) => {
await tx.insert(workflow).values({
id: workflowId,
userId,
workspaceId,
folderId: folderId || null,
sortOrder,
name,
description,
color,
lastSynced: now,
createdAt: now,
updatedAt: now,
isDeployed: false,
runCount: 0,
variables: {},
})

await saveWorkflowToNormalizedTables(workflowId, workflowState, tx)
})

logger.info(`[${requestId}] Successfully created empty workflow ${workflowId}`)
logger.info(`[${requestId}] Successfully created workflow ${workflowId} with default blocks`)

recordAudit({
workspaceId,
Expand All @@ -290,6 +298,8 @@ export async function POST(req: NextRequest) {
sortOrder,
createdAt: now,
updatedAt: now,
startBlockId,
subBlockValues,
})
} catch (error) {
if (error instanceof z.ZodError) {
Expand Down
12 changes: 3 additions & 9 deletions apps/sim/app/api/workspaces/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,9 @@ async function createWorkspace(
runCount: 0,
variables: {},
})

const { workflowState } = buildDefaultWorkflowArtifacts()
await saveWorkflowToNormalizedTables(workflowId, workflowState, tx)
}

logger.info(
Expand All @@ -181,15 +184,6 @@ async function createWorkspace(
: `Created workspace ${workspaceId} with initial workflow ${workflowId} for user ${userId}`
)
})

if (!skipDefaultWorkflow) {
const { workflowState } = buildDefaultWorkflowArtifacts()
const seedResult = await saveWorkflowToNormalizedTables(workflowId, workflowState)

if (!seedResult.success) {
throw new Error(seedResult.error || 'Failed to seed default workflow state')
}
}
} catch (error) {
logger.error(`Failed to create workspace ${workspaceId}:`, error)
throw error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2232,6 +2232,10 @@ const WorkflowContent = React.memo(
return
}

if (hydration.phase === 'creating') {
return
}

// If already loading (state-loading phase), skip
if (hydration.phase === 'state-loading' && hydration.workflowId === currentId) {
return
Expand Down Expand Up @@ -2299,6 +2303,10 @@ const WorkflowContent = React.memo(
return
}

if (hydration.phase === 'creating') {
return
}

// If no workflows exist after loading, redirect to workspace root
if (workflowCount === 0) {
logger.info('No workflows found, redirecting to workspace root')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { getWorkflows } from '@/hooks/queries/utils/workflow-cache'
import { useCreateWorkflow } from '@/hooks/queries/workflows'
import { useFolderStore } from '@/stores/folders/store'
import type { FolderTreeNode } from '@/stores/folders/types'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { generateCreativeWorkflowName } from '@/stores/workflows/registry/utils'

const logger = createLogger('FolderItem')
Expand Down Expand Up @@ -135,29 +136,23 @@ export function FolderItem({

const isEditingRef = useRef(false)

const handleCreateWorkflowInFolder = useCallback(async () => {
try {
const name = generateCreativeWorkflowName()
const color = getNextWorkflowColor()
const handleCreateWorkflowInFolder = useCallback(() => {
const name = generateCreativeWorkflowName()
const color = getNextWorkflowColor()
const id = crypto.randomUUID()

const result = await createWorkflowMutation.mutateAsync({
workspaceId,
folderId: folder.id,
name,
color,
id: crypto.randomUUID(),
})
createWorkflowMutation.mutate({
workspaceId,
folderId: folder.id,
name,
color,
id,
})

if (result.id) {
router.push(`/workspace/${workspaceId}/w/${result.id}`)
expandFolder()
window.dispatchEvent(
new CustomEvent(SIDEBAR_SCROLL_EVENT, { detail: { itemId: result.id } })
)
}
} catch (error) {
logger.error('Failed to create workflow in folder:', error)
}
useWorkflowRegistry.getState().markWorkflowCreating(id)
expandFolder()
router.push(`/workspace/${workspaceId}/w/${id}`)
window.dispatchEvent(new CustomEvent(SIDEBAR_SCROLL_EVENT, { detail: { itemId: id } }))
}, [createWorkflowMutation, workspaceId, folder.id, router, expandFolder])

const handleCreateFolderInFolder = useCallback(async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { useCallback, useMemo } from 'react'
import { createLogger } from '@sim/logger'
import { useRouter } from 'next/navigation'
import { getNextWorkflowColor } from '@/lib/workflows/colors'
import { useCreateWorkflow, useWorkflowMap } from '@/hooks/queries/workflows'
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { generateCreativeWorkflowName } from '@/stores/workflows/registry/utils'

const logger = createLogger('useWorkflowOperations')

interface UseWorkflowOperationsProps {
workspaceId: string
}
Expand All @@ -25,30 +23,24 @@ export function useWorkflowOperations({ workspaceId }: UseWorkflowOperationsProp
[workflows, workspaceId]
)

const handleCreateWorkflow = useCallback(async (): Promise<string | null> => {
try {
const { clearDiff } = useWorkflowDiffStore.getState()
clearDiff()

const name = generateCreativeWorkflowName()
const color = getNextWorkflowColor()

const result = await createWorkflowMutation.mutateAsync({
workspaceId,
name,
color,
id: crypto.randomUUID(),
})

if (result.id) {
router.push(`/workspace/${workspaceId}/w/${result.id}`)
return result.id
}
return null
} catch (error) {
logger.error('Error creating workflow:', error)
return null
}
const handleCreateWorkflow = useCallback((): Promise<string | null> => {
const { clearDiff } = useWorkflowDiffStore.getState()
clearDiff()

const name = generateCreativeWorkflowName()
const color = getNextWorkflowColor()
const id = crypto.randomUUID()

createWorkflowMutation.mutate({
workspaceId,
name,
color,
id,
})

useWorkflowRegistry.getState().markWorkflowCreating(id)
router.push(`/workspace/${workspaceId}/w/${id}`)
return Promise.resolve(id)
}, [createWorkflowMutation, workspaceId, router])

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export function useWorkspaceManagement({
const {
data: workspaces = [],
isLoading: isWorkspacesLoading,
isFetching: isWorkspacesFetching,
refetch: refetchWorkspaces,
} = useWorkspacesQuery(Boolean(sessionUserId))

Expand Down Expand Up @@ -71,14 +72,17 @@ export function useWorkspaceManagement({
const matchingWorkspace = workspaces.find((w) => w.id === currentWorkspaceId)

if (!matchingWorkspace) {
if (isWorkspacesFetching) {
return
}
logger.warn(`Workspace ${currentWorkspaceId} not found in user's workspaces`)
const fallbackWorkspace = workspaces[0]
logger.info(`Redirecting to fallback workspace: ${fallbackWorkspace.id}`)
routerRef.current?.push(`/workspace/${fallbackWorkspace.id}/home`)
}

hasValidatedRef.current = true
}, [workspaces, isWorkspacesLoading])
}, [workspaces, isWorkspacesLoading, isWorkspacesFetching])

const refreshWorkspaceList = useCallback(async () => {
await queryClient.invalidateQueries({ queryKey: workspaceKeys.lists() })
Expand Down
9 changes: 6 additions & 3 deletions apps/sim/app/workspace/providers/socket-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { useParams } from 'next/navigation'
import type { Socket } from 'socket.io-client'
import { getEnv } from '@/lib/core/config/env'
import { useOperationQueueStore } from '@/stores/operation-queue/store'
import { useWorkflowRegistry as useWorkflowRegistryStore } from '@/stores/workflows/registry/store'

const logger = createLogger('SocketContext')

Expand Down Expand Up @@ -387,13 +388,11 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
{ useWorkflowRegistry },
{ useWorkflowStore },
{ useSubBlockStore },
{ useWorkflowDiffStore },
] = await Promise.all([
import('@/stores/operation-queue/store'),
import('@/stores/workflows/registry/store'),
import('@/stores/workflows/workflow/store'),
import('@/stores/workflows/subblock/store'),
import('@/stores/workflow-diff/store'),
])

const { activeWorkflowId } = useWorkflowRegistry.getState()
Expand Down Expand Up @@ -542,9 +541,13 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
}
}, [user?.id, authFailed])

const hydrationPhase = useWorkflowRegistryStore((s) => s.hydration.phase)

useEffect(() => {
if (!socket || !isConnected || !urlWorkflowId) return

if (hydrationPhase === 'creating') return

// Skip if already in the correct room
if (currentWorkflowId === urlWorkflowId) return

Expand All @@ -562,7 +565,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
workflowId: urlWorkflowId,
tabSessionId: getTabSessionId(),
})
}, [socket, isConnected, urlWorkflowId, currentWorkflowId])
}, [socket, isConnected, urlWorkflowId, currentWorkflowId, hydrationPhase])

const joinWorkflow = useCallback(
(workflowId: string) => {
Expand Down
Loading
Loading