Skip to content

Commit 45f1d64

Browse files
committed
feat(explorer): add context menu for tabs
1 parent 0f54bae commit 45f1d64

9 files changed

Lines changed: 188 additions & 60 deletions

File tree

.github/scripts/telegram-notify.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ if (GITHUB_EVENT_PATH && fs.existsSync(GITHUB_EVENT_PATH)) {
2525

2626
if (GITHUB_EVENT_NAME === 'push') {
2727
message += `\n*Push*\n\n`
28-
message += `Branch: ${GITHUB_REF?.replace('refs/heads/', '')}\n`
28+
message += `Branch: ${GITHUB_REF?.replace('refs/heads/', '')}\n\n`
2929

3030
payload.commits?.slice(0, 3).forEach((c, i) => {
3131
message += `${i + 1}. ${c.message.split('\n')[0]}\n`
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import type { ReactNode } from 'react'
2+
3+
import { useContextMenu } from '@/shared/hooks/useContextMenu'
4+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem } from '@/shared/ui/dropdown-menu'
5+
6+
interface Props {
7+
children: ReactNode
8+
disabledCloseOthers: boolean
9+
onClose: () => void
10+
onCloseOthers: () => void
11+
onCloseAll: () => void
12+
}
13+
export function ExplorerTabContextMenu({
14+
children,
15+
disabledCloseOthers,
16+
onClose,
17+
onCloseOthers,
18+
onCloseAll
19+
}: Props) {
20+
const { open, position, onContextMenu, close } = useContextMenu()
21+
22+
return (
23+
<>
24+
<div onContextMenu={onContextMenu}>{children}</div>
25+
26+
{open && (
27+
<DropdownMenu open={open} onOpenChange={val => !val && close()}>
28+
<DropdownMenuContent
29+
align="start"
30+
sideOffset={4}
31+
className="absolute"
32+
style={{ top: position.y, left: position.x }}
33+
>
34+
<DropdownMenuItem
35+
onClick={() => {
36+
onClose()
37+
close()
38+
}}
39+
>
40+
Close
41+
</DropdownMenuItem>
42+
<DropdownMenuItem
43+
disabled={disabledCloseOthers}
44+
onClick={() => {
45+
onCloseOthers()
46+
close()
47+
}}
48+
>
49+
Close Others
50+
</DropdownMenuItem>
51+
<DropdownMenuItem
52+
disabled={disabledCloseOthers}
53+
onClick={() => {
54+
onCloseAll()
55+
close()
56+
}}
57+
>
58+
Close All
59+
</DropdownMenuItem>
60+
</DropdownMenuContent>
61+
</DropdownMenu>
62+
)}
63+
</>
64+
)
65+
}

apps/desktop/renderer/src/modules/explorer/components/tabs/ExplorerTabs.tsx

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,51 @@ import type { ReactNode } from 'react'
22

33
import type { Tab } from '../../types/explorer.types'
44

5+
import { ExplorerTabContextMenu } from './ExplorerTabContextMenu'
56
import { TabContent } from './TabContent'
6-
import { TableTabs } from '@/shared/components/table-tabs'
7+
import { TableTabs } from './TableTabs'
78
import { cn } from '@/shared/lib/utils'
89

910
interface Props {
1011
tabs: Tab[]
1112
activeTab: string
1213
onSelectTab: (id: string) => void
1314
onCloseTab: (id: string) => void
15+
onCloseOthers: (id: string) => void
16+
onCloseAll: () => void
1417
children?: ReactNode
1518
}
1619

17-
export function ExplorerTabs({ tabs, activeTab, onSelectTab, onCloseTab }: Props) {
20+
export function ExplorerTabs({
21+
tabs,
22+
activeTab,
23+
onSelectTab,
24+
onCloseTab,
25+
onCloseOthers,
26+
onCloseAll
27+
}: Props) {
1828
return (
1929
<div className="flex h-full flex-col">
2030
<TableTabs
2131
tabs={tabs}
2232
activeTab={activeTab}
2333
onSelectTab={onSelectTab}
2434
onCloseTab={onCloseTab}
35+
renderTab={(tab, tabNode) => (
36+
<ExplorerTabContextMenu
37+
disabledCloseOthers={tabs.length <= 1}
38+
onClose={() => onCloseTab(tab.id)}
39+
onCloseOthers={() => {
40+
onSelectTab(tab.id)
41+
onCloseOthers(tab.id)
42+
}}
43+
onCloseAll={() => {
44+
onCloseAll()
45+
}}
46+
>
47+
<div className="h-full">{tabNode}</div>
48+
</ExplorerTabContextMenu>
49+
)}
2550
>
2651
{tabs.map(tab => (
2752
<div

apps/desktop/renderer/src/shared/components/table-tabs.tsx renamed to apps/desktop/renderer/src/modules/explorer/components/tabs/TableTabs.tsx

Lines changed: 43 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { ChevronLeft, ChevronRight, Table, X } from 'lucide-react'
2-
import React from 'react'
3-
import { useEffect, useRef, useState } from 'react'
2+
import { type ReactNode, useEffect, useRef, useState } from 'react'
43

5-
import { cn } from '../lib/utils'
6-
import { Button } from '../ui/button'
4+
import { cn } from '../../../../shared/lib/utils'
5+
import { Button } from '../../../../shared/ui/button'
76

87
export interface Tab {
98
id: string
@@ -16,10 +15,18 @@ interface TableTabsProps {
1615
activeTab: string
1716
onSelectTab: (id: string) => void
1817
onCloseTab: (id: string) => void
19-
children: React.ReactNode
18+
renderTab?: (tab: Tab, defaultTab: ReactNode) => ReactNode
19+
children: ReactNode
2020
}
2121

22-
export function TableTabs({ tabs, activeTab, onSelectTab, onCloseTab, children }: TableTabsProps) {
22+
export function TableTabs({
23+
tabs,
24+
activeTab,
25+
onSelectTab,
26+
onCloseTab,
27+
renderTab,
28+
children
29+
}: TableTabsProps) {
2330
const tabsRef = useRef<HTMLDivElement>(null)
2431
const [showScrollButtons, setShowScrollButtons] = useState(false)
2532

@@ -61,38 +68,42 @@ export function TableTabs({ tabs, activeTab, onSelectTab, onCloseTab, children }
6168
)}
6269

6370
<div ref={tabsRef} className="scrollbar-none flex flex-1 overflow-x-auto">
64-
{tabs.map(tab => (
65-
<button
66-
key={tab.id}
67-
type="button"
68-
className={cn(
69-
'group border-border flex shrink-0 items-center gap-2 border-r px-4 py-2 text-sm transition-colors',
70-
activeTab === tab.id
71-
? 'bg-background text-foreground'
72-
: 'bg-card text-muted-foreground hover:bg-secondary hover:text-foreground'
73-
)}
74-
onClick={() => onSelectTab(tab.id)}
75-
>
76-
<Table className="text-primary h-4 w-4" />
77-
<span className="max-w-32 truncate">{tab.name}</span>
71+
{tabs.map(tab => {
72+
const defaultTab = (
7873
<button
74+
key={tab.id}
7975
type="button"
8076
className={cn(
81-
'ml-1 rounded p-0.5 transition-colors',
82-
'text-muted-foreground hover:bg-secondary hover:text-foreground',
83-
'opacity-0 group-hover:opacity-100',
84-
activeTab === tab.id && 'opacity-100'
77+
'group border-border flex shrink-0 items-center gap-2 border-r px-4 py-2 text-sm transition-colors select-none',
78+
activeTab === tab.id
79+
? 'bg-background text-foreground'
80+
: 'bg-card text-muted-foreground hover:bg-secondary hover:text-foreground'
8581
)}
86-
onClick={e => {
87-
e.stopPropagation()
88-
onCloseTab(tab.id)
89-
}}
82+
onClick={() => onSelectTab(tab.id)}
9083
>
91-
<X className="h-3.5 w-3.5" />
92-
<span className="sr-only">Close tab</span>
84+
<Table className="text-primary h-4 w-4" />
85+
<span className="max-w-32 truncate">{tab.name}</span>
86+
87+
<button
88+
type="button"
89+
className={cn(
90+
'ml-1 rounded p-0.5 transition-colors',
91+
'text-muted-foreground hover:bg-secondary hover:text-foreground',
92+
'opacity-0 group-hover:opacity-100',
93+
activeTab === tab.id && 'opacity-100'
94+
)}
95+
onClick={e => {
96+
e.stopPropagation()
97+
onCloseTab(tab.id)
98+
}}
99+
>
100+
<X className="h-3.5 w-3.5" />
101+
</button>
93102
</button>
94-
</button>
95-
))}
103+
)
104+
105+
return renderTab ? renderTab(tab, defaultTab) : defaultTab
106+
})}
96107
</div>
97108

98109
{showScrollButtons && (
@@ -103,7 +114,6 @@ export function TableTabs({ tabs, activeTab, onSelectTab, onCloseTab, children }
103114
onClick={() => scroll('right')}
104115
>
105116
<ChevronRight className="h-4 w-4" />
106-
<span className="sr-only">Scroll right</span>
107117
</Button>
108118
)}
109119
</div>

apps/desktop/renderer/src/modules/explorer/hooks/useExplorerActions.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,6 @@ export function useExplorerActions(tabs: any, database: any) {
4040
}
4141

4242
const handleDisconnect = async () => {
43-
await window.datary.db.disconnect()
44-
4543
navigate('/')
4644
}
4745

apps/desktop/renderer/src/modules/explorer/hooks/useExplorerTabs.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,7 @@ export function useExplorerTabs() {
2222
const index = prev.findIndex(t => t.id === tabId)
2323
const nextTabs = prev.filter(t => t.id !== tabId)
2424

25-
if (activeTab !== tabId) {
26-
return nextTabs
27-
}
25+
if (activeTab !== tabId) return nextTabs
2826

2927
if (nextTabs.length === 0) {
3028
setActiveTab('')
@@ -40,5 +38,23 @@ export function useExplorerTabs() {
4038
[activeTab]
4139
)
4240

43-
return { tabs, activeTab, setActiveTab, addTab, closeTab }
41+
const closeOthers = useCallback((tabId: string) => {
42+
setTabs(prev => prev.filter(t => t.id === tabId))
43+
setActiveTab(tabId)
44+
}, [])
45+
46+
const closeAll = useCallback(() => {
47+
setTabs([])
48+
setActiveTab('')
49+
}, [])
50+
51+
return {
52+
tabs,
53+
activeTab,
54+
setActiveTab,
55+
addTab,
56+
closeTab,
57+
closeOthers,
58+
closeAll
59+
}
4460
}

apps/desktop/renderer/src/modules/explorer/pages/explorer.page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ export default function ExplorerPage() {
5151
activeTab={explorer.tabs.activeTab}
5252
onSelectTab={explorer.tabs.setActiveTab}
5353
onCloseTab={explorer.tabs.closeTab}
54+
onCloseOthers={explorer.tabs.closeOthers}
55+
onCloseAll={explorer.tabs.closeAll}
5456
/>
5557
</main>
5658
</div>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { useCallback, useState } from 'react'
2+
3+
export function useContextMenu() {
4+
const [open, setOpen] = useState(false)
5+
const [position, setPosition] = useState<{ x: number; y: number } | null>(null)
6+
7+
const onContextMenu = useCallback((e: React.MouseEvent) => {
8+
e.preventDefault()
9+
10+
setPosition({ x: e.clientX, y: e.clientY })
11+
setOpen(true)
12+
}, [])
13+
14+
const openAtButton = useCallback(() => {
15+
setPosition(null)
16+
setOpen(true)
17+
}, [])
18+
19+
const close = useCallback(() => setOpen(false), [])
20+
21+
return { open, position, onContextMenu, openAtButton, close }
22+
}

apps/desktop/renderer/src/widgets/sidebar/components/ConnectionItem.tsx

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useState } from 'react'
44
import { getConnectionIcon } from '../lib/get-connection-icon'
55

66
import type { DatabaseConnection } from '@/entities/connection/model/connection.types'
7+
import { useContextMenu } from '@/shared/hooks/useContextMenu'
78
import {
89
AlertDialog,
910
AlertDialogAction,
@@ -32,19 +33,13 @@ interface Props {
3233
}
3334

3435
export function ConnectionItem({ connection, collapsed, onSelect, onConnect, onDelete }: Props) {
35-
const [contextMenuOpen, setContextMenuOpen] = useState(false)
3636
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
37+
const { open, position, onContextMenu, openAtButton, close } = useContextMenu()
3738

38-
const handleContextMenu = (e: React.MouseEvent) => {
39-
e.preventDefault()
40-
setContextMenuOpen(true)
41-
}
42-
43-
const handleCloseContext = () => setContextMenuOpen(false)
4439
const handleDeleteConfirm = () => {
4540
onDelete?.()
4641
setIsDeleteDialogOpen(false)
47-
handleCloseContext()
42+
close()
4843
}
4944

5045
if (collapsed) {
@@ -65,11 +60,11 @@ export function ConnectionItem({ connection, collapsed, onSelect, onConnect, onD
6560

6661
return (
6762
<>
68-
<DropdownMenu open={contextMenuOpen} onOpenChange={setContextMenuOpen}>
63+
<DropdownMenu open={open} onOpenChange={val => !val && close()}>
6964
<DropdownMenuTrigger asChild>
7065
<button
7166
onClick={onSelect}
72-
onContextMenu={handleContextMenu}
67+
onContextMenu={onContextMenu}
7368
className="group hover:bg-sidebar-accent/50 relative flex w-full gap-3 rounded-md px-3 py-2.5 text-left"
7469
>
7570
<div className="mt-0.5 shrink-0">{getConnectionIcon(connection.type)}</div>
@@ -95,7 +90,7 @@ export function ConnectionItem({ connection, collapsed, onSelect, onConnect, onD
9590
variant="ghost"
9691
onClick={e => {
9792
e.stopPropagation()
98-
setContextMenuOpen(true)
93+
openAtButton()
9994
}}
10095
className="absolute top-1 right-1 h-6 w-6 opacity-0 group-hover:opacity-100"
10196
>
@@ -104,16 +99,11 @@ export function ConnectionItem({ connection, collapsed, onSelect, onConnect, onD
10499
</button>
105100
</DropdownMenuTrigger>
106101

107-
<DropdownMenuContent
108-
align="end"
109-
side="right"
110-
className="w-20"
111-
onCloseAutoFocus={handleCloseContext}
112-
>
102+
<DropdownMenuContent align="end" side="right" className="w-28">
113103
<DropdownMenuItem
114104
onClick={() => {
115105
onConnect?.()
116-
handleCloseContext()
106+
close()
117107
}}
118108
>
119109
<Database className="mr-2 h-4 w-4" />

0 commit comments

Comments
 (0)