Files
supabase/apps/studio/components/layouts/ExplorerLayout/ExplorerNavNotebooks.tsx
Joshen Lim 14bda8a5cf Joshenlim/fe 4438 explorer allow deleting notebookchat without first having to (#50701)
### Context

Adds context menu to the explorer nav to allow users to delete notebooks
or chats from there (instead of having to open the notebook / chat
first, then select delete from the header)
<img width="304" height="339" alt="image"
src="https://github.com/user-attachments/assets/2ad0f21c-0d49-407c-97a2-ef11ae59e280"
/>

### Changes involved:
- Consolidates the rendering of explorer nav items into a single
component that both `ExplorerNavNotebook`, `ExplorerNavHome` and
`ExplorerNavChats` use.
- We were previously rendering a link for notebook, and a button for
chats. But chats' button was just calling `router.push` under the hood.
- Opting to use a Context to handle the delete functionality such that
we can render the confirmation modal just once at the layout level since
there's multiple places that have this delete functionality - notebook
tab, chat tab, context menu in each of the explorer navs

### To test
- [ ] Verify that navigating around the explorer is status quo
- [ ] Verify that you can delete a notebook/chat from within the
notebook/chat tab
- [ ] Verify that you can delete a notebook/chat from the explorer nav
- [ ] Verify that after deleting a notebook/chat from either locations,
the tabs should clear

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

- **New Features**
- Unified chat and notebook navigation with direct links and active-item
highlighting.
- Added context-menu options to delete chats and notebooks with
confirmation and status notifications.
- Added double-click support for keeping recently viewed chats and
notebooks open as tabs.
  - Deleted items and their related tabs are removed automatically.

- **Tests**
- Updated Explorer navigation tests for the unified link-based item
behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-22 16:21:55 +08:00

114 lines
3.5 KiB
TypeScript

import { useDebounce } from '@uidotdev/usehooks'
import { useParams } from 'common'
import { useRouter } from 'next/router'
import { useMemo, useState } from 'react'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { ExplorerNavResourceWrapper } from './ExplorerLayout.constants'
import { ExplorerNavItem } from './ExplorerNavItem'
import { useExplorerDeleteItem } from './ExplorerProvider'
import {
InfiniteListDefault,
LoaderForIconMenuItems,
type RowComponentBaseProps,
} from '@/components/ui/InfiniteList'
import {
NotebookRow,
useNotebooksInfiniteQuery,
} from '@/data/content/notebooks/notebooks-infinite-query'
import { createTabId, useTabsStateSnapshot } from '@/state/tabs'
const NOTEBOOK_ROW_HEIGHT = 28
type NotebookListItemProps = RowComponentBaseProps<NotebookRow> & {
projectRef: string | undefined
activeNotebookId: string | undefined
onSelectDelete: (item: { id: string; type: 'notebook' | 'chat'; name: string }) => void
}
const NotebookListItem = ({
item: notebook,
style,
projectRef,
activeNotebookId,
onSelectDelete,
}: NotebookListItemProps) => {
const isActive = activeNotebookId === notebook.id
const tabs = useTabsStateSnapshot()
return (
<ExplorerNavItem
name={notebook.name}
type="notebook"
isActive={isActive}
style={style}
href={`/project/${projectRef}/explorer/notebook/${notebook.id}`}
onDoubleClick={() => tabs.makeTabPermanent(createTabId('notebook', { id: notebook.id }))}
onSelectDelete={() =>
onSelectDelete({ id: notebook.id, type: 'notebook', name: notebook.name })
}
/>
)
}
export const ExplorerNavNotebooks = () => {
const router = useRouter()
const { ref, id } = useParams()
const { onSelectDelete } = useExplorerDeleteItem()
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, onSelectDelete }),
[ref, activeNotebookId, onSelectDelete]
)
return (
<ExplorerNavResourceWrapper type="notebook" search={search} setSearch={setSearch}>
<div className="flex flex-1 min-h-0 flex-col p-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>
)
}