chore(studio): move Explorer SQL Editor link to sidebar footer (#50829)

## What

- Moves the temporary "Switch to SQL Editor" button out of the Explorer
sidebar header into a footer section ("Looking for snippets?") with a
short explanation and an **Open SQL Editor** button.
- Replaces the header slot with a menu for the Explorer startup
preference (**Start page** / **SQL query**), instead of linking out to
account preferences.

## How to test

1. Enable the Explorer feature preview and open
`/project/<ref>/explorer`.
2. **Header menu:** click the ⋮ button next to the Explorer title. Pick
**SQL query**, then check that **Explorer startup** on `/account/me`
shows the same value (and vice versa).
3. **Footer:** click **Open SQL Editor**. You should land in the SQL
Editor with the **Back to Explorer** button in its title bar.
4. Open **Notebooks** or **Chats** in the sidebar and check that the
menu and footer are hidden there, like the old button was.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Choose whether the Explorer opens to the Start page or SQL query from
the Explorer preferences menu. Your selection is saved and retained when
you reopen the menu.
  * Access the SQL Editor from the Explorer’s sidebar footer.
* Explorer preferences are available from the Explorer navigation
header.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Tyler <dshukertjr@gmail.com>
Co-authored-by: Nik Richers <nrichers@gmail.com>
Co-authored-by: Danny White <3104761+dnywh@users.noreply.github.com>
Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
Co-authored-by: Jordi Enric <37541088+jordienr@users.noreply.github.com>
Co-authored-by: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Co-authored-by: Katerina Skroumpelou <mandarini@users.noreply.github.com>
Co-authored-by: Franek <franek@ferly.co.uk>
Co-authored-by: Franek Richardson <franek@supabase.io>
Co-authored-by: Michał Olszewski <35968924+charconstpointer@users.noreply.github.com>
Co-authored-by: Steven Eubank <47563310+smeubank@users.noreply.github.com>
Co-authored-by: Anthony Lio <lionnet.ant@gmail.com>
Co-authored-by: Joey Lei <6957385+leizerbeam@users.noreply.github.com>
Co-authored-by: Ali Waseem <waseema393@gmail.com>
Co-authored-by: Samir Ketema <6003000+samirketema@users.noreply.github.com>
Co-authored-by: K-Dog (Kevin) <k.grueneberg1994@gmail.com>
This commit is contained in:
13 files changed
+149 -121

No files matched your search

@@ -43,6 +43,7 @@ import { useLints } from '@/hooks/misc/useLints'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { useTrack } from '@/lib/telemetry/track'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
export const ICON_SIZE = 32
@@ -259,6 +260,7 @@ const ActiveDot = ({ hasErrors, hasWarnings }: { hasErrors: boolean; hasWarnings
const ProjectLinks = () => {
const router = useRouter()
const track = useTrack()
const { ref } = useParams()
const { data: project, isPending: isProjectPending } = useSelectedProjectQuery()
const { securityLints, errorLints } = useLints()
@@ -314,6 +316,11 @@ const ProjectLinks = () => {
route={route}
active={activeRoute === route.key}
isLoading={isProjectPending}
onClick={
route.key === 'explorer' && activeRoute === 'sql'
? () => track('sql_editor_back_explorer_clicked')
: undefined
}
/>
))}
</SidebarMenu>
@@ -1,18 +0,0 @@
import { SqlEditor } from 'icons'
import { ComponentProps } from 'react'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
export const EditorNavigationButton = ({
tooltip,
...props
}: { tooltip: string } & Omit<ComponentProps<typeof ButtonTooltip>, 'tooltip'>) => (
<ButtonTooltip
size="tiny"
variant="outline"
className="size-7 shrink-0 px-0"
icon={<SqlEditor size={14} strokeWidth={1.5} />}
tooltip={{ content: { side: 'bottom', text: tooltip } }}
{...props}
/>
)
@@ -1,7 +1,5 @@
import { useParams } from 'common'
import { AnimatePresence, motion } from 'framer-motion'
import { Home, MessageCirclePlus, NotebookText, Plus, SquareCode } from 'lucide-react'
import Link from 'next/link'
import { ComponentProps, ReactNode, useEffect, useEffectEvent, useState } from 'react'
import {
cn,
@@ -12,7 +10,6 @@ import {
TabsTrigger,
} from 'ui'
import { EditorNavigationButton } from '../EditorNavigationButton'
import { ProjectLayoutWithAuth } from '../ProjectLayout'
import { EditorTabs } from '../Tabs/Tabs'
import { type ExplorerResourceType } from './ExplorerLayout.constants'
@@ -20,6 +17,7 @@ import { ExplorerNavChats } from './ExplorerNavChats'
import { ExplorerNavHeader } from './ExplorerNavHeader'
import { ExplorerNavHome } from './ExplorerNavHome'
import { ExplorerNavNotebooks } from './ExplorerNavNotebooks'
import { ExplorerPreferencesDropdown } from './ExplorerPreferencesDropdown'
import { ExplorerProvider } from './ExplorerProvider'
import { useExplorerPreferences } from '@/components/interfaces/Account/Preferences/useExplorerPreferences'
import { ExplorerNotebookTabCoordinator } from '@/components/interfaces/Explorer/ExplorerNotebookTabCoordinator'
@@ -30,8 +28,6 @@ import {
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 {
editorEntityTypes,
EXPLORER_HOME_TAB,
@@ -46,7 +42,6 @@ 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()
@@ -54,12 +49,6 @@ export const ExplorerLayout = ({ browserTitle, children, title }: ExplorerLayout
const [section, setSection] = useState<ExplorerResourceType>()
const { setIsTemporary: setIsTemporarySqlEditorVisit } = useIsTemporarySqlEditorVisit(ref)
useEffect(() => {
if (ref) setIsTemporarySqlEditorVisit(false)
}, [ref, setIsTemporarySqlEditorVisit])
const activeTab = tabs.activeTab ? tabs.tabsMap[tabs.activeTab] : undefined
const isActiveExplorerTab =
activeTab !== undefined && editorEntityTypes.explorer.includes(activeTab.type)
@@ -84,7 +73,7 @@ export const ExplorerLayout = ({ browserTitle, children, title }: ExplorerLayout
<ExplorerNavHeader
section={section}
onBack={() => setSection(undefined)}
rootAction={<BackToSqlEditorButton />}
rootAction={<ExplorerPreferencesDropdown />}
/>
}
productMenu={
@@ -117,30 +106,6 @@ export const ExplorerLayout = ({ browserTitle, children, title }: ExplorerLayout
)
}
const BackToSqlEditorButton = () => {
const { ref } = useParams()
const track = useTrack()
const { setIsTemporary } = useIsTemporarySqlEditorVisit(ref)
if (!ref) return null
return (
<EditorNavigationButton
asChild
tooltip="Temporarily switch to SQL Editor to access your snippets"
>
<Link
href={`/project/${ref}/sql`}
aria-label="Switch to SQL Editor"
onClick={() => {
setIsTemporary(true)
track('explorer_temp_access_sql_editor_clicked')
}}
/>
</EditorNavigationButton>
)
}
const TabClassName =
'flex items-center justify-center min-w-(--header-height) min-h-(--header-height) hover:bg-surface-100 shrink-0'
@@ -23,7 +23,7 @@ function renderHeader(section?: ExplorerResourceType) {
<ExplorerNavHeader
section={section}
onBack={onBack}
rootAction={<span>Switch to SQL Editor</span>}
rootAction={<span>Explorer preferences</span>}
/>
}
/>
@@ -34,11 +34,11 @@ function renderHeader(section?: ExplorerResourceType) {
describe('Explorer sidebar header', () => {
beforeEach(() => vi.clearAllMocks())
it('shows the smaller root title with the SQL action', () => {
it('shows the smaller root title with the preferences action', () => {
renderHeader()
expect(screen.getAllByText('Explorer')).toHaveLength(1)
expect(screen.getByText('Explorer')).toHaveClass('text-sm')
expect(screen.getByText('Switch to SQL Editor')).toBeInTheDocument()
expect(screen.getByText('Explorer preferences')).toBeInTheDocument()
expect(screen.queryByRole('button')).not.toBeInTheDocument()
})
@@ -56,7 +56,7 @@ describe('Explorer sidebar header', () => {
'page'
)
expect(within(breadcrumb).getAllByRole('listitem')).toHaveLength(2)
expect(screen.queryByText('Switch to SQL Editor')).not.toBeInTheDocument()
expect(screen.queryByText('Explorer preferences')).not.toBeInTheDocument()
expect(screen.queryByRole('button', { name: 'Back' })).not.toBeInTheDocument()
const user = userEvent.setup()
await user.click(screen.getByRole('button', { name: parent }))
@@ -1,8 +1,6 @@
import { useParams } from 'common'
import { motion } from 'framer-motion'
import { ChevronRight, Plus, Settings, SquareCode } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { ChevronRight, Plus, SquareCode } from 'lucide-react'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import {
@@ -15,11 +13,11 @@ import {
import { formatRelativeTimeShort, getRecentlyUpdatedItems } from './ExplorerNavHome.utils'
import { ExplorerNavItem } from './ExplorerNavItem'
import { useExplorerDeleteItem } from './ExplorerProvider'
import { ExplorerSqlEditorFooter } from './ExplorerSqlEditorFooter'
import { useCreateQuery } from '@/components/interfaces/Explorer/hooks'
import { useContentCountQuery } from '@/data/content/content-count-query'
import { useNotebooksInfiniteQuery } from '@/data/content/notebooks/notebooks-infinite-query'
import { useAiAssistantChatList } from '@/state/ai-assistant-state'
import { useAppStateSnapshot } from '@/state/app-state'
import { createTabId, useTabsStateSnapshot } from '@/state/tabs'
export const ExplorerNavHome = ({
@@ -27,10 +25,8 @@ export const ExplorerNavHome = ({
}: {
onSelectSection: (section: ExplorerResourceType) => void
}) => {
const router = useRouter()
const { id, ref } = useParams()
const tabs = useTabsStateSnapshot()
const appStateSnapshot = useAppStateSnapshot()
const { createQuery } = useCreateQuery()
const { onSelectDelete } = useExplorerDeleteItem()
@@ -139,16 +135,7 @@ export const ExplorerNavHome = ({
</section>
</div>
<div className="shrink-0 border-t border-default p-3">
<Link
href="/account/me#dashboard"
className={rowClassName(false)}
onClick={() => appStateSnapshot.setLastRouteBeforeVisitingAccountPage(router.asPath)}
>
<Settings size={14} className="shrink-0" />
<span className="flex-1 text-left">Preferences</span>
</Link>
</div>
<ExplorerSqlEditorFooter />
</motion.div>
)
}
@@ -0,0 +1,38 @@
import { screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { LOCAL_STORAGE_KEYS } from 'common'
import { afterEach, describe, expect, it } from 'vitest'
import { ExplorerPreferencesDropdown } from './ExplorerPreferencesDropdown'
import { customRender } from '@/tests/lib/custom-render'
afterEach(() => localStorage.clear())
describe('ExplorerPreferencesDropdown', () => {
it('shows the Explorer startup options and saves the selected one', async () => {
const user = userEvent.setup()
customRender(<ExplorerPreferencesDropdown />)
await user.click(screen.getByRole('button', { name: 'Explorer preferences' }))
expect(screen.getByText('Open Explorer to')).toBeInTheDocument()
await waitFor(() =>
expect(screen.getByRole('menuitemradio', { name: 'Start page' })).toHaveAttribute(
'aria-checked',
'true'
)
)
await user.click(screen.getByRole('menuitemradio', { name: 'SQL query' }))
await waitFor(() =>
expect(
JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEYS.EXPLORER_PREFERENCES) ?? '{}')
).toMatchObject({ 'self-hosted': { home: 'query' } })
)
await user.click(screen.getByRole('button', { name: 'Explorer preferences' }))
expect(screen.getByRole('menuitemradio', { name: 'SQL query' })).toHaveAttribute(
'aria-checked',
'true'
)
})
})
@@ -0,0 +1,51 @@
import { MoreVertical } from 'lucide-react'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from 'ui'
import {
explorerHomeSchema,
useExplorerPreferences,
} from '@/components/interfaces/Account/Preferences/useExplorerPreferences'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
export const ExplorerPreferencesDropdown = () => {
const { home, setHome, isReady } = useExplorerPreferences()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<ButtonTooltip
size="tiny"
variant="outline"
aria-label="Explorer preferences"
className="size-7 shrink-0 px-0"
icon={<MoreVertical size={14} strokeWidth={1.5} />}
tooltip={{ content: { side: 'bottom', text: 'Explorer preferences' } }}
/>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-44" align="end">
<DropdownMenuLabel>Open Explorer to</DropdownMenuLabel>
<DropdownMenuRadioGroup
value={home}
onValueChange={(value) => {
const result = explorerHomeSchema.safeParse(value)
if (result.success) setHome(result.data)
}}
>
<DropdownMenuRadioItem value="home" disabled={!isReady}>
Start page
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="query" disabled={!isReady}>
SQL query
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
)
}
@@ -0,0 +1,37 @@
import { useParams } from 'common'
import { SqlEditor } from 'icons'
import Link from 'next/link'
import { Button } from 'ui'
import { useTrack } from '@/lib/telemetry/track'
export const ExplorerSqlEditorFooter = () => {
const { ref } = useParams()
const track = useTrack()
if (!ref) return null
return (
<section
aria-labelledby="explorer-sql-editor-footer-title"
className="flex shrink-0 flex-col gap-3 border-t border-default p-3"
>
<div className="flex flex-col gap-1">
<h3 id="explorer-sql-editor-footer-title" className="text-sm text-foreground">
Looking for snippets?
</h3>
<p className="text-xs text-foreground-lighter">
Your snippets can be accessed via the SQL Editor while Explorer is in preview.
</p>
</div>
<Button asChild block variant="outline" icon={<SqlEditor size={14} strokeWidth={1.5} />}>
<Link
href={`/project/${ref}/sql`}
onClick={() => track('explorer_temp_access_sql_editor_clicked')}
>
Open SQL Editor
</Link>
</Button>
</section>
)
}
@@ -45,7 +45,7 @@ const ExplorerMobileMenu = ({ initialSection }: { initialSection: ExplorerResour
<ExplorerNavHeader
section={section}
onBack={() => setSection(undefined)}
rootAction={<span>Switch to SQL Editor</span>}
rootAction={<span>Explorer preferences</span>}
/>
}
/>
@@ -95,7 +95,7 @@ describe('Mobile product header', () => {
screen.getByRole('button', { name: 'Explorer' }).focus()
await user.keyboard('{Enter}')
expect(screen.getByText('Home content')).toBeInTheDocument()
expect(screen.getByText('Switch to SQL Editor')).toBeInTheDocument()
expect(screen.getByText('Explorer preferences')).toBeInTheDocument()
expect(screen.queryByRole('button', { name: `New ${section}` })).not.toBeInTheDocument()
}
)
@@ -1,15 +1,12 @@
import { useParams } from 'common'
import { usePathname, useRouter } from 'next/navigation'
import { usePathname } from 'next/navigation'
import { ComponentProps, ReactNode } from 'react'
import { cn } from 'ui'
import { EditorNavigationButton } from '../EditorNavigationButton'
import { ProjectLayoutWithAuth } from '../ProjectLayout'
import { CollapseButton } from '../Tabs/CollapseButton'
import { EditorTabs } from '../Tabs/Tabs'
import { useEditorType } from './EditorsLayout.hooks'
import { useIsTemporarySqlEditorVisit } from '@/hooks/misc/useIsTemporarySqlEditorVisit'
import { useTrack } from '@/lib/telemetry/track'
import { useTabsStateSnapshot } from '@/state/tabs'
export interface ExplorerLayoutProps extends ComponentProps<typeof ProjectLayoutWithAuth> {
@@ -65,7 +62,6 @@ export const EditorBaseLayout = ({
resizableSidebar
product={product}
browserTitle={mergedBrowserTitle}
productMenuBadge={editor === 'sql' ? <BackToExplorerButton /> : undefined}
productMenuClassName={productMenuClassName}
productMenu={productMenu}
>
@@ -83,23 +79,3 @@ export const EditorBaseLayout = ({
</ProjectLayoutWithAuth>
)
}
const BackToExplorerButton = () => {
const { ref } = useParams()
const router = useRouter()
const track = useTrack()
const { isTemporary, setIsTemporary } = useIsTemporarySqlEditorVisit(ref)
if (!ref || !isTemporary) return null
return (
<EditorNavigationButton
tooltip="Back to Explorer"
onClick={() => {
setIsTemporary(false)
track('sql_editor_back_explorer_clicked')
router.push(`/project/${ref}/explorer`)
}}
/>
)
}
@@ -1,12 +0,0 @@
import { LOCAL_STORAGE_KEYS } from 'common'
import { useLocalStorageQuery } from './useLocalStorage'
export const useIsTemporarySqlEditorVisit = (ref: string | undefined) => {
const [isTemporary, setIsTemporary] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.SQL_EDITOR_TEMPORARY_FROM_EXPLORER(ref ?? ''),
false
)
return { isTemporary, setIsTemporary }
}
@@ -54,9 +54,6 @@ export const LOCAL_STORAGE_KEYS = {
SQL_EDITOR_SECTION_STATE: (ref: string) => `sql-editor-section-state-${ref}`,
SQL_EDITOR_SORT: (ref: string) => `sql-editor-sort-${ref}`,
SQL_EDITOR_MANUAL_SAVE_NOTICE_DISMISSED: 'sql-editor-manual-save-notice-dismissed',
// Set when a user follows the "temporarily switch to SQL Editor" link from Explorer;
// shows a way back and is cleared once they return to Explorer
SQL_EDITOR_TEMPORARY_FROM_EXPLORER: (ref: string) => `sql-editor-temporary-from-explorer-${ref}`,
EXPLORER_QUERY_DRAFTS: (ref: string) => `explorer-query-drafts-${ref}`,
NOTEBOOK_DRAFTS: (ref: string) => `notebook-drafts-${ref}`,
+4 -4
View File
@@ -1611,8 +1611,8 @@ export interface ExplorerBannerCtaButtonClickedEvent {
}
/**
* User clicked the button in the Explorer sidebar title bar to temporarily switch to the SQL
* Editor for snippet access.
* User clicked the SQL Editor button in the Explorer sidebar footer to temporarily switch
* to the SQL Editor for snippet access.
*
* @group Events
* @source studio
@@ -1624,8 +1624,8 @@ export interface ExplorerTempAccessSqlEditorClickedEvent {
}
/**
* User clicked the "Back to Explorer" button in the SQL Editor title bar, shown only when the
* visit originated from the Explorer's temporary switch button.
* User clicked the Explorer sidebar nav item while on the SQL Editor page, navigating back
* to Explorer.
*
* @group Events
* @source studio