Files
supabase/apps/studio/components/layouts/ExplorerLayout/ExplorerNavChats.test.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

78 lines
2.5 KiB
TypeScript

import { fireEvent, screen } from '@testing-library/react'
import type { PropsWithChildren } from 'react'
import { describe, expect, it, vi } from 'vitest'
import { ExplorerNavChats } from './ExplorerNavChats'
import { customRender } from '@/tests/lib/custom-render'
vi.mock('common', async (importOriginal) => {
const actual = await importOriginal<typeof import('common')>()
return { ...actual, useParams: () => ({ id: 'recent-chat' }) }
})
vi.mock('next/router', () => ({
useRouter: () => ({ pathname: '/project/[ref]/explorer/chat/[id]' }),
}))
vi.mock('./ExplorerLayout.constants', () => ({
ExplorerNavResourceWrapper: ({
children,
search,
setSearch,
}: PropsWithChildren<{ search: string; setSearch: (value: string) => void }>) => (
<div>
<input
aria-label="Search chats"
value={search}
onChange={(event) => setSearch(event.target.value)}
/>
{children}
</div>
),
rowClassName: (isActive: boolean) => (isActive ? 'active' : 'inactive'),
}))
vi.mock('@/components/interfaces/Explorer/hooks', () => ({
useCreateChat: () => ({ openChat: vi.fn() }),
}))
vi.mock('./ExplorerProvider', () => ({
useExplorerDeleteItem: () => ({ confirmDelete: vi.fn() }),
}))
vi.mock('@/state/ai-assistant-state', () => ({
useAiAssistantChatList: () => [
{ id: 'old-chat', name: 'Older investigation', updatedAt: new Date('2026-01-01') },
{ id: 'missing-date', name: 'Rehydrated chat' },
{ id: 'recent-chat', name: 'Recent investigation', updatedAt: new Date('2026-02-01') },
{
id: 'support',
name: 'Support conversation',
updatedAt: new Date('2026-03-01'),
supportMetadata: { isSupportChat: true },
},
],
}))
describe('ExplorerNavChats', () => {
it('filters support chats, safely sorts rehydrated chats, and marks the route active', () => {
customRender(<ExplorerNavChats />)
const chatLinks = screen.getAllByRole('link')
expect(chatLinks.map((link) => link.textContent)).toEqual([
'Recent investigation',
'Older investigation',
'Rehydrated chat',
])
expect(chatLinks[0]).toHaveClass('active')
expect(screen.queryByText('Support conversation')).not.toBeInTheDocument()
fireEvent.change(screen.getByRole('textbox', { name: 'Search chats' }), {
target: { value: 'rehydrated' },
})
expect(screen.getByRole('link')).toHaveTextContent('Rehydrated chat')
expect(screen.queryByText('Recent investigation')).not.toBeInTheDocument()
})
})