Skip to content

Commit 7101dc5

Browse files
improvement: loading, optimistic actions (#2193)
* improvement: loading, optimistic operations * improvement: folders update * fix usage indicator rounding + new tsconfig * remove redundant checks * fix hmr case for missing workflow loads * add abstraction for zustand/react hybrid optimism * remove comments --------- Co-authored-by: Vikhyath Mondreti <[email protected]>
1 parent 58251e2 commit 7101dc5

File tree

14 files changed

+511
-238
lines changed

14 files changed

+511
-238
lines changed

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

Lines changed: 17 additions & 4 deletions
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 type { OAuthConnectEventDetail } from '@/lib/copilot/tools/client/other/oauth-request-access'
1516
import { createLogger } from '@/lib/logs/console/logger'
1617
import type { OAuthProvider } from '@/lib/oauth'
@@ -1276,7 +1277,7 @@ const WorkflowContent = React.memo(() => {
12761277
[screenToFlowPosition, isPointInLoopNode, getNodes]
12771278
)
12781279

1279-
// Initialize workflow when it exists in registry and isn't active
1280+
// Initialize workflow when it exists in registry and isn't active or needs hydration
12801281
useEffect(() => {
12811282
let cancelled = false
12821283
const currentId = params.workflowId as string
@@ -1294,8 +1295,16 @@ const WorkflowContent = React.memo(() => {
12941295
return
12951296
}
12961297

1297-
if (activeWorkflowId !== currentId) {
1298-
// Clear diff and set as active
1298+
// Check if we need to load the workflow state:
1299+
// 1. Different workflow than currently active
1300+
// 2. Same workflow but hydration phase is not 'ready' (e.g., after a quick refresh)
1301+
const needsWorkflowLoad =
1302+
activeWorkflowId !== currentId ||
1303+
(activeWorkflowId === currentId &&
1304+
hydration.phase !== 'ready' &&
1305+
hydration.phase !== 'state-loading')
1306+
1307+
if (needsWorkflowLoad) {
12991308
const { clearDiff } = useWorkflowDiffStore.getState()
13001309
clearDiff()
13011310

@@ -2216,7 +2225,11 @@ const WorkflowContent = React.memo(() => {
22162225
return (
22172226
<div className='flex h-screen w-full flex-col overflow-hidden'>
22182227
<div className='relative h-full w-full flex-1 transition-all duration-200'>
2219-
<div className='workflow-container h-full' />
2228+
<div className='workflow-container flex h-full items-center justify-center'>
2229+
<div className='flex flex-col items-center gap-3'>
2230+
<Loader2 className='h-[24px] w-[24px] animate-spin text-muted-foreground' />
2231+
</div>
2232+
</div>
22202233
</div>
22212234
<Panel />
22222235
<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: 4 additions & 11 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,18 +271,12 @@ 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.toFixed(2)}
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'>
286-
${usage.limit}
279+
${usage.limit.toFixed(2)}
287280
</span>
288281
</>
289282
)}

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

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ interface ContextMenuProps {
8181
* Set to true when user lacks permissions
8282
*/
8383
disableDelete?: boolean
84+
/**
85+
* Whether the create option is disabled (default: false)
86+
* Set to true when creation is in progress or user lacks permissions
87+
*/
88+
disableCreate?: boolean
8489
}
8590

8691
/**
@@ -108,6 +113,7 @@ export function ContextMenu({
108113
disableRename = false,
109114
disableDuplicate = false,
110115
disableDelete = false,
116+
disableCreate = false,
111117
}: ContextMenuProps) {
112118
return (
113119
<Popover open={isOpen} onOpenChange={onClose}>
@@ -125,10 +131,8 @@ export function ContextMenu({
125131
<PopoverItem
126132
disabled={disableRename}
127133
onClick={() => {
128-
if (!disableRename) {
129-
onRename()
130-
onClose()
131-
}
134+
onRename()
135+
onClose()
132136
}}
133137
>
134138
<Pencil className='h-3 w-3' />
@@ -137,6 +141,7 @@ export function ContextMenu({
137141
)}
138142
{showCreate && onCreate && (
139143
<PopoverItem
144+
disabled={disableCreate}
140145
onClick={() => {
141146
onCreate()
142147
onClose()
@@ -150,10 +155,8 @@ export function ContextMenu({
150155
<PopoverItem
151156
disabled={disableDuplicate}
152157
onClick={() => {
153-
if (!disableDuplicate) {
154-
onDuplicate()
155-
onClose()
156-
}
158+
onDuplicate()
159+
onClose()
157160
}}
158161
>
159162
<Copy className='h-3 w-3' />
@@ -164,10 +167,8 @@ export function ContextMenu({
164167
<PopoverItem
165168
disabled={disableExport}
166169
onClick={() => {
167-
if (!disableExport) {
168-
onExport()
169-
onClose()
170-
}
170+
onExport()
171+
onClose()
171172
}}
172173
>
173174
<ArrowUp className='h-3 w-3' />
@@ -177,10 +178,8 @@ export function ContextMenu({
177178
<PopoverItem
178179
disabled={disableDelete}
179180
onClick={() => {
180-
if (!disableDelete) {
181-
onDelete()
182-
onClose()
183-
}
181+
onDelete()
182+
onClose()
184183
}}
185184
>
186185
<Trash className='h-3 w-3' />

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

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useCallback, useState } from 'react'
44
import clsx from 'clsx'
55
import { ChevronRight, Folder, FolderOpen } from 'lucide-react'
66
import { useParams, useRouter } from 'next/navigation'
7+
import { createLogger } from '@/lib/logs/console/logger'
78
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
89
import { ContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/components-new/workflow-list/components/context-menu/context-menu'
910
import { DeleteModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components-new/workflow-list/components/delete-modal/delete-modal'
@@ -17,6 +18,12 @@ import { useDeleteFolder, useDuplicateFolder } from '@/app/workspace/[workspaceI
1718
import { useUpdateFolder } from '@/hooks/queries/folders'
1819
import { useCreateWorkflow } from '@/hooks/queries/workflows'
1920
import type { FolderTreeNode } from '@/stores/folders/store'
21+
import {
22+
generateCreativeWorkflowName,
23+
getNextWorkflowColor,
24+
} from '@/stores/workflows/registry/utils'
25+
26+
const logger = createLogger('FolderItem')
2027

2128
interface FolderItemProps {
2229
folder: FolderTreeNode
@@ -60,16 +67,29 @@ export function FolderItem({ folder, level, hoverHandlers }: FolderItemProps) {
6067
})
6168

6269
/**
63-
* Handle create workflow in folder using React Query mutation
70+
* Handle create workflow in folder using React Query mutation.
71+
* Generates name and color upfront for optimistic UI updates.
72+
* The UI disables the trigger when isPending, so no guard needed here.
6473
*/
6574
const handleCreateWorkflowInFolder = useCallback(async () => {
66-
const result = await createWorkflowMutation.mutateAsync({
67-
workspaceId,
68-
folderId: folder.id,
69-
})
75+
try {
76+
// Generate name and color upfront for optimistic updates
77+
const name = generateCreativeWorkflowName()
78+
const color = getNextWorkflowColor()
79+
80+
const result = await createWorkflowMutation.mutateAsync({
81+
workspaceId,
82+
folderId: folder.id,
83+
name,
84+
color,
85+
})
7086

71-
if (result.id) {
72-
router.push(`/workspace/${workspaceId}/w/${result.id}`)
87+
if (result.id) {
88+
router.push(`/workspace/${workspaceId}/w/${result.id}`)
89+
}
90+
} catch (error) {
91+
// Error already handled by mutation's onError callback
92+
logger.error('Failed to create workflow in folder:', error)
7393
}
7494
}, [createWorkflowMutation, workspaceId, folder.id, router])
7595

@@ -263,6 +283,7 @@ export function FolderItem({ folder, level, hoverHandlers }: FolderItemProps) {
263283
onDelete={() => setIsDeleteModalOpen(true)}
264284
showCreate={true}
265285
disableRename={!userPermissions.canEdit}
286+
disableCreate={!userPermissions.canEdit || createWorkflowMutation.isPending}
266287
disableDuplicate={!userPermissions.canEdit}
267288
disableDelete={!userPermissions.canEdit}
268289
/>

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

Lines changed: 5 additions & 12 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,33 @@ 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

23-
/**
24-
* Create folder handler - creates folder with auto-generated name
25-
*/
2623
const handleCreateFolder = useCallback(async (): Promise<string | null> => {
27-
if (isCreatingFolder || !workspaceId) {
28-
logger.info('Folder creation already in progress or no workspaceId available')
24+
if (!workspaceId) {
2925
return null
3026
}
3127

3228
try {
33-
setIsCreatingFolder(true)
3429
const folderName = await generateFolderName(workspaceId)
3530
const folder = await createFolderMutation.mutateAsync({ name: folderName, workspaceId })
3631
logger.info(`Created folder: ${folderName}`)
3732
return folder.id
3833
} catch (error) {
3934
logger.error('Failed to create folder:', { error })
4035
return null
41-
} finally {
42-
setIsCreatingFolder(false)
4336
}
44-
}, [createFolderMutation, workspaceId, isCreatingFolder])
37+
}, [createFolderMutation, workspaceId])
4538

4639
return {
4740
// State
48-
isCreatingFolder,
41+
isCreatingFolder: createFolderMutation.isPending,
4942

5043
// Operations
5144
handleCreateFolder,

0 commit comments

Comments
 (0)