| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251 |
- 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>
- )
- }
|