feat: add tab close menu actions (#24)

Co-authored-by: SuLe <sule@SuLedeMacBook-Air.local>
This commit is contained in:
SuLea-IT 2026-04-30 16:35:01 +08:00 committed by GitHub
parent 9ee31aa188
commit aea6bf3239
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 133 additions and 22 deletions

View File

@ -5,6 +5,13 @@ import { DatabaseZap, FilePlus2, Play, Loader2, X, Globe, Moon, Sun, Upload, Dow
import { Splitpanes, Pane } from "splitpanes";
import "splitpanes/dist/splitpanes.css";
import { Button } from "@/components/ui/button";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
import {
Select,
SelectContent,
@ -701,34 +708,62 @@ async function setupFileDrop() {
<div class="h-full flex flex-col min-w-0">
<!-- Tabs Bar -->
<div v-if="queryStore.tabs.length > 0" class="h-8 flex items-center border-b bg-muted/20 overflow-x-auto shrink-0">
<div
<ContextMenu
v-for="tab in queryStore.tabs"
:key="tab.id"
class="group flex min-w-0 items-center gap-1 px-3 h-full text-xs cursor-pointer border-r hover:bg-accent transition-colors whitespace-nowrap"
:class="{ 'bg-background font-medium': tab.id === queryStore.activeTabId }"
@click="queryStore.activeTabId = tab.id"
>
<span v-if="connectionColor(tab.connectionId)" class="h-4 w-1 rounded-full shrink-0" :style="{ backgroundColor: connectionColor(tab.connectionId) }" />
<span class="min-w-0 truncate">{{ tabDisplayTitle(tab) }}</span>
<Tooltip>
<TooltipTrigger as-child>
<ContextMenuTrigger as-child>
<div
class="group flex min-w-0 items-center gap-1 px-3 h-full text-xs cursor-pointer border-r hover:bg-accent transition-colors whitespace-nowrap"
:class="{ 'bg-background font-medium': tab.id === queryStore.activeTabId }"
@click="queryStore.activeTabId = tab.id"
>
<span v-if="connectionColor(tab.connectionId)" class="h-4 w-1 rounded-full shrink-0" :style="{ backgroundColor: connectionColor(tab.connectionId) }" />
<span class="min-w-0 truncate">{{ tabDisplayTitle(tab) }}</span>
<Tooltip>
<TooltipTrigger as-child>
<button
class="ml-1 rounded p-0.5 text-muted-foreground hover:bg-muted-foreground/20 hover:text-foreground focus:opacity-100"
:class="tab.pinned ? 'opacity-100 text-primary' : 'opacity-0 group-hover:opacity-100'"
@click.stop="queryStore.togglePinnedTab(tab.id)"
>
<Pin class="h-3 w-3" :class="{ 'fill-current': tab.pinned }" />
</button>
</TooltipTrigger>
<TooltipContent>{{ tab.pinned ? t('contextMenu.unpin') : t('contextMenu.pin') }}</TooltipContent>
</Tooltip>
<button
class="ml-1 rounded p-0.5 text-muted-foreground hover:bg-muted-foreground/20 hover:text-foreground focus:opacity-100"
:class="tab.pinned ? 'opacity-100 text-primary' : 'opacity-0 group-hover:opacity-100'"
@click.stop="queryStore.togglePinnedTab(tab.id)"
class="rounded hover:bg-muted-foreground/20 p-0.5"
@click.stop="queryStore.closeTab(tab.id)"
>
<Pin class="h-3 w-3" :class="{ 'fill-current': tab.pinned }" />
<X class="h-3 w-3" />
</button>
</TooltipTrigger>
<TooltipContent>{{ tab.pinned ? t('contextMenu.unpin') : t('contextMenu.pin') }}</TooltipContent>
</Tooltip>
<button
class="rounded hover:bg-muted-foreground/20 p-0.5"
@click.stop="queryStore.closeTab(tab.id)"
>
<X class="h-3 w-3" />
</button>
</div>
</div>
</ContextMenuTrigger>
<ContextMenuContent class="w-44">
<ContextMenuItem @click="queryStore.togglePinnedTab(tab.id)">
<Pin class="w-3.5 h-3.5 mr-2" :class="{ 'fill-current': tab.pinned }" />
{{ tab.pinned ? t('contextMenu.unpin') : t('contextMenu.pin') }}
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem @click="queryStore.closeTab(tab.id)">
<X class="w-3.5 h-3.5 mr-2" />
{{ t('contextMenu.closeTab') }}
</ContextMenuItem>
<ContextMenuItem
:disabled="queryStore.tabs.length <= 1"
@click="queryStore.closeOtherTabs(tab.id)"
>
<X class="w-3.5 h-3.5 mr-2" />
{{ t('contextMenu.closeOtherTabs') }}
</ContextMenuItem>
<ContextMenuItem variant="destructive" @click="queryStore.closeAllTabs">
<X class="w-3.5 h-3.5 mr-2" />
{{ t('contextMenu.closeAllTabs') }}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
</div>
<!-- Editor Panel -->

View File

@ -170,6 +170,9 @@ export default {
refreshChildren: "Refresh",
pin: "Pin",
unpin: "Unpin",
closeTab: "Close Tab",
closeOtherTabs: "Close Other Tabs",
closeAllTabs: "Close All Tabs",
copyName: "Copy Name",
},
tree: {

View File

@ -168,6 +168,9 @@ export default {
refreshChildren: "刷新",
pin: "置顶",
unpin: "取消置顶",
closeTab: "关闭标签页",
closeOtherTabs: "关闭其他标签页",
closeAllTabs: "关闭全部标签页",
copyName: "复制名称",
},
tree: {

View File

@ -0,0 +1,26 @@
export interface TabLike {
id: string;
}
export interface TabCloseState<T extends TabLike> {
tabs: T[];
activeTabId: string | null;
}
export function closeOtherTabsState<T extends TabLike>(
tabs: readonly T[],
activeTabId: string | null,
targetTabId: string,
): TabCloseState<T> {
const target = tabs.find((tab) => tab.id === targetTabId);
if (!target) return { tabs: [...tabs], activeTabId };
return { tabs: [target], activeTabId: target.id };
}
export function closeAllTabsState<T extends TabLike>(
_tabs: readonly T[],
_activeTabId: string | null,
): TabCloseState<T> {
return { tabs: [], activeTabId: null };
}

View File

@ -2,6 +2,7 @@ import { defineStore } from "pinia";
import { ref } from "vue";
import type { QueryTab } from "@/types/database";
import { orderPinnedFirst } from "@/lib/pinnedItems";
import { closeAllTabsState, closeOtherTabsState } from "@/lib/tabCloseActions";
import * as api from "@/lib/tauri";
export const useQueryStore = defineStore("query", () => {
@ -46,6 +47,18 @@ export const useQueryStore = defineStore("query", () => {
}
}
function closeOtherTabs(id: string) {
const next = closeOtherTabsState(tabs.value, activeTabId.value, id);
tabs.value = next.tabs;
activeTabId.value = next.activeTabId;
}
function closeAllTabs() {
const next = closeAllTabsState(tabs.value, activeTabId.value);
tabs.value = next.tabs;
activeTabId.value = next.activeTabId;
}
function updateSql(id: string, sql: string) {
const tab = tabs.value.find((t) => t.id === id);
if (tab) tab.sql = sql;
@ -123,6 +136,8 @@ export const useQueryStore = defineStore("query", () => {
activeTabId,
createTab,
closeTab,
closeOtherTabs,
closeAllTabs,
updateSql,
togglePinnedTab,
updateDatabase,

View File

@ -0,0 +1,29 @@
import { strict as assert } from "node:assert";
import test from "node:test";
import { closeAllTabsState, closeOtherTabsState } from "../src/lib/tabCloseActions.js";
test("close other tabs keeps the target tab and makes it active", () => {
const tabs = [{ id: "a" }, { id: "b" }, { id: "c" }];
const result = closeOtherTabsState(tabs, "a", "b");
assert.deepEqual(result.tabs.map((tab) => tab.id), ["b"]);
assert.equal(result.activeTabId, "b");
assert.deepEqual(tabs.map((tab) => tab.id), ["a", "b", "c"]);
});
test("close other tabs is a no-op when the target tab is missing", () => {
const tabs = [{ id: "a" }, { id: "b" }];
const result = closeOtherTabsState(tabs, "a", "missing");
assert.deepEqual(result.tabs.map((tab) => tab.id), ["a", "b"]);
assert.equal(result.activeTabId, "a");
});
test("close all tabs clears every tab and active tab", () => {
const result = closeAllTabsState([{ id: "a" }, { id: "b" }], "a");
assert.deepEqual(result.tabs, []);
assert.equal(result.activeTabId, null);
});