mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
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. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## 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. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
a31ca2bad0
commit
509c797be1
3 files changed
+113
No files matched your search
@@ -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(
|
||||
<TabsStateContext.Provider value={tabs}>
|
||||
<ExplorerLayout>Explorer content</ExplorerLayout>
|
||||
</TabsStateContext.Provider>,
|
||||
{ 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()
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -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<typeof ProjectLayout
|
||||
export const ExplorerLayout = ({ browserTitle, children, title }: ExplorerLayoutProps) => {
|
||||
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 (
|
||||
<ProjectLayoutWithAuth
|
||||
product="Explorer"
|
||||
@@ -99,6 +105,7 @@ export const ExplorerLayout = ({ browserTitle, children, title }: ExplorerLayout
|
||||
isCollapseButtonHidden
|
||||
customTabs={shouldShowHomeTab ? <HomeTabButton /> : undefined}
|
||||
newTabButton={<NewTabButton />}
|
||||
onTabChange={handleTabChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-grow min-h-0">{children}</div>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user