From 509c797be19fc451be49bab757dcc94f9d14914b Mon Sep 17 00:00:00 2001 From: Saxon Fletcher Date: Tue, 22 Sep 2026 11:34:31 +1000 Subject: [PATCH] fix(studio): keep Explorer Home tab selected (#50684) ## Problem Selecting Explorer's Home tab immediately redirects back to the last visited tab. ## Solution Clear the saved Explorer destination before navigating Home. Explorer owns this behavior through a generic `onTabChange` callback on the shared tabs component. ## Review instructions 1. Open Explorer with the Home tab visible, then open a query, notebook, or chat. 2. Click Home. Confirm it stays selected and the previous tab remains open. 3. Switch back to the previous tab, then use Tab and arrow keys to select Home. Confirm it stays selected. 4. Select a content tab, leave Explorer, and return. Confirm the last visited tab still restores. ## Validation Mouse and keyboard regression tests and existing restoration tests pass (12 tests). Typecheck, targeted lint, and formatting checks pass. ## Checklist - [x] I have read [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md). - Docs authoring checks: not applicable; no docs changes. ## Summary by CodeRabbit * **Bug Fixes** * Explorer navigation now clears the previously saved Explorer destination when returning to the home tab. * Returning to Explorer home works consistently through both mouse and keyboard navigation. * **Tests** * Added coverage for Explorer tab navigation, history cleanup, and selected-tab state. --- .../ExplorerLayout/ExplorerLayout.test.tsx | 103 ++++++++++++++++++ .../layouts/ExplorerLayout/ExplorerLayout.tsx | 7 ++ apps/studio/components/layouts/Tabs/Tabs.tsx | 3 + 3 files changed, 113 insertions(+) create mode 100644 apps/studio/components/layouts/ExplorerLayout/ExplorerLayout.test.tsx diff --git a/apps/studio/components/layouts/ExplorerLayout/ExplorerLayout.test.tsx b/apps/studio/components/layouts/ExplorerLayout/ExplorerLayout.test.tsx new file mode 100644 index 00000000000..79d443b8f87 --- /dev/null +++ b/apps/studio/components/layouts/ExplorerLayout/ExplorerLayout.test.tsx @@ -0,0 +1,103 @@ +import { QueryClient } from '@tanstack/react-query' +import { screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { LOCAL_STORAGE_KEYS } from 'common' +import mockRouter from 'next-router-mock' +import type { ReactNode } from 'react' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { ExplorerLayout } from './ExplorerLayout' +import { createTabsState, TabsStateContext } from '@/state/tabs' +import { customRender } from '@/tests/lib/custom-render' + +vi.mock('next/navigation', () => ({ + usePathname: () => mockRouter.asPath, +})) + +vi.mock('../ProjectLayout', () => ({ + ProjectLayoutWithAuth: ({ children }: { children: ReactNode }) => <>{children}, +})) + +vi.mock('@/components/interfaces/Account/Preferences/useExplorerPreferences', () => ({ + useExplorerPreferences: () => ({ home: 'home', hasCompletedOnboarding: true, isReady: true }), +})) + +vi.mock('@/components/interfaces/Explorer/ExplorerQueryTabCoordinator', () => ({ + ExplorerQueryTabCoordinator: () => null, +})) + +vi.mock('@/components/interfaces/Explorer/ExplorerNotebookTabCoordinator', () => ({ + ExplorerNotebookTabCoordinator: () => null, +})) + +vi.mock('@/components/interfaces/Explorer/hooks', () => ({ + useCreateChat: () => ({ createChat: vi.fn() }), + useCreateNotebook: () => ({ createNotebook: vi.fn() }), + useCreateQuery: () => ({ createQuery: vi.fn() }), +})) + +describe('Explorer Home tab navigation', () => { + beforeEach(() => { + vi.restoreAllMocks() + localStorage.clear() + mockRouter.setCurrentUrl({ + pathname: '/project/[ref]/explorer/query/[id]', + query: { ref: 'default', id: 'query-1' }, + }) + }) + + it.each(['mouse', 'keyboard'] as const)( + 'clears the saved Explorer destination before navigating Home with the %s', + async (input) => { + const user = userEvent.setup() + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false, staleTime: Infinity } }, + }) + const historyKey = ['localStorage', LOCAL_STORAGE_KEYS.DASHBOARD_HISTORY('default')] + const otherHistory = { editor: 'table-1', sql: 'snippet-1' } + queryClient.setQueryData(historyKey, { + ...otherHistory, + explorer: { type: 'query', id: 'query-1' }, + }) + + const tabs = createTabsState('default') + tabs.addTab({ + id: 'query-query-1', + type: 'query', + label: 'My query', + metadata: { queryId: 'query-1' }, + isPreview: false, + }) + let historyAtNavigation: unknown + const push = vi.spyOn(mockRouter, 'push') + push.mockImplementationOnce(async () => { + historyAtNavigation = queryClient.getQueryData(historyKey) + return true + }) + + customRender( + + Explorer content + , + { queryClient } + ) + + if (input === 'mouse') { + await user.click(screen.getByRole('tab', { name: 'Open Explorer home' })) + } else { + await user.tab() + expect(screen.getByRole('tab', { name: 'My query' })).toHaveFocus() + await user.keyboard('{ArrowLeft}') + } + + expect(push).toHaveBeenCalledWith('/project/default/explorer') + expect(historyAtNavigation).toEqual(otherHistory) + expect(queryClient.getQueryData(historyKey)).toEqual(otherHistory) + expect(screen.getByRole('tab', { name: 'Open Explorer home' })).toHaveAttribute( + 'aria-selected', + 'true' + ) + expect(tabs.tabsMap['query-query-1']).toBeDefined() + } + ) +}) diff --git a/apps/studio/components/layouts/ExplorerLayout/ExplorerLayout.tsx b/apps/studio/components/layouts/ExplorerLayout/ExplorerLayout.tsx index da87c153c7f..f73170c15a1 100644 --- a/apps/studio/components/layouts/ExplorerLayout/ExplorerLayout.tsx +++ b/apps/studio/components/layouts/ExplorerLayout/ExplorerLayout.tsx @@ -28,6 +28,7 @@ import { useCreateNotebook, useCreateQuery, } from '@/components/interfaces/Explorer/hooks' +import { useDashboardHistory } from '@/hooks/misc/useDashboardHistory' import { useIsTemporarySqlEditorVisit } from '@/hooks/misc/useIsTemporarySqlEditorVisit' import { useTrack } from '@/lib/telemetry/track' import { @@ -46,6 +47,7 @@ export interface ExplorerLayoutProps extends ComponentProps { const { ref } = useParams() const tabs = useTabsStateSnapshot() + const { setLastVisitedExplorerTab } = useDashboardHistory() const { home, hasCompletedOnboarding, isReady } = useExplorerPreferences() const shouldShowHomeTab = isReady && (!hasCompletedOnboarding || home === 'home') @@ -68,6 +70,10 @@ export const ExplorerLayout = ({ browserTitle, children, title }: ExplorerLayout entity: browserTitle?.entity ?? activeTabLabel, } + const handleTabChange = (id: string) => { + if (id === EXPLORER_HOME_TAB_ID) setLastVisitedExplorerTab(undefined) + } + return ( : undefined} newTabButton={} + onTabChange={handleTabChange} />
{children}
diff --git a/apps/studio/components/layouts/Tabs/Tabs.tsx b/apps/studio/components/layouts/Tabs/Tabs.tsx index 05022bc102c..36839849c4c 100644 --- a/apps/studio/components/layouts/Tabs/Tabs.tsx +++ b/apps/studio/components/layouts/Tabs/Tabs.tsx @@ -41,6 +41,7 @@ interface EditorTabsProps { customTabs?: ReactNode newTabButton?: ReactNode isCollapseButtonHidden?: boolean + onTabChange?: (id: string) => void } // [Joshen] Will be adjusting this component to support Explorer @@ -49,6 +50,7 @@ export const EditorTabs = ({ customTabs, newTabButton, isCollapseButtonHidden, + onTabChange, }: EditorTabsProps) => { const { ref } = useParams() const router = useRouter() @@ -168,6 +170,7 @@ export const EditorTabs = ({ } const handleTabChange = (id: string) => { + onTabChange?.(id) tabs.handleTabNavigation(id, router) }