From 715c4c83856dad35ec26735d904ae330ab8c5ee7 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Fri, 14 Aug 2026 13:18:04 -0700 Subject: [PATCH] fix(settings): preserve tab when switching workspaces --- .../hooks/use-workspace-management.test.tsx | 48 ++++++++++++++++++- .../sidebar/hooks/use-workspace-management.ts | 40 ++++++++++++++-- 2 files changed, 84 insertions(+), 4 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-management.test.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-management.test.tsx index cfba54d6f8d..0a1501a66e4 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-management.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-management.test.tsx @@ -20,6 +20,7 @@ const { })) vi.mock('next/navigation', () => ({ + usePathname: () => '/workspace/workspace-denied', useRouter: () => ({ push: mockPush }), })) @@ -49,7 +50,52 @@ vi.mock('@/stores/workflows/registry/store', () => ({ ) => selector({ switchToWorkspace: mockSwitchToWorkspace }), })) -import { useWorkspaceManagement } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-management' +import { + resolveWorkspaceSwitchHref, + useWorkspaceManagement, +} from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-management' + +describe('resolveWorkspaceSwitchHref', () => { + it('preserves the active settings section', () => { + expect( + resolveWorkspaceSwitchHref({ + pathname: '/workspace/workspace-a/settings/mcp', + currentWorkspaceId: 'workspace-a', + targetWorkspaceId: 'workspace-b', + }) + ).toBe('/workspace/workspace-b/settings/mcp') + }) + + it('drops workspace-scoped settings detail segments', () => { + expect( + resolveWorkspaceSwitchHref({ + pathname: '/workspace/workspace-a/settings/secrets/credential-a', + currentWorkspaceId: 'workspace-a', + targetWorkspaceId: 'workspace-b', + }) + ).toBe('/workspace/workspace-b/settings/secrets') + }) + + it('navigates to the workspace root outside settings', () => { + expect( + resolveWorkspaceSwitchHref({ + pathname: '/workspace/workspace-a/w/workflow-a', + currentWorkspaceId: 'workspace-a', + targetWorkspaceId: 'workspace-b', + }) + ).toBe('/workspace/workspace-b') + }) + + it('fails fast when a settings pathname has no section', () => { + expect(() => + resolveWorkspaceSwitchHref({ + pathname: '/workspace/workspace-a/settings/', + currentWorkspaceId: 'workspace-a', + targetWorkspaceId: 'workspace-b', + }) + ).toThrow('Settings pathname is missing a section') + }) +}) function Harness() { useWorkspaceManagement({ workspaceId: 'workspace-denied', sessionUserId: 'user-1' }) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-management.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-management.ts index e4a444de479..0deca94ef97 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-management.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-management.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createLogger } from '@sim/logger' -import { useRouter } from 'next/navigation' +import { usePathname, useRouter } from 'next/navigation' import { requestJson } from '@/lib/api/client/request' import { updateUserSettingsContract } from '@/lib/api/contracts' import { WorkspaceRecencyStorage } from '@/lib/core/utils/browser-storage' @@ -25,6 +25,33 @@ interface UseWorkspaceManagementProps { sessionUserId?: string } +interface ResolveWorkspaceSwitchHrefParams { + pathname: string + currentWorkspaceId: string + targetWorkspaceId: string +} + +/** + * Keeps the active settings section across workspace switches without carrying + * workspace-scoped detail IDs into the destination workspace. + */ +export function resolveWorkspaceSwitchHref({ + pathname, + currentWorkspaceId, + targetWorkspaceId, +}: ResolveWorkspaceSwitchHrefParams): string { + const targetWorkspaceHref = `/workspace/${targetWorkspaceId}` + const settingsPrefix = `/workspace/${currentWorkspaceId}/settings/` + if (!pathname.startsWith(settingsPrefix)) return targetWorkspaceHref + + const [section] = pathname.slice(settingsPrefix.length).split('/') + if (!section) { + throw new Error(`Settings pathname is missing a section: ${pathname}`) + } + + return `${targetWorkspaceHref}/settings/${section}` +} + /** * Manages workspace operations including fetching, switching, creating, deleting, and leaving workspaces. * Handles URL synchronization and recency-based ordering. Route access is @@ -40,6 +67,7 @@ export function useWorkspaceManagement({ sessionUserId, }: UseWorkspaceManagementProps) { const router = useRouter() + const pathname = usePathname() const switchToWorkspace = useWorkflowRegistry((state) => state.switchToWorkspace) const { data: workspaces = [], isLoading: isWorkspacesLoading } = useWorkspacesQuery( @@ -157,15 +185,21 @@ export function useWorkspaceManagement({ return } + const href = resolveWorkspaceSwitchHref({ + pathname, + currentWorkspaceId: workspaceIdRef.current, + targetWorkspaceId: workspace.id, + }) + try { switchToWorkspace(workspace.id) - routerRef.current?.push(`/workspace/${workspace.id}`) + routerRef.current.push(href) logger.info(`Switched to workspace: ${workspace.name} (${workspace.id})`) } catch (error) { logger.error('Error switching workspace:', error) } }, - [switchToWorkspace] + [pathname, switchToWorkspace] ) const handleCreateWorkspace = useCallback(