Skip to content

Commit 391c3ef

Browse files
authored
improvement(sockets): fixed positioning of blocks recalc, hydration issue with workspaceId, and presence (#547)
* fixed positioning of blocks recalc, hydration issue with workspaceId, and presence * fixed loading animation, auto-close workspace selector onClick
1 parent d30e116 commit 391c3ef

9 files changed

Lines changed: 184 additions & 129 deletions

File tree

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/user-avatar-stack/user-avatar-stack.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,9 @@ export function UserAvatarStack({
4343
}
4444
}, [users, maxVisible])
4545

46-
// Don't render anything if there are no users
47-
if (users.length === 0) {
46+
// Only show presence when there are multiple users (>1)
47+
// Don't render anything if there are no users or only 1 user
48+
if (users.length <= 1) {
4849
return null
4950
}
5051

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -239,10 +239,9 @@ WorkspaceEditModal.displayName = 'WorkspaceEditModal'
239239
export const WorkspaceHeader = React.memo<WorkspaceHeaderProps>(
240240
({ onCreateWorkflow, isCollapsed, onDropdownOpenChange }) => {
241241
// Get sidebar store state to check current mode
242-
const { mode, workspaceDropdownOpen, setAnyModalOpen } = useSidebarStore()
242+
const { mode, workspaceDropdownOpen, setWorkspaceDropdownOpen, setAnyModalOpen } =
243+
useSidebarStore()
243244

244-
// Keep local isOpen state in sync with the store (for internal component use)
245-
const [isOpen, setIsOpen] = useState(workspaceDropdownOpen)
246245
const { data: sessionData, isPending } = useSession()
247246
const [plan, setPlan] = useState('Free Plan')
248247
// Use client-side loading instead of isPending to avoid hydration mismatch
@@ -335,22 +334,22 @@ export const WorkspaceHeader = React.memo<WorkspaceHeaderProps>(
335334

336335
const switchWorkspace = useCallback(
337336
(workspace: Workspace) => {
338-
// If already on this workspace, do nothing
337+
// If already on this workspace, close dropdown and do nothing else
339338
if (activeWorkspace?.id === workspace.id) {
340-
setIsOpen(false)
339+
setWorkspaceDropdownOpen(false)
341340
return
342341
}
343342

344343
setActiveWorkspace(workspace)
345-
setIsOpen(false)
344+
setWorkspaceDropdownOpen(false)
346345

347346
// Use full workspace switch which now handles localStorage automatically
348347
switchToWorkspace(workspace.id)
349348

350349
// Update URL to include workspace ID
351350
router.push(`/workspace/${workspace.id}/w`)
352351
},
353-
[activeWorkspace?.id, switchToWorkspace, router]
352+
[activeWorkspace?.id, switchToWorkspace, router, setWorkspaceDropdownOpen]
354353
)
355354

356355
const handleCreateWorkspace = useCallback(
@@ -472,7 +471,7 @@ export const WorkspaceHeader = React.memo<WorkspaceHeaderProps>(
472471
setActiveWorkspace(updatedWorkspaces[0])
473472
}
474473

475-
setIsOpen(false)
474+
setWorkspaceDropdownOpen(false)
476475
} catch (err) {
477476
logger.error('Error deleting workspace:', err)
478477
} finally {
@@ -504,13 +503,13 @@ export const WorkspaceHeader = React.memo<WorkspaceHeaderProps>(
504503
// Notify parent component when dropdown opens/closes
505504
const handleDropdownOpenChange = useCallback(
506505
(open: boolean) => {
507-
setIsOpen(open)
506+
setWorkspaceDropdownOpen(open)
508507
// Inform the parent component about the dropdown state change
509508
if (onDropdownOpenChange) {
510509
onDropdownOpenChange(open)
511510
}
512511
},
513-
[onDropdownOpenChange]
512+
[onDropdownOpenChange, setWorkspaceDropdownOpen]
514513
)
515514

516515
// Special handling for click interactions in hover mode
@@ -521,10 +520,10 @@ export const WorkspaceHeader = React.memo<WorkspaceHeaderProps>(
521520
e.stopPropagation()
522521
e.preventDefault()
523522
// Toggle dropdown state
524-
handleDropdownOpenChange(!isOpen)
523+
handleDropdownOpenChange(!workspaceDropdownOpen)
525524
}
526525
},
527-
[mode, isOpen, handleDropdownOpenChange]
526+
[mode, workspaceDropdownOpen, handleDropdownOpenChange]
528527
)
529528

530529
const handleContainerClick = useCallback(
@@ -568,7 +567,7 @@ export const WorkspaceHeader = React.memo<WorkspaceHeaderProps>(
568567
workspace={editingWorkspace}
569568
/>
570569

571-
<DropdownMenu open={isOpen} onOpenChange={handleDropdownOpenChange}>
570+
<DropdownMenu open={workspaceDropdownOpen} onOpenChange={handleDropdownOpenChange}>
572571
<div
573572
className={`group relative cursor-pointer rounded-md ${isCollapsed ? 'flex justify-center' : ''}`}
574573
onClick={handleContainerClick}
@@ -600,7 +599,7 @@ export const WorkspaceHeader = React.memo<WorkspaceHeaderProps>(
600599
href={workspaceUrl}
601600
className='group flex h-6 w-6 shrink-0 items-center justify-center rounded bg-[#802FFF]'
602601
onClick={(e) => {
603-
if (isOpen) e.preventDefault()
602+
if (workspaceDropdownOpen) e.preventDefault()
604603
}}
605604
>
606605
<AgentIcon className='-translate-y-[0.5px] h-[18px] w-[18px] text-white transition-all group-hover:scale-105' />
Lines changed: 43 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,12 @@
11
'use client'
22

3-
import { useEffect } from 'react'
3+
import { useEffect, useState } from 'react'
44
import { useParams, usePathname, useRouter } from 'next/navigation'
55
import { createLogger } from '@/lib/logs/console-logger'
66
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
77

88
const logger = createLogger('UseRegistryLoading')
99

10-
/**
11-
* Extract workflow ID from pathname
12-
* @param pathname - Current pathname
13-
* @returns workflow ID if found, null otherwise
14-
*/
15-
function extractWorkflowIdFromPathname(pathname: string): string | null {
16-
try {
17-
const pathSegments = pathname.split('/')
18-
// Check if URL matches pattern /w/{workflowId}
19-
if (pathSegments.length >= 3 && pathSegments[1] === 'w') {
20-
const workflowId = pathSegments[2]
21-
// Basic UUID validation (36 characters, contains hyphens)
22-
if (workflowId && workflowId.length === 36 && workflowId.includes('-')) {
23-
return workflowId
24-
}
25-
}
26-
return null
27-
} catch (error) {
28-
logger.warn('Failed to extract workflow ID from pathname:', error)
29-
return null
30-
}
31-
}
32-
3310
/**
3411
* Custom hook to manage workflow registry loading state and handle first-time navigation
3512
*
@@ -43,35 +20,55 @@ export function useRegistryLoading() {
4320
const params = useParams()
4421
const workspaceId = params.workspaceId as string
4522

46-
// Load workflows for current workspace
23+
// Track hydration state to prevent premature API calls
24+
const [isHydrated, setIsHydrated] = useState(false)
25+
26+
// Handle client-side hydration
27+
useEffect(() => {
28+
setIsHydrated(true)
29+
}, [])
30+
31+
// Load workflows for current workspace only after hydration
4732
useEffect(() => {
48-
if (workspaceId) {
49-
loadWorkflows(workspaceId).catch((error) => {
50-
logger.warn('Failed to load workflows for workspace:', error)
51-
})
33+
// Only proceed if we're hydrated and have a valid workspaceId
34+
if (
35+
!isHydrated ||
36+
!workspaceId ||
37+
typeof workspaceId !== 'string' ||
38+
workspaceId.trim() === ''
39+
) {
40+
return
5241
}
53-
}, [workspaceId, loadWorkflows])
42+
43+
logger.debug('Loading workflows for workspace:', workspaceId)
44+
loadWorkflows(workspaceId).catch((error) => {
45+
logger.warn('Failed to load workflows for workspace:', error)
46+
})
47+
}, [isHydrated, workspaceId, loadWorkflows])
5448

5549
// Handle first-time navigation: if we're at /w and have workflows, navigate to first one
5650
useEffect(() => {
57-
if (!isLoading && workspaceId && Object.keys(workflows).length > 0) {
58-
const currentWorkflowId = extractWorkflowIdFromPathname(pathname)
51+
// Only proceed if hydrated and we have valid data
52+
if (!isHydrated || !workspaceId || isLoading || Object.keys(workflows).length === 0) {
53+
return
54+
}
5955

60-
// Check if we're on the workspace root and need to redirect to first workflow
61-
if (
62-
(pathname === `/workspace/${workspaceId}/w` ||
63-
pathname === `/workspace/${workspaceId}/w/`) &&
64-
Object.keys(workflows).length > 0
65-
) {
66-
const firstWorkflowId = Object.keys(workflows)[0]
67-
logger.info('First-time navigation: redirecting to first workflow:', firstWorkflowId)
68-
router.replace(`/workspace/${workspaceId}/w/${firstWorkflowId}`)
69-
}
56+
// Check if we're on the workspace root and need to redirect to first workflow
57+
if (
58+
(pathname === `/workspace/${workspaceId}/w` || pathname === `/workspace/${workspaceId}/w/`) &&
59+
Object.keys(workflows).length > 0
60+
) {
61+
const firstWorkflowId = Object.keys(workflows)[0]
62+
logger.info('First-time navigation: redirecting to first workflow:', firstWorkflowId)
63+
router.replace(`/workspace/${workspaceId}/w/${firstWorkflowId}`)
7064
}
71-
}, [isLoading, workspaceId, workflows, pathname, router])
65+
}, [isHydrated, isLoading, workspaceId, workflows, pathname, router])
7266

73-
// Handle loading states
67+
// Handle loading states - only after hydration
7468
useEffect(() => {
69+
// Don't manage loading state until we're hydrated
70+
if (!isHydrated) return
71+
7572
// Only set loading if we don't have workflows and aren't already loading
7673
if (Object.keys(workflows).length === 0 && !isLoading) {
7774
setLoading(true)
@@ -83,26 +80,6 @@ export function useRegistryLoading() {
8380
return
8481
}
8582

86-
// Only create timeout if we're actually loading
87-
if (!isLoading) return
88-
89-
// Create a timeout to clear loading state after max time
90-
const timeout = setTimeout(() => {
91-
setLoading(false)
92-
}, 3000) // 3 second maximum loading time
93-
94-
// Listen for workflows to be loaded
95-
const checkInterval = setInterval(() => {
96-
const currentWorkflows = useWorkflowRegistry.getState().workflows
97-
if (Object.keys(currentWorkflows).length > 0) {
98-
setLoading(false)
99-
clearInterval(checkInterval)
100-
}
101-
}, 200)
102-
103-
return () => {
104-
clearTimeout(timeout)
105-
clearInterval(checkInterval)
106-
}
107-
}, [setLoading, workflows, isLoading])
83+
// The fetch function itself handles setting isLoading to false
84+
}, [isHydrated, setLoading, workflows, isLoading])
10885
}

apps/sim/app/workspace/[workspaceId]/w/page.tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { useEffect } from 'react'
3+
import { useEffect, useState } from 'react'
44
import { useParams, useRouter } from 'next/navigation'
55
import { LoadingAgent } from '@/components/ui/loading-agent'
66
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
@@ -11,7 +11,20 @@ export default function WorkflowsPage() {
1111
const params = useParams()
1212
const workspaceId = params.workspaceId
1313

14+
// Track hydration state to prevent premature redirects
15+
const [isHydrated, setIsHydrated] = useState(false)
16+
17+
// Handle client-side hydration
18+
useEffect(() => {
19+
setIsHydrated(true)
20+
}, [])
21+
1422
useEffect(() => {
23+
// Don't do anything until we're hydrated and have a valid workspaceId
24+
if (!isHydrated || !workspaceId || typeof workspaceId !== 'string') {
25+
return
26+
}
27+
1528
// Wait for workflows to load
1629
if (isLoading) return
1730

@@ -23,11 +36,11 @@ export default function WorkflowsPage() {
2336
return
2437
}
2538

26-
// If no workflows exist, this means the workspace creation didn't work properly
27-
// or the user doesn't have any workspaces. Redirect to home to let the system
28-
// handle workspace/workflow creation properly.
39+
// If no workflows exist after loading is complete, this means the workspace creation
40+
// didn't work properly or the user doesn't have any workspaces.
41+
// Redirect to home to let the system handle workspace/workflow creation properly.
2942
router.replace('/')
30-
}, [workflows, isLoading, router, workspaceId])
43+
}, [isHydrated, workflows, isLoading, router, workspaceId])
3144

3245
// Show loading state while determining where to redirect
3346
return (

apps/sim/components/ui/loading-agent.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,10 @@ export function LoadingAgent({ size = 'md' }: LoadingAgentProps) {
3333
strokeWidth='1.8'
3434
strokeLinecap='round'
3535
strokeLinejoin='round'
36-
// The magic: dash array & offset
3736
style={{
3837
strokeDasharray: pathLength,
3938
strokeDashoffset: pathLength,
40-
animation: 'dash 1.5s linear forwards',
39+
animation: 'dashLoop 3s linear infinite',
4140
}}
4241
/>
4342
<path
@@ -49,8 +48,8 @@ export function LoadingAgent({ size = 'md' }: LoadingAgentProps) {
4948
style={{
5049
strokeDasharray: pathLength,
5150
strokeDashoffset: pathLength,
52-
animation: 'dash 1.5s linear forwards',
53-
animationDelay: '0.5s', // if you want to stagger it
51+
animation: 'dashLoop 3s linear infinite',
52+
animationDelay: '0.5s',
5453
}}
5554
/>
5655
<path
@@ -62,16 +61,22 @@ export function LoadingAgent({ size = 'md' }: LoadingAgentProps) {
6261
style={{
6362
strokeDasharray: pathLength,
6463
strokeDashoffset: pathLength,
65-
animation: 'dash 1.5s linear forwards',
64+
animation: 'dashLoop 3s linear infinite',
6665
animationDelay: '1s',
6766
}}
6867
/>
6968
<style>
7069
{`
71-
@keyframes dash {
72-
to {
70+
@keyframes dashLoop {
71+
0% {
72+
stroke-dashoffset: ${pathLength};
73+
}
74+
50% {
7375
stroke-dashoffset: 0;
7476
}
77+
100% {
78+
stroke-dashoffset: ${pathLength};
79+
}
7580
}
7681
`}
7782
</style>

0 commit comments

Comments
 (0)