Files
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

97 lines
3.2 KiB
TypeScript

import { useParams } from 'common'
import { useRouter } from 'next/router'
import { createContext, useContext, useState, type PropsWithChildren } from 'react'
import { toast } from 'sonner'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { useContentDeleteMutation } from '@/data/content/content-delete-mutation'
import { useDashboardHistory } from '@/hooks/misc/useDashboardHistory'
import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state'
import { useNotebooksStateSnapshot } from '@/state/notebooks/notebooks-state'
import { createTabId, useTabsStateSnapshot } from '@/state/tabs'
type ExplorerDeleteItem = { id: string; type: 'notebook' | 'chat'; name: string }
interface ExplorerContextValue {
onSelectDelete: (item: ExplorerDeleteItem) => void
}
const ExplorerContext = createContext<ExplorerContextValue | undefined>(undefined)
export const useExplorerDeleteItem = () => {
const context = useContext(ExplorerContext)
if (!context) {
throw new Error('useExplorerDeleteItem must be used within an ExplorerProvider')
}
return context
}
export const ExplorerProvider = ({ children }: PropsWithChildren) => {
const router = useRouter()
const { ref } = useParams()
const tabs = useTabsStateSnapshot()
const notebooksSnap = useNotebooksStateSnapshot()
const aiAssistantSnap = useAiAssistantStateSnapshot()
const { setLastVisitedExplorerTab } = useDashboardHistory()
const [pending, setPending] = useState<ExplorerDeleteItem | null>(null)
const onClearDashboardHistory = () => setLastVisitedExplorerTab(undefined)
const closeTabForItem = (item: ExplorerDeleteItem) => {
tabs.handleTabClose({
id: createTabId(item.type, { id: item.id }),
router,
editor: 'explorer',
onClearDashboardHistory,
})
}
const { mutate: deleteNotebook, isPending: isDeletingNotebook } = useContentDeleteMutation({
onSuccess: () => {
if (!pending) return
toast.success('Successfully deleted notebook')
notebooksSnap.removeNotebook({ id: pending.id })
closeTabForItem(pending)
setPending(null)
},
onError: (error) => toast.error(`Failed to delete notebook: ${error.message}`),
})
const handleConfirm = () => {
if (!pending) return
if (pending.type === 'notebook') {
if (!ref) return
deleteNotebook({ projectRef: ref, ids: [pending.id] })
} else {
aiAssistantSnap.deleteChat(pending.id)
toast.success(`Deleted "${pending.name}"`)
closeTabForItem(pending)
setPending(null)
}
}
return (
<ExplorerContext.Provider value={{ onSelectDelete: setPending }}>
{children}
<ConfirmationModal
size="small"
variant="destructive"
visible={!!pending}
title={`Confirm to delete ${pending?.type} '${pending?.name ?? ''}'`}
confirmLabel={`Delete ${pending?.type}`}
confirmLabelLoading="Deleting..."
loading={isDeletingNotebook}
onCancel={() => setPending(null)}
onConfirm={handleConfirm}
>
<p className="text-sm text-foreground-light">
This action cannot be undone. Are you sure you want to delete '{pending?.name}'?
</p>
</ConfirmationModal>
</ExplorerContext.Provider>
)
}