Skip to content

Commit 3d4e3d2

Browse files
fix(settings): preserve tab when switching workspaces (#6704)
1 parent af076a7 commit 3d4e3d2

2 files changed

Lines changed: 84 additions & 4 deletions

File tree

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

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const {
2020
}))
2121

2222
vi.mock('next/navigation', () => ({
23+
usePathname: () => '/workspace/workspace-denied',
2324
useRouter: () => ({ push: mockPush }),
2425
}))
2526

@@ -49,7 +50,52 @@ vi.mock('@/stores/workflows/registry/store', () => ({
4950
) => selector({ switchToWorkspace: mockSwitchToWorkspace }),
5051
}))
5152

52-
import { useWorkspaceManagement } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-management'
53+
import {
54+
resolveWorkspaceSwitchHref,
55+
useWorkspaceManagement,
56+
} from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-management'
57+
58+
describe('resolveWorkspaceSwitchHref', () => {
59+
it('preserves the active settings section', () => {
60+
expect(
61+
resolveWorkspaceSwitchHref({
62+
pathname: '/workspace/workspace-a/settings/mcp',
63+
currentWorkspaceId: 'workspace-a',
64+
targetWorkspaceId: 'workspace-b',
65+
})
66+
).toBe('/workspace/workspace-b/settings/mcp')
67+
})
68+
69+
it('drops workspace-scoped settings detail segments', () => {
70+
expect(
71+
resolveWorkspaceSwitchHref({
72+
pathname: '/workspace/workspace-a/settings/secrets/credential-a',
73+
currentWorkspaceId: 'workspace-a',
74+
targetWorkspaceId: 'workspace-b',
75+
})
76+
).toBe('/workspace/workspace-b/settings/secrets')
77+
})
78+
79+
it('navigates to the workspace root outside settings', () => {
80+
expect(
81+
resolveWorkspaceSwitchHref({
82+
pathname: '/workspace/workspace-a/w/workflow-a',
83+
currentWorkspaceId: 'workspace-a',
84+
targetWorkspaceId: 'workspace-b',
85+
})
86+
).toBe('/workspace/workspace-b')
87+
})
88+
89+
it('fails fast when a settings pathname has no section', () => {
90+
expect(() =>
91+
resolveWorkspaceSwitchHref({
92+
pathname: '/workspace/workspace-a/settings/',
93+
currentWorkspaceId: 'workspace-a',
94+
targetWorkspaceId: 'workspace-b',
95+
})
96+
).toThrow('Settings pathname is missing a section')
97+
})
98+
})
5399

54100
function Harness() {
55101
useWorkspaceManagement({ workspaceId: 'workspace-denied', sessionUserId: 'user-1' })

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

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
22
import { createLogger } from '@sim/logger'
3-
import { useRouter } from 'next/navigation'
3+
import { usePathname, useRouter } from 'next/navigation'
44
import { requestJson } from '@/lib/api/client/request'
55
import { updateUserSettingsContract } from '@/lib/api/contracts'
66
import { WorkspaceRecencyStorage } from '@/lib/core/utils/browser-storage'
@@ -25,6 +25,33 @@ interface UseWorkspaceManagementProps {
2525
sessionUserId?: string
2626
}
2727

28+
interface ResolveWorkspaceSwitchHrefParams {
29+
pathname: string
30+
currentWorkspaceId: string
31+
targetWorkspaceId: string
32+
}
33+
34+
/**
35+
* Keeps the active settings section across workspace switches without carrying
36+
* workspace-scoped detail IDs into the destination workspace.
37+
*/
38+
export function resolveWorkspaceSwitchHref({
39+
pathname,
40+
currentWorkspaceId,
41+
targetWorkspaceId,
42+
}: ResolveWorkspaceSwitchHrefParams): string {
43+
const targetWorkspaceHref = `/workspace/${targetWorkspaceId}`
44+
const settingsPrefix = `/workspace/${currentWorkspaceId}/settings/`
45+
if (!pathname.startsWith(settingsPrefix)) return targetWorkspaceHref
46+
47+
const [section] = pathname.slice(settingsPrefix.length).split('/')
48+
if (!section) {
49+
throw new Error(`Settings pathname is missing a section: ${pathname}`)
50+
}
51+
52+
return `${targetWorkspaceHref}/settings/${section}`
53+
}
54+
2855
/**
2956
* Manages workspace operations including fetching, switching, creating, deleting, and leaving workspaces.
3057
* Handles URL synchronization and recency-based ordering. Route access is
@@ -40,6 +67,7 @@ export function useWorkspaceManagement({
4067
sessionUserId,
4168
}: UseWorkspaceManagementProps) {
4269
const router = useRouter()
70+
const pathname = usePathname()
4371
const switchToWorkspace = useWorkflowRegistry((state) => state.switchToWorkspace)
4472

4573
const { data: workspaces = [], isLoading: isWorkspacesLoading } = useWorkspacesQuery(
@@ -157,15 +185,21 @@ export function useWorkspaceManagement({
157185
return
158186
}
159187

188+
const href = resolveWorkspaceSwitchHref({
189+
pathname,
190+
currentWorkspaceId: workspaceIdRef.current,
191+
targetWorkspaceId: workspace.id,
192+
})
193+
160194
try {
161195
switchToWorkspace(workspace.id)
162-
routerRef.current?.push(`/workspace/${workspace.id}`)
196+
routerRef.current.push(href)
163197
logger.info(`Switched to workspace: ${workspace.name} (${workspace.id})`)
164198
} catch (error) {
165199
logger.error('Error switching workspace:', error)
166200
}
167201
},
168-
[switchToWorkspace]
202+
[pathname, switchToWorkspace]
169203
)
170204

171205
const handleCreateWorkspace = useCallback(

0 commit comments

Comments
 (0)