Skip to content

Commit 2fc4037

Browse files
committed
improvement: loading, optimistic operations
1 parent e581608 commit 2fc4037

File tree

8 files changed

+180
-146
lines changed

8 files changed

+180
-146
lines changed

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import ReactFlow, {
1111
useReactFlow,
1212
} from 'reactflow'
1313
import 'reactflow/dist/style.css'
14+
import { Loader2 } from 'lucide-react'
1415
import { createLogger } from '@/lib/logs/console/logger'
1516
import { TriggerUtils } from '@/lib/workflows/triggers/triggers'
1617
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
@@ -2187,7 +2188,11 @@ const WorkflowContent = React.memo(() => {
21872188
return (
21882189
<div className='flex h-screen w-full flex-col overflow-hidden'>
21892190
<div className='relative h-full w-full flex-1 transition-all duration-200'>
2190-
<div className='workflow-container h-full' />
2191+
<div className='workflow-container flex h-full items-center justify-center'>
2192+
<div className='flex flex-col items-center gap-3'>
2193+
<Loader2 className='h-[24px] w-[24px] animate-spin text-muted-foreground' />
2194+
</div>
2195+
</div>
21912196
</div>
21922197
<Panel />
21932198
<Terminal />

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/usage-indicator/rotating-digit.tsx

Lines changed: 0 additions & 92 deletions
This file was deleted.

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/usage-indicator/usage-indicator.tsx

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
getUsage,
1313
} from '@/lib/billing/client/utils'
1414
import { createLogger } from '@/lib/logs/console/logger'
15-
import { RotatingDigit } from '@/app/workspace/[workspaceId]/w/components/sidebar/components-new/usage-indicator/rotating-digit'
1615
import { useSocket } from '@/app/workspace/providers/socket-provider'
1716
import { subscriptionKeys, useSubscriptionData } from '@/hooks/queries/subscription'
1817
import { MIN_SIDEBAR_WIDTH, useSidebarStore } from '@/stores/sidebar/store'
@@ -272,15 +271,9 @@ export function UsageIndicator({ onClick }: UsageIndicatorProps) {
272271
</>
273272
) : (
274273
<>
275-
<div className='flex items-center font-medium text-[12px] text-[var(--text-tertiary)]'>
276-
<span className='mr-[1px]'>$</span>
277-
<RotatingDigit
278-
value={usage.current}
279-
height={14}
280-
width={7}
281-
textClassName='font-medium text-[12px] text-[var(--text-tertiary)] tabular-nums'
282-
/>
283-
</div>
274+
<span className='font-medium text-[12px] text-[var(--text-tertiary)] tabular-nums'>
275+
${usage.current}
276+
</span>
284277
<span className='font-medium text-[12px] text-[var(--text-tertiary)]'>/</span>
285278
<span className='font-medium text-[12px] text-[var(--text-tertiary)] tabular-nums'>
286279
${usage.limit}

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ import { useDeleteFolder, useDuplicateFolder } from '@/app/workspace/[workspaceI
1717
import { useUpdateFolder } from '@/hooks/queries/folders'
1818
import { useCreateWorkflow } from '@/hooks/queries/workflows'
1919
import type { FolderTreeNode } from '@/stores/folders/store'
20+
import {
21+
generateCreativeWorkflowName,
22+
getNextWorkflowColor,
23+
} from '@/stores/workflows/registry/utils'
2024

2125
interface FolderItemProps {
2226
folder: FolderTreeNode
@@ -60,12 +64,23 @@ export function FolderItem({ folder, level, hoverHandlers }: FolderItemProps) {
6064
})
6165

6266
/**
63-
* Handle create workflow in folder using React Query mutation
67+
* Handle create workflow in folder using React Query mutation.
68+
* Generates name and color upfront for optimistic UI updates.
6469
*/
6570
const handleCreateWorkflowInFolder = useCallback(async () => {
71+
if (createWorkflowMutation.isPending) {
72+
return
73+
}
74+
75+
// Generate name and color upfront for optimistic updates
76+
const name = generateCreativeWorkflowName()
77+
const color = getNextWorkflowColor()
78+
6679
const result = await createWorkflowMutation.mutateAsync({
6780
workspaceId,
6881
folderId: folder.id,
82+
name,
83+
color,
6984
})
7085

7186
if (result.id) {

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

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useState } from 'react'
1+
import { useCallback } from 'react'
22
import { createLogger } from '@/lib/logs/console/logger'
33
import { generateFolderName } from '@/lib/workspaces/naming'
44
import { useCreateFolder } from '@/hooks/queries/folders'
@@ -12,40 +12,39 @@ interface UseFolderOperationsProps {
1212
/**
1313
* Custom hook to manage folder operations including creating folders.
1414
* Handles folder name generation and state management.
15+
* Uses React Query mutation's isPending state for immediate loading feedback.
1516
*
1617
* @param props - Configuration object containing workspaceId
1718
* @returns Folder operations state and handlers
1819
*/
1920
export function useFolderOperations({ workspaceId }: UseFolderOperationsProps) {
2021
const createFolderMutation = useCreateFolder()
21-
const [isCreatingFolder, setIsCreatingFolder] = useState(false)
2222

2323
/**
24-
* Create folder handler - creates folder with auto-generated name
24+
* Create folder handler - creates folder with auto-generated name.
25+
* Generates name upfront to enable optimistic UI updates.
2526
*/
2627
const handleCreateFolder = useCallback(async (): Promise<string | null> => {
27-
if (isCreatingFolder || !workspaceId) {
28+
if (createFolderMutation.isPending || !workspaceId) {
2829
logger.info('Folder creation already in progress or no workspaceId available')
2930
return null
3031
}
3132

3233
try {
33-
setIsCreatingFolder(true)
34+
// Generate folder name upfront for optimistic updates
3435
const folderName = await generateFolderName(workspaceId)
3536
const folder = await createFolderMutation.mutateAsync({ name: folderName, workspaceId })
3637
logger.info(`Created folder: ${folderName}`)
3738
return folder.id
3839
} catch (error) {
3940
logger.error('Failed to create folder:', { error })
4041
return null
41-
} finally {
42-
setIsCreatingFolder(false)
4342
}
44-
}, [createFolderMutation, workspaceId, isCreatingFolder])
43+
}, [createFolderMutation, workspaceId])
4544

4645
return {
4746
// State
48-
isCreatingFolder,
47+
isCreatingFolder: createFolderMutation.isPending,
4948

5049
// Operations
5150
handleCreateFolder,

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

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
import { useCallback, useState } from 'react'
1+
import { useCallback } from 'react'
22
import { useRouter } from 'next/navigation'
33
import { createLogger } from '@/lib/logs/console/logger'
44
import { useCreateWorkflow, useWorkflows } from '@/hooks/queries/workflows'
55
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
66
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
7+
import {
8+
generateCreativeWorkflowName,
9+
getNextWorkflowColor,
10+
} from '@/stores/workflows/registry/utils'
711

812
const logger = createLogger('useWorkflowOperations')
913

@@ -29,7 +33,6 @@ export function useWorkflowOperations({
2933
const { workflows } = useWorkflowRegistry()
3034
const workflowsQuery = useWorkflows(workspaceId)
3135
const createWorkflowMutation = useCreateWorkflow()
32-
const [isCreatingWorkflow, setIsCreatingWorkflow] = useState(false)
3336

3437
/**
3538
* Filter and sort workflows for the current workspace
@@ -42,25 +45,30 @@ export function useWorkflowOperations({
4245
})
4346

4447
/**
45-
* Create workflow handler - creates workflow and navigates to it
46-
* Now uses React Query mutation for better performance and caching
48+
* Create workflow handler - creates workflow and navigates to it.
49+
* Uses React Query mutation's isPending state for immediate loading feedback.
50+
* Generates name and color upfront to enable optimistic UI updates.
4751
*/
4852
const handleCreateWorkflow = useCallback(async (): Promise<string | null> => {
49-
if (isCreatingWorkflow) {
53+
if (createWorkflowMutation.isPending) {
5054
logger.info('Workflow creation already in progress, ignoring request')
5155
return null
5256
}
5357

5458
try {
55-
setIsCreatingWorkflow(true)
56-
5759
// Clear workflow diff store when creating a new workflow
5860
const { clearDiff } = useWorkflowDiffStore.getState()
5961
clearDiff()
6062

61-
// Use React Query mutation for creation
63+
// Generate name and color upfront for optimistic updates
64+
const name = generateCreativeWorkflowName()
65+
const color = getNextWorkflowColor()
66+
67+
// Use React Query mutation for creation - isPending updates immediately
6268
const result = await createWorkflowMutation.mutateAsync({
63-
workspaceId: workspaceId,
69+
workspaceId,
70+
name,
71+
color,
6472
})
6573

6674
// Navigate to the newly created workflow
@@ -72,17 +80,15 @@ export function useWorkflowOperations({
7280
} catch (error) {
7381
logger.error('Error creating workflow:', error)
7482
return null
75-
} finally {
76-
setIsCreatingWorkflow(false)
7783
}
78-
}, [isCreatingWorkflow, createWorkflowMutation, workspaceId, router])
84+
}, [createWorkflowMutation, workspaceId, router])
7985

8086
return {
8187
// State
8288
workflows,
8389
regularWorkflows,
8490
workflowsLoading: workflowsQuery.isLoading,
85-
isCreatingWorkflow,
91+
isCreatingWorkflow: createWorkflowMutation.isPending,
8692

8793
// Operations
8894
handleCreateWorkflow,

apps/sim/hooks/queries/folders.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ interface CreateFolderVariables {
6565
color?: string
6666
}
6767

68+
interface CreateFolderContext {
69+
tempId: string
70+
previousFolders: Record<string, WorkflowFolder>
71+
}
72+
6873
interface UpdateFolderVariables {
6974
workspaceId: string
7075
id: string
@@ -103,7 +108,62 @@ export function useCreateFolder() {
103108
const { folder } = await response.json()
104109
return mapFolder(folder)
105110
},
106-
onSuccess: (_data, variables) => {
111+
onMutate: async (variables): Promise<CreateFolderContext> => {
112+
// Cancel any outgoing refetches to prevent race conditions
113+
await queryClient.cancelQueries({ queryKey: folderKeys.list(variables.workspaceId) })
114+
115+
// Snapshot previous state for rollback
116+
const previousFolders = { ...useFolderStore.getState().folders }
117+
118+
const tempId = `temp-folder-${Date.now()}`
119+
120+
// Optimistically add folder entry immediately
121+
useFolderStore.setState((state) => ({
122+
folders: {
123+
...state.folders,
124+
[tempId]: {
125+
id: tempId,
126+
name: variables.name,
127+
userId: '',
128+
workspaceId: variables.workspaceId,
129+
parentId: variables.parentId || null,
130+
color: variables.color || '#808080',
131+
isExpanded: false,
132+
sortOrder: 0,
133+
createdAt: new Date(),
134+
updatedAt: new Date(),
135+
},
136+
},
137+
}))
138+
139+
logger.info(`Added optimistic folder entry: ${tempId}`)
140+
return { tempId, previousFolders }
141+
},
142+
onSuccess: (data, _variables, context) => {
143+
logger.info(`Folder ${data.id} created successfully, replacing temp entry ${context.tempId}`)
144+
145+
// Replace optimistic entry with real folder data
146+
useFolderStore.setState((state) => {
147+
const { [context.tempId]: _, ...remainingFolders } = state.folders
148+
return {
149+
folders: {
150+
...remainingFolders,
151+
[data.id]: data,
152+
},
153+
}
154+
})
155+
},
156+
onError: (error: Error, _variables, context) => {
157+
logger.error('Failed to create folder:', error)
158+
159+
// Rollback to previous state snapshot
160+
if (context?.previousFolders) {
161+
useFolderStore.setState({ folders: context.previousFolders })
162+
logger.info(`Rolled back to previous folders state`)
163+
}
164+
},
165+
onSettled: (_data, _error, variables) => {
166+
// Always invalidate to sync with server state
107167
queryClient.invalidateQueries({ queryKey: folderKeys.list(variables.workspaceId) })
108168
},
109169
})

0 commit comments

Comments
 (0)