Skip to content

Commit a30f2d6

Browse files
icecrasher321waleedlatif1
authored andcommitted
improvement(workflow): seed start block on server side (#3890)
* improvement(workflow): seed start block on server side * add creating state machine for optimistic switch * fix worksapce switch * address comments * address error handling at correct level
1 parent a3d1fd5 commit a30f2d6

File tree

13 files changed

+260
-173
lines changed

13 files changed

+260
-173
lines changed

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ vi.mock('@sim/db', () => ({
2828
db: {
2929
select: (...args: unknown[]) => mockDbSelect(...args),
3030
insert: (...args: unknown[]) => mockDbInsert(...args),
31+
transaction: vi.fn(async (fn: (tx: Record<string, unknown>) => Promise<void>) => {
32+
const tx = {
33+
select: (...args: unknown[]) => mockDbSelect(...args),
34+
insert: (...args: unknown[]) => mockDbInsert(...args),
35+
}
36+
await fn(tx)
37+
}),
3138
},
3239
}))
3340

@@ -87,6 +94,18 @@ vi.mock('@/lib/core/telemetry', () => ({
8794
},
8895
}))
8996

97+
vi.mock('@/lib/workflows/defaults', () => ({
98+
buildDefaultWorkflowArtifacts: vi.fn().mockReturnValue({
99+
workflowState: { blocks: {}, edges: [], loops: {}, parallels: {} },
100+
subBlockValues: {},
101+
startBlockId: 'start-block-id',
102+
}),
103+
}))
104+
105+
vi.mock('@/lib/workflows/persistence/utils', () => ({
106+
saveWorkflowToNormalizedTables: vi.fn().mockResolvedValue({ success: true }),
107+
}))
108+
90109
import { POST } from '@/app/api/workflows/route'
91110

92111
describe('Workflows API Route - POST ordering', () => {

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

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
88
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
99
import { generateRequestId } from '@/lib/core/utils/request'
1010
import { getNextWorkflowColor } from '@/lib/workflows/colors'
11+
import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults'
12+
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils'
1113
import { deduplicateWorkflowName, listWorkflows, type WorkflowScope } from '@/lib/workflows/utils'
1214
import { getUserEntityPermissions, workspaceExists } from '@/lib/workspaces/permissions/utils'
1315
import { verifyWorkspaceMembership } from '@/app/api/workflows/utils'
@@ -247,24 +249,30 @@ export async function POST(req: NextRequest) {
247249
// Silently fail
248250
})
249251

250-
await db.insert(workflow).values({
251-
id: workflowId,
252-
userId,
253-
workspaceId,
254-
folderId: folderId || null,
255-
sortOrder,
256-
name,
257-
description,
258-
color,
259-
lastSynced: now,
260-
createdAt: now,
261-
updatedAt: now,
262-
isDeployed: false,
263-
runCount: 0,
264-
variables: {},
252+
const { workflowState, subBlockValues, startBlockId } = buildDefaultWorkflowArtifacts()
253+
254+
await db.transaction(async (tx) => {
255+
await tx.insert(workflow).values({
256+
id: workflowId,
257+
userId,
258+
workspaceId,
259+
folderId: folderId || null,
260+
sortOrder,
261+
name,
262+
description,
263+
color,
264+
lastSynced: now,
265+
createdAt: now,
266+
updatedAt: now,
267+
isDeployed: false,
268+
runCount: 0,
269+
variables: {},
270+
})
271+
272+
await saveWorkflowToNormalizedTables(workflowId, workflowState, tx)
265273
})
266274

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

269277
recordAudit({
270278
workspaceId,
@@ -290,6 +298,8 @@ export async function POST(req: NextRequest) {
290298
sortOrder,
291299
createdAt: now,
292300
updatedAt: now,
301+
startBlockId,
302+
subBlockValues,
293303
})
294304
} catch (error) {
295305
if (error instanceof z.ZodError) {

apps/sim/app/api/workspaces/route.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,9 @@ async function createWorkspace(
173173
runCount: 0,
174174
variables: {},
175175
})
176+
177+
const { workflowState } = buildDefaultWorkflowArtifacts()
178+
await saveWorkflowToNormalizedTables(workflowId, workflowState, tx)
176179
}
177180

178181
logger.info(
@@ -181,15 +184,6 @@ async function createWorkspace(
181184
: `Created workspace ${workspaceId} with initial workflow ${workflowId} for user ${userId}`
182185
)
183186
})
184-
185-
if (!skipDefaultWorkflow) {
186-
const { workflowState } = buildDefaultWorkflowArtifacts()
187-
const seedResult = await saveWorkflowToNormalizedTables(workflowId, workflowState)
188-
189-
if (!seedResult.success) {
190-
throw new Error(seedResult.error || 'Failed to seed default workflow state')
191-
}
192-
}
193187
} catch (error) {
194188
logger.error(`Failed to create workspace ${workspaceId}:`, error)
195189
throw error

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2232,6 +2232,10 @@ const WorkflowContent = React.memo(
22322232
return
22332233
}
22342234

2235+
if (hydration.phase === 'creating') {
2236+
return
2237+
}
2238+
22352239
// If already loading (state-loading phase), skip
22362240
if (hydration.phase === 'state-loading' && hydration.workflowId === currentId) {
22372241
return
@@ -2299,6 +2303,10 @@ const WorkflowContent = React.memo(
22992303
return
23002304
}
23012305

2306+
if (hydration.phase === 'creating') {
2307+
return
2308+
}
2309+
23022310
// If no workflows exist after loading, redirect to workspace root
23032311
if (workflowCount === 0) {
23042312
logger.info('No workflows found, redirecting to workspace root')

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { getWorkflows } from '@/hooks/queries/utils/workflow-cache'
3232
import { useCreateWorkflow } from '@/hooks/queries/workflows'
3333
import { useFolderStore } from '@/stores/folders/store'
3434
import type { FolderTreeNode } from '@/stores/folders/types'
35+
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
3536
import { generateCreativeWorkflowName } from '@/stores/workflows/registry/utils'
3637

3738
const logger = createLogger('FolderItem')
@@ -135,29 +136,23 @@ export function FolderItem({
135136

136137
const isEditingRef = useRef(false)
137138

138-
const handleCreateWorkflowInFolder = useCallback(async () => {
139-
try {
140-
const name = generateCreativeWorkflowName()
141-
const color = getNextWorkflowColor()
139+
const handleCreateWorkflowInFolder = useCallback(() => {
140+
const name = generateCreativeWorkflowName()
141+
const color = getNextWorkflowColor()
142+
const id = crypto.randomUUID()
142143

143-
const result = await createWorkflowMutation.mutateAsync({
144-
workspaceId,
145-
folderId: folder.id,
146-
name,
147-
color,
148-
id: crypto.randomUUID(),
149-
})
144+
createWorkflowMutation.mutate({
145+
workspaceId,
146+
folderId: folder.id,
147+
name,
148+
color,
149+
id,
150+
})
150151

151-
if (result.id) {
152-
router.push(`/workspace/${workspaceId}/w/${result.id}`)
153-
expandFolder()
154-
window.dispatchEvent(
155-
new CustomEvent(SIDEBAR_SCROLL_EVENT, { detail: { itemId: result.id } })
156-
)
157-
}
158-
} catch (error) {
159-
logger.error('Failed to create workflow in folder:', error)
160-
}
152+
useWorkflowRegistry.getState().markWorkflowCreating(id)
153+
expandFolder()
154+
router.push(`/workspace/${workspaceId}/w/${id}`)
155+
window.dispatchEvent(new CustomEvent(SIDEBAR_SCROLL_EVENT, { detail: { itemId: id } }))
161156
}, [createWorkflowMutation, workspaceId, folder.id, router, expandFolder])
162157

163158
const handleCreateFolderInFolder = useCallback(async () => {

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workflow-operations.ts

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import { useCallback, useMemo } from 'react'
2-
import { createLogger } from '@sim/logger'
32
import { useRouter } from 'next/navigation'
43
import { getNextWorkflowColor } from '@/lib/workflows/colors'
54
import { useCreateWorkflow, useWorkflowMap } from '@/hooks/queries/workflows'
65
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
6+
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
77
import { generateCreativeWorkflowName } from '@/stores/workflows/registry/utils'
88

9-
const logger = createLogger('useWorkflowOperations')
10-
119
interface UseWorkflowOperationsProps {
1210
workspaceId: string
1311
}
@@ -25,30 +23,24 @@ export function useWorkflowOperations({ workspaceId }: UseWorkflowOperationsProp
2523
[workflows, workspaceId]
2624
)
2725

28-
const handleCreateWorkflow = useCallback(async (): Promise<string | null> => {
29-
try {
30-
const { clearDiff } = useWorkflowDiffStore.getState()
31-
clearDiff()
32-
33-
const name = generateCreativeWorkflowName()
34-
const color = getNextWorkflowColor()
35-
36-
const result = await createWorkflowMutation.mutateAsync({
37-
workspaceId,
38-
name,
39-
color,
40-
id: crypto.randomUUID(),
41-
})
42-
43-
if (result.id) {
44-
router.push(`/workspace/${workspaceId}/w/${result.id}`)
45-
return result.id
46-
}
47-
return null
48-
} catch (error) {
49-
logger.error('Error creating workflow:', error)
50-
return null
51-
}
26+
const handleCreateWorkflow = useCallback((): Promise<string | null> => {
27+
const { clearDiff } = useWorkflowDiffStore.getState()
28+
clearDiff()
29+
30+
const name = generateCreativeWorkflowName()
31+
const color = getNextWorkflowColor()
32+
const id = crypto.randomUUID()
33+
34+
createWorkflowMutation.mutate({
35+
workspaceId,
36+
name,
37+
color,
38+
id,
39+
})
40+
41+
useWorkflowRegistry.getState().markWorkflowCreating(id)
42+
router.push(`/workspace/${workspaceId}/w/${id}`)
43+
return Promise.resolve(id)
5244
}, [createWorkflowMutation, workspaceId, router])
5345

5446
return {

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-management.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export function useWorkspaceManagement({
3939
const {
4040
data: workspaces = [],
4141
isLoading: isWorkspacesLoading,
42+
isFetching: isWorkspacesFetching,
4243
refetch: refetchWorkspaces,
4344
} = useWorkspacesQuery(Boolean(sessionUserId))
4445

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

7374
if (!matchingWorkspace) {
75+
if (isWorkspacesFetching) {
76+
return
77+
}
7478
logger.warn(`Workspace ${currentWorkspaceId} not found in user's workspaces`)
7579
const fallbackWorkspace = workspaces[0]
7680
logger.info(`Redirecting to fallback workspace: ${fallbackWorkspace.id}`)
7781
routerRef.current?.push(`/workspace/${fallbackWorkspace.id}/home`)
7882
}
7983

8084
hasValidatedRef.current = true
81-
}, [workspaces, isWorkspacesLoading])
85+
}, [workspaces, isWorkspacesLoading, isWorkspacesFetching])
8286

8387
const refreshWorkspaceList = useCallback(async () => {
8488
await queryClient.invalidateQueries({ queryKey: workspaceKeys.lists() })

apps/sim/app/workspace/providers/socket-provider.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { useParams } from 'next/navigation'
1515
import type { Socket } from 'socket.io-client'
1616
import { getEnv } from '@/lib/core/config/env'
1717
import { useOperationQueueStore } from '@/stores/operation-queue/store'
18+
import { useWorkflowRegistry as useWorkflowRegistryStore } from '@/stores/workflows/registry/store'
1819

1920
const logger = createLogger('SocketContext')
2021

@@ -387,13 +388,11 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
387388
{ useWorkflowRegistry },
388389
{ useWorkflowStore },
389390
{ useSubBlockStore },
390-
{ useWorkflowDiffStore },
391391
] = await Promise.all([
392392
import('@/stores/operation-queue/store'),
393393
import('@/stores/workflows/registry/store'),
394394
import('@/stores/workflows/workflow/store'),
395395
import('@/stores/workflows/subblock/store'),
396-
import('@/stores/workflow-diff/store'),
397396
])
398397

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

544+
const hydrationPhase = useWorkflowRegistryStore((s) => s.hydration.phase)
545+
545546
useEffect(() => {
546547
if (!socket || !isConnected || !urlWorkflowId) return
547548

549+
if (hydrationPhase === 'creating') return
550+
548551
// Skip if already in the correct room
549552
if (currentWorkflowId === urlWorkflowId) return
550553

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

567570
const joinWorkflow = useCallback(
568571
(workflowId: string) => {

0 commit comments

Comments
 (0)