mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 10:55:06 +03:00
### 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 -->
155 lines
5.6 KiB
TypeScript
155 lines
5.6 KiB
TypeScript
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 { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
|
|
|
|
import {
|
|
EXPLORER_SECTIONS,
|
|
ExplorerResourceType,
|
|
LEVEL_OFFSET,
|
|
LEVEL_TRANSITION,
|
|
rowClassName,
|
|
} from './ExplorerLayout.constants'
|
|
import { formatRelativeTimeShort, getRecentlyUpdatedItems } from './ExplorerNavHome.utils'
|
|
import { ExplorerNavItem } from './ExplorerNavItem'
|
|
import { useExplorerDeleteItem } from './ExplorerProvider'
|
|
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 = ({
|
|
onSelectSection,
|
|
}: {
|
|
onSelectSection: (section: ExplorerResourceType) => void
|
|
}) => {
|
|
const router = useRouter()
|
|
const { id, ref } = useParams()
|
|
const tabs = useTabsStateSnapshot()
|
|
const appStateSnapshot = useAppStateSnapshot()
|
|
|
|
const { createQuery } = useCreateQuery()
|
|
const { onSelectDelete } = useExplorerDeleteItem()
|
|
|
|
const { data: notebooksData } = useNotebooksInfiniteQuery({ projectRef: ref, limit: 100 })
|
|
const notebooks = notebooksData?.pages.flatMap((page) => page.content) ?? []
|
|
const chats = useAiAssistantChatList()
|
|
|
|
const { data: notebookCountData, isPending } = useContentCountQuery({
|
|
projectRef: ref,
|
|
type: 'notebook',
|
|
})
|
|
// [Joshen] Notebooks are all shared by default, none private
|
|
const notebookCount = notebookCountData?.shared ?? 0
|
|
|
|
const recentItems = getRecentlyUpdatedItems({ notebooks, chats })
|
|
|
|
return (
|
|
<motion.div
|
|
key="root"
|
|
role="group"
|
|
aria-label="All resources"
|
|
initial={{ opacity: 0, x: -LEVEL_OFFSET }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
exit={{ opacity: 0, x: -LEVEL_OFFSET }}
|
|
transition={LEVEL_TRANSITION}
|
|
className="absolute inset-0 flex flex-col overflow-hidden"
|
|
>
|
|
<div className="flex-1 min-h-0 flex flex-col gap-4 overflow-y-auto p-3">
|
|
<nav className="flex flex-col gap-px">
|
|
<button
|
|
type="button"
|
|
tabIndex={0}
|
|
className={rowClassName(false)}
|
|
onClick={() => createQuery()}
|
|
>
|
|
<SquareCode size={14} className="shrink-0" />
|
|
<span className="flex-1 text-left">Run SQL</span>
|
|
<Plus size={14} className="shrink-0 text-foreground-muted" />
|
|
</button>
|
|
{EXPLORER_SECTIONS.map(({ type, label, icon: Icon }) => {
|
|
return (
|
|
<button
|
|
key={type}
|
|
type="button"
|
|
tabIndex={0}
|
|
className={rowClassName(false)}
|
|
onClick={() => onSelectSection(type)}
|
|
>
|
|
<Icon size={14} className="shrink-0" />
|
|
<span className="flex-1 text-left">{label}</span>
|
|
{type === 'notebook' ? (
|
|
isPending ? (
|
|
<ShimmeringLoader className="w-3 py-2" />
|
|
) : (
|
|
<span className="text-xs text-foreground-lighter">{notebookCount}</span>
|
|
)
|
|
) : (
|
|
<span className="text-xs text-foreground-lighter">{chats.length}</span>
|
|
)}
|
|
<ChevronRight size={14} className="shrink-0 text-foreground-muted" />
|
|
</button>
|
|
)
|
|
})}
|
|
</nav>
|
|
|
|
<section className="flex flex-col gap-px">
|
|
<h3 className="mb-2 px-2 font-mono text-sm font-normal uppercase text-foreground-lighter">
|
|
Recently updated
|
|
</h3>
|
|
{recentItems.length === 0 ? (
|
|
<p className="px-2 text-xs text-foreground-lighter">Nothing edited yet</p>
|
|
) : (
|
|
recentItems.map((item) => {
|
|
const isActive = id === item.id
|
|
|
|
const href =
|
|
item.type === 'chat'
|
|
? `/project/${ref}/explorer/chat/${item.id}`
|
|
: `/project/${ref}/explorer/notebook/${item.id}`
|
|
|
|
const onDoubleClick = () => {
|
|
if (item.type === 'chat') {
|
|
tabs.makeTabPermanent(createTabId('chat', { id: item.id }))
|
|
} else {
|
|
tabs.makeTabPermanent(createTabId('notebook', { id: item.id }))
|
|
}
|
|
}
|
|
|
|
return (
|
|
<ExplorerNavItem
|
|
key={item.id}
|
|
type={item.type}
|
|
href={href}
|
|
name={item.label}
|
|
isActive={isActive}
|
|
onDoubleClick={onDoubleClick}
|
|
onSelectDelete={() =>
|
|
onSelectDelete({ id: item.id, type: item.type, name: item.label })
|
|
}
|
|
description={formatRelativeTimeShort(item.updatedAt)}
|
|
/>
|
|
)
|
|
})
|
|
)}
|
|
</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>
|
|
</motion.div>
|
|
)
|
|
}
|