Files
supabase/apps/studio/components/layouts/ExplorerLayout/ExplorerNavNotebooks.tsx
T
Saxon FletcherandJoshen Lim d9742d707f chore(studio): refine Explorer sidebar breadcrumbs (#50188)
Moves Explorer’s back navigation and create actions into a reusable
sidebar breadcrumb header. Reduces product-menu headings globally to
`text-sm` and keeps breadcrumb links free of padding, borders, and
backgrounds.

### How to test

1. Open `/project/<ref>/explorer` and confirm the header shows Explorer
and the SQL Editor switch action.
2. Open Notebooks and Chats. Confirm the header shows `Explorer >
Notebooks/Chats` and the corresponding create action works.
3. Return using the Explorer breadcrumb with a click or Tab + Enter.
Check that the label stays aligned and has no hover background.
4. Open another product, such as Database, and confirm its sidebar
heading uses the smaller font size.

5. At a mobile viewport, open the menu and repeat the notebook/chat
actions and back navigation without closing the sheet. Confirm the
header stays current and disappears when returning to the main menu or
opening another product.

Validation: 18 focused tests, typecheck, formatting, and lint ratchet
passed.


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

- **New Features**
- Added a shared Explorer sidebar header with breadcrumbs and contextual
actions for creating notebooks and chats.
- Added keyboard-accessible navigation between the Explorer overview and
notebook or chat sections.
- Added support for customized product menu headers across project
layouts.

- **Improvements**
- Centralized Explorer navigation and actions in the shared sidebar
layout.
- Improved mobile menu updates when navigating between Explorer
resources.
- Refined Explorer home layout and drag-handle behavior across screen
sizes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2026-09-10 15:38:19 +08:00

104 lines
3.1 KiB
TypeScript

import { useDebounce } from '@uidotdev/usehooks'
import { useParams } from 'common'
import { NotebookText } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useMemo, useState } from 'react'
import { cn } from 'ui'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { ExplorerNavResourceWrapper, rowClassName } from './ExplorerLayout.constants'
import {
InfiniteListDefault,
LoaderForIconMenuItems,
type RowComponentBaseProps,
} from '@/components/ui/InfiniteList'
import {
NotebookRow,
useNotebooksInfiniteQuery,
} from '@/data/content/notebooks/notebooks-infinite-query'
const NOTEBOOK_ROW_HEIGHT = 28
type NotebookListItemProps = RowComponentBaseProps<NotebookRow> & {
projectRef: string | undefined
activeNotebookId: string | undefined
}
const NotebookListItem = ({
item: notebook,
style,
projectRef,
activeNotebookId,
}: NotebookListItemProps) => {
const isActive = activeNotebookId === notebook.id
return (
<Link
href={`/project/${projectRef}/explorer/notebook/${notebook.id}`}
className={rowClassName(isActive)}
style={style}
>
<NotebookText size={14} className={cn('shrink-0', isActive && 'text-foreground')} />
<span className="truncate text-left">{notebook.name}</span>
</Link>
)
}
export const ExplorerNavNotebooks = () => {
const router = useRouter()
const { ref, id } = useParams()
const [search, setSearch] = useState('')
const debouncedSearch = useDebounce(search, 500)
const {
data: notebooksData,
isPending,
hasNextPage,
isFetchingNextPage,
fetchNextPage,
} = useNotebooksInfiniteQuery({
projectRef: ref,
limit: 100,
name: search.length === 0 ? search : debouncedSearch,
})
const notebooks = useMemo(() => {
const items = notebooksData?.pages.flatMap((page) => page.content) ?? []
return items
}, [notebooksData?.pages])
const activeNotebookId = router.pathname.includes('/explorer/notebook/') ? id : undefined
const itemProps = useMemo(() => ({ projectRef: ref, activeNotebookId }), [ref, activeNotebookId])
return (
<ExplorerNavResourceWrapper type="notebook" search={search} setSearch={setSearch}>
<div className="flex flex-1 min-h-0 flex-col px-3 pb-3">
{isPending ? (
<GenericSkeletonLoader />
) : notebooks.length === 0 ? (
<p className="px-2 py-2 text-xs text-foreground-lighter">
{search ? 'No notebooks found' : 'No notebooks created yet'}
</p>
) : (
<InfiniteListDefault
className="h-full w-full"
items={notebooks}
itemProps={itemProps}
ItemComponent={NotebookListItem}
LoaderComponent={LoaderForIconMenuItems}
getItemKey={(index) => notebooks[index]?.id ?? `notebook-${index}`}
getItemSize={() => NOTEBOOK_ROW_HEIGHT}
gap={1}
hasNextPage={hasNextPage}
isLoadingNextPage={isFetchingNextPage}
onLoadNextPage={fetchNextPage}
/>
)}
</div>
</ExplorerNavResourceWrapper>
)
}