diff --git a/apps/studio/components/interfaces/Sidebar.tsx b/apps/studio/components/interfaces/Sidebar.tsx index 17c6f422e92..ec7b8db062b 100644 --- a/apps/studio/components/interfaces/Sidebar.tsx +++ b/apps/studio/components/interfaces/Sidebar.tsx @@ -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 + } /> ))} diff --git a/apps/studio/components/layouts/EditorNavigationButton.tsx b/apps/studio/components/layouts/EditorNavigationButton.tsx deleted file mode 100644 index fa9464b7124..00000000000 --- a/apps/studio/components/layouts/EditorNavigationButton.tsx +++ /dev/null @@ -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, 'tooltip'>) => ( - } - tooltip={{ content: { side: 'bottom', text: tooltip } }} - {...props} - /> -) diff --git a/apps/studio/components/layouts/ExplorerLayout/ExplorerLayout.tsx b/apps/studio/components/layouts/ExplorerLayout/ExplorerLayout.tsx index f9e218cc8df..970c86cc7b1 100644 --- a/apps/studio/components/layouts/ExplorerLayout/ExplorerLayout.tsx +++ b/apps/studio/components/layouts/ExplorerLayout/ExplorerLayout.tsx @@ -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 { - 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() - 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 setSection(undefined)} - rootAction={} + rootAction={} /> } 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 ( - - { - setIsTemporary(true) - track('explorer_temp_access_sql_editor_clicked') - }} - /> - - ) -} - const TabClassName = 'flex items-center justify-center min-w-(--header-height) min-h-(--header-height) hover:bg-surface-100 shrink-0' diff --git a/apps/studio/components/layouts/ExplorerLayout/ExplorerNavHeader.test.tsx b/apps/studio/components/layouts/ExplorerLayout/ExplorerNavHeader.test.tsx index 6ef1b974d6a..2f64ebd68c0 100644 --- a/apps/studio/components/layouts/ExplorerLayout/ExplorerNavHeader.test.tsx +++ b/apps/studio/components/layouts/ExplorerLayout/ExplorerNavHeader.test.tsx @@ -23,7 +23,7 @@ function renderHeader(section?: ExplorerResourceType) { Switch to SQL Editor} + rootAction={Explorer preferences} /> } /> @@ -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 })) diff --git a/apps/studio/components/layouts/ExplorerLayout/ExplorerNavHome.tsx b/apps/studio/components/layouts/ExplorerLayout/ExplorerNavHome.tsx index 7937a35f1de..3e42cbcda54 100644 --- a/apps/studio/components/layouts/ExplorerLayout/ExplorerNavHome.tsx +++ b/apps/studio/components/layouts/ExplorerLayout/ExplorerNavHome.tsx @@ -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 = ({ -
- appStateSnapshot.setLastRouteBeforeVisitingAccountPage(router.asPath)} - > - - Preferences - -
+ ) } diff --git a/apps/studio/components/layouts/ExplorerLayout/ExplorerPreferencesDropdown.test.tsx b/apps/studio/components/layouts/ExplorerLayout/ExplorerPreferencesDropdown.test.tsx new file mode 100644 index 00000000000..afc72b26c07 --- /dev/null +++ b/apps/studio/components/layouts/ExplorerLayout/ExplorerPreferencesDropdown.test.tsx @@ -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() + + 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' + ) + }) +}) diff --git a/apps/studio/components/layouts/ExplorerLayout/ExplorerPreferencesDropdown.tsx b/apps/studio/components/layouts/ExplorerLayout/ExplorerPreferencesDropdown.tsx new file mode 100644 index 00000000000..cd667d354da --- /dev/null +++ b/apps/studio/components/layouts/ExplorerLayout/ExplorerPreferencesDropdown.tsx @@ -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 ( + + + } + tooltip={{ content: { side: 'bottom', text: 'Explorer preferences' } }} + /> + + + Open Explorer to + { + const result = explorerHomeSchema.safeParse(value) + if (result.success) setHome(result.data) + }} + > + + Start page + + + SQL query + + + + + ) +} diff --git a/apps/studio/components/layouts/ExplorerLayout/ExplorerSqlEditorFooter.tsx b/apps/studio/components/layouts/ExplorerLayout/ExplorerSqlEditorFooter.tsx new file mode 100644 index 00000000000..b9a6d67988a --- /dev/null +++ b/apps/studio/components/layouts/ExplorerLayout/ExplorerSqlEditorFooter.tsx @@ -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 ( +
+
+ +

+ Your snippets can be accessed via the SQL Editor while Explorer is in preview. +

+
+ +
+ ) +} diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/MobileMenuContent/MobileMenuContent.test.tsx b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/MobileMenuContent/MobileMenuContent.test.tsx index dfa9c030c41..5d1ce2953fb 100644 --- a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/MobileMenuContent/MobileMenuContent.test.tsx +++ b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/MobileMenuContent/MobileMenuContent.test.tsx @@ -45,7 +45,7 @@ const ExplorerMobileMenu = ({ initialSection }: { initialSection: ExplorerResour setSection(undefined)} - rootAction={Switch to SQL Editor} + rootAction={Explorer preferences} /> } /> @@ -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() } ) diff --git a/apps/studio/components/layouts/editors/EditorBaseLayout.tsx b/apps/studio/components/layouts/editors/EditorBaseLayout.tsx index 7f61a8a5b9f..bf1722d1c78 100644 --- a/apps/studio/components/layouts/editors/EditorBaseLayout.tsx +++ b/apps/studio/components/layouts/editors/EditorBaseLayout.tsx @@ -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 { @@ -65,7 +62,6 @@ export const EditorBaseLayout = ({ resizableSidebar product={product} browserTitle={mergedBrowserTitle} - productMenuBadge={editor === 'sql' ? : undefined} productMenuClassName={productMenuClassName} productMenu={productMenu} > @@ -83,23 +79,3 @@ export const EditorBaseLayout = ({ ) } - -const BackToExplorerButton = () => { - const { ref } = useParams() - const router = useRouter() - const track = useTrack() - const { isTemporary, setIsTemporary } = useIsTemporarySqlEditorVisit(ref) - - if (!ref || !isTemporary) return null - - return ( - { - setIsTemporary(false) - track('sql_editor_back_explorer_clicked') - router.push(`/project/${ref}/explorer`) - }} - /> - ) -} diff --git a/apps/studio/hooks/misc/useIsTemporarySqlEditorVisit.ts b/apps/studio/hooks/misc/useIsTemporarySqlEditorVisit.ts deleted file mode 100644 index 309c78f85fa..00000000000 --- a/apps/studio/hooks/misc/useIsTemporarySqlEditorVisit.ts +++ /dev/null @@ -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 } -} diff --git a/packages/common/constants/local-storage.ts b/packages/common/constants/local-storage.ts index 622d0f96671..08c2acb6dd6 100644 --- a/packages/common/constants/local-storage.ts +++ b/packages/common/constants/local-storage.ts @@ -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}`, diff --git a/packages/common/telemetry-constants.ts b/packages/common/telemetry-constants.ts index 2523a72ad17..dbddceff257 100644 --- a/packages/common/telemetry-constants.ts +++ b/packages/common/telemetry-constants.ts @@ -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