Files
supabase/apps/studio/components/layouts/Tabs/Tabs.tsx
Gildas Garcia 4e86c39ea1 chore: remove <ContextMenu> _Shadcn_ suffix (#45971)
## Problem

The `_Shadcn_` suffix isn't needed anymore on `<ContextMenu_Shadcn_>`
and related components

## Solution

Remove it. No other changes

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

* **Refactor**
* Replaced legacy context-menu component variants with the unified UI
context-menu components across the app for consistent rendering and
imports; behavior and menu content remain unchanged.
* **Tests**
* Updated a test mock to track the unified context-menu component mount
count.
* **Chores**
* Simplified UI package re-exports to expose the canonical context-menu
symbols.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/45971)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-15 15:09:25 +02:00

252 lines
8.6 KiB
TypeScript

import {
DndContext,
DragEndEvent,
DragOverlay,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core'
import { horizontalListSortingStrategy, SortableContext } from '@dnd-kit/sortable'
import { useParams } from 'common'
import { AnimatePresence, motion } from 'framer-motion'
import { Plus, X } from 'lucide-react'
import { useRouter } from 'next/router'
import {
cn,
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
Tabs_Shadcn_,
TabsList_Shadcn_,
TabsTrigger_Shadcn_,
} from 'ui'
import { useEditorType } from '../editors/EditorsLayout.hooks'
import { CollapseButton } from './CollapseButton'
import { SortableTab } from './SortableTab'
import { TabPreview } from './TabPreview'
import { useTabsScroll } from './Tabs.utils'
import { useDashboardHistory } from '@/hooks/misc/useDashboardHistory'
import { editorEntityTypes, useTabsStateSnapshot, type Tab } from '@/state/tabs'
export const EditorTabs = () => {
const { ref, id } = useParams()
const router = useRouter()
const { setLastVisitedSnippet, setLastVisitedTable } = useDashboardHistory()
const editor = useEditorType()
const tabs = useTabsStateSnapshot()
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 1, // Start with a very small distance
},
})
)
const openTabs = tabs.openTabs
.map((id) => tabs.tabsMap[id])
.filter((tab) => tab !== undefined) as Tab[]
const hasNewTab = router.asPath.includes('/new')
// Filter by editor type - only show SQL tabs for SQL editor and table tabs for table editor
const editorTabs = !!editor
? openTabs.filter((tab) => editorEntityTypes[editor]?.includes(tab.type))
: []
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event
if (!over || active.id === over.id) return
const oldIndex = tabs.openTabs.indexOf(active.id.toString())
const newIndex = tabs.openTabs.indexOf(over.id.toString())
if (oldIndex !== newIndex) {
tabs.handleTabDragEnd(oldIndex, newIndex, active.id.toString(), router)
}
}
const onClearDashboardHistory = () => {
if (editor === 'table') {
setLastVisitedTable(undefined)
} else if (editor === 'sql') {
setLastVisitedSnippet(undefined)
}
}
const handleClose = (tabId: string) => {
tabs.handleTabClose({ id: tabId, router, editor, onClearDashboardHistory })
}
const handleCloseAll = () => {
if (editor) {
const tabsToClose =
editor === 'table'
? tabs.openTabs.filter((x) => !x.startsWith('sql'))
: tabs.openTabs.filter((x) => x.startsWith('sql'))
tabs.removeTabs(tabsToClose)
onClearDashboardHistory()
router.push(`/project/${ref}/${editor === 'table' ? 'editor' : 'sql'}`)
}
}
const handleCloseOthers = (tabId: string) => {
if (editor) {
const tabsToClose =
editor === 'table'
? tabs.openTabs.filter((x) => !x.startsWith('sql') && x !== tabId)
: tabs.openTabs.filter((x) => x.startsWith('sql') && x !== tabId)
tabs.removeTabs(tabsToClose)
onClearDashboardHistory()
const entityId = editor === 'table' ? tabId.split('-')[1] : tabId.split('sql-')[1]
if (id !== entityId) {
router.push(`/project/${ref}/${editor === 'table' ? 'editor' : 'sql'}/${entityId}`)
}
}
}
const handleCloseRight = (tabId: string) => {
if (editor) {
const openedTabs =
editor === 'table'
? tabs.openTabs.filter((x) => !x.startsWith('sql'))
: tabs.openTabs.filter((x) => x.startsWith('sql'))
const tabIdx = openedTabs.indexOf(tabId)
const activeTabIdx = openedTabs.indexOf(tabs.activeTab!)
const tabsToClose = openedTabs.slice(tabIdx + 1)
tabs.removeTabs(tabsToClose)
const isActiveTabClosed = tabIdx < activeTabIdx
if (isActiveTabClosed) {
const id = editor === 'table' ? tabId.split('-')[1] : tabId.split('sql-')[1]
router.push(`/project/${ref}/${editor === 'table' ? 'editor' : 'sql'}/${id}`)
}
}
}
const handleTabChange = (id: string) => {
tabs.handleTabNavigation(id, router)
}
const { tabsListRef } = useTabsScroll({ activeTab: tabs.activeTab, tabCount: editorTabs.length })
return (
<DndContext sensors={sensors} onDragEnd={handleDragEnd}>
<Tabs_Shadcn_
className="w-full flex"
value={hasNewTab ? 'new' : (tabs.activeTab ?? undefined)}
onValueChange={handleTabChange}
>
<CollapseButton hideTabs={false} />
<TabsList_Shadcn_
ref={tabsListRef}
className={cn(
'rounded-b-none gap-0 min-h-(--header-height) flex items-center w-full z-1',
'bg-surface-200 dark:bg-alternative border-none text-clip overflow-x-auto'
)}
>
<SortableContext
items={editorTabs.map((tab) => tab.id)}
strategy={horizontalListSortingStrategy}
>
{editorTabs.map((tab, index) => (
<ContextMenu key={tab.id}>
<ContextMenuTrigger>
<SortableTab
key={tab.id}
tab={tab}
index={index}
openTabs={openTabs}
onClose={() => handleClose(tab.id)}
/>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem onClick={() => handleClose(tab.id)}>Close</ContextMenuItem>
<ContextMenuItem onClick={() => handleCloseOthers(tab.id)}>
Close Others
</ContextMenuItem>
<ContextMenuItem onClick={() => handleCloseRight(tab.id)}>
Close to the Right
</ContextMenuItem>
<ContextMenuItem onClick={handleCloseAll}>Close All</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
))}
</SortableContext>
{/* Non-draggable new tab */}
{hasNewTab && (
<TabsTrigger_Shadcn_
value="new"
className={cn(
'flex items-center gap-2 px-3 text-xs',
'bg-dash-sidebar/50 dark:bg-surface-100/50',
'data-[state=active]:bg-dash-sidebar dark:data-[state=active]:bg-surface-100',
'relative group h-full border-t-2 border-b-0!',
'hover:bg-surface-300 dark:hover:bg-surface-100'
)}
>
<Plus size={16} strokeWidth={1.5} className={'text-foreground-lighter'} />
<div className="flex items-center gap-0">
<span>New</span>
</div>
<span
role="button"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
}}
className="ml-1 opacity-0 group-hover:opacity-100 hover:bg-200 rounded-xs cursor-pointer"
onMouseDown={(e) => {
e.preventDefault()
e.stopPropagation()
}}
onPointerDown={(e) => {
e.preventDefault()
e.stopPropagation()
handleClose('new')
}}
>
<X size={12} className="text-foreground-light" />
</span>{' '}
<div className="absolute w-full -bottom-px left-0 right-0 h-px bg-dash-sidebar dark:bg-surface-100 opacity-0 group-data-[state=active]:opacity-100" />
</TabsTrigger_Shadcn_>
)}
<AnimatePresence initial={false}>
{!hasNewTab && (
<motion.button
className="flex items-center justify-center w-10 min-h-(--header-height) hover:bg-surface-100 shrink-0 border-b"
onClick={() =>
router.push(
`/project/${router.query.ref}/${editor === 'table' ? 'editor' : 'sql'}/new?skip=true`
)
}
initial={{ opacity: 0, scale: 0.8, x: -10 }}
animate={{ opacity: 1, scale: 1, x: 0 }}
transition={{ duration: 0.2 }}
>
<Plus
size={16}
strokeWidth={1.5}
className="text-foreground-lighter hover:text-foreground-light"
/>
</motion.button>
)}
</AnimatePresence>
<div className="grow h-full border-b pr-6" />
</TabsList_Shadcn_>
</Tabs_Shadcn_>
<DragOverlay dropAnimation={null}>
{tabs.activeTab ? <TabPreview tab={tabs.activeTab} /> : null}
</DragOverlay>
</DndContext>
)
}