feat: add tab close menu actions (#24)
Co-authored-by: SuLe <sule@SuLedeMacBook-Air.local>
This commit is contained in:
parent
9ee31aa188
commit
aea6bf3239
79
src/App.vue
79
src/App.vue
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -168,6 +168,9 @@ export default {
|
|||
refreshChildren: "刷新",
|
||||
pin: "置顶",
|
||||
unpin: "取消置顶",
|
||||
closeTab: "关闭标签页",
|
||||
closeOtherTabs: "关闭其他标签页",
|
||||
closeAllTabs: "关闭全部标签页",
|
||||
copyName: "复制名称",
|
||||
},
|
||||
tree: {
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
Loading…
Reference in New Issue