feat(query): make data view header pills clickable navigation

This commit is contained in:
miracle
2026-10-02 00:54:55 +08:00
committed by GitHub
parent 4ed3662da2
commit 16b0052b74
2 changed files with 155 additions and 4 deletions
@@ -2619,15 +2619,30 @@ defineExpose({
(#7880). The min-w floor keeps scrollWidth reporting real
overflow so the measured tiers condense the row before the
chips collapse. -->
<span v-if="activeConnection?.name?.trim()" data-data-header-connection class="inline-flex min-w-12 items-center truncate rounded border border-border bg-muted/30 px-2 py-0.5 text-muted-foreground" :title="activeConnection.name">
<button
v-if="activeConnection?.name?.trim()"
type="button"
data-data-header-connection
class="inline-flex min-w-12 items-center truncate rounded border border-border bg-muted/30 px-2 py-0.5 text-muted-foreground cursor-pointer hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none"
:title="activeConnection.name"
:disabled="!activeTab.connectionId"
@click="queryStore.openDatabaseBrowser(activeTab.connectionId)"
>
{{ activeConnection.name }}
</span>
</button>
<span class="inline-flex min-w-12 items-center truncate rounded border border-border bg-muted/50 px-2 py-0.5 font-medium" :title="activeDataTabTableMeta?.tableName || activeTab.title">
{{ activeDataTabTableMeta?.tableName || activeTab.title }}
</span>
<span class="inline-flex min-w-12 items-center truncate rounded border border-border bg-muted/30 px-2 py-0.5 text-muted-foreground" :title="[activeDataTabTableMeta?.schema, databaseDisplayNameForTab(activeTab.connectionId, activeTab.database, t)].filter(Boolean).join('@')">
<button
type="button"
data-data-header-database
class="inline-flex min-w-12 items-center truncate rounded border border-border bg-muted/30 px-2 py-0.5 text-muted-foreground cursor-pointer hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none"
:title="[activeDataTabTableMeta?.schema, databaseDisplayNameForTab(activeTab.connectionId, activeTab.database, t)].filter(Boolean).join('@')"
:disabled="!activeTab.connectionId || !activeTab.database"
@click="queryStore.openObjectBrowser(activeTab.connectionId, activeTab.database, activeDataTabTableMeta?.schema, activeDataTabTableMeta?.catalog)"
>
<template v-if="activeDataTabTableMeta?.schema">{{ activeDataTabTableMeta.schema }}@</template>{{ databaseDisplayNameForTab(activeTab.connectionId, activeTab.database, t) }}
</span>
</button>
<span v-if="showDataColumnsChip && activeDataTabTableMeta" class="inline-flex shrink-0 items-center rounded border border-border bg-muted/30 px-2 py-0.5 font-medium text-muted-foreground tabular-nums"> {{ activeDataTabTableMeta.columns.length }} {{ t("tree.columns") }} </span>
<span class="ml-auto" />
<DataGridColumnLayoutPopover v-if="activeTab.result?.columns.length" :grid="dataGridRef" trigger-class="px-1.5" />
@@ -0,0 +1,136 @@
// @vitest-environment happy-dom
import { createApp, h, nextTick, reactive, type App } from "vue";
import { createPinia, setActivePinia } from "pinia";
import { createI18n } from "vue-i18n";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { ConnectionConfig, QueryTab } from "@/types/database";
vi.mock("@/components/editor/QueryEditor.vue", () => ({ default: { render: () => null } }));
vi.mock("@/components/grid/DataGrid.vue", () => ({ default: { render: () => null } }));
import ContentArea from "../ContentArea.vue";
import { useConnectionStore } from "@/stores/connectionStore";
import { useQueryStore } from "@/stores/queryStore";
const mounted: Array<{ app: App; host: HTMLElement }> = [];
const connection: ConnectionConfig = {
id: "sqlserver-prd",
name: "辽宁-prd",
db_type: "sqlserver",
host: "localhost",
port: 1433,
username: "sa",
password: "",
};
afterEach(() => {
for (const { app, host } of mounted.splice(0)) {
app.unmount();
host.remove();
}
localStorage.clear();
});
async function mountDataTab() {
const pinia = createPinia();
setActivePinia(pinia);
useConnectionStore().connections = [connection];
const store = useQueryStore();
const tab = reactive<QueryTab>({
id: "abnormal-orders",
title: "B_TAbnormalOrder",
connectionId: connection.id,
database: "Inspharmacy",
schema: "dbo",
mode: "data",
sql: "SELECT * FROM dbo.B_TAbnormalOrder",
isExecuting: false,
tableMeta: { tableName: "B_TAbnormalOrder", schema: "dbo", columns: [], primaryKeys: [] },
});
store.tabs = [tab];
store.groups = [{ id: "main", tabIds: [tab.id], activeTabId: tab.id }];
store.switchTab(tab.id);
const host = document.createElement("div");
document.body.appendChild(host);
const app = createApp({
setup: () => () =>
h(ContentArea, {
activeTab: tab,
activeConnection: connection,
activeOutputView: "result",
executableSql: "",
formatSqlRequest: null,
compressSqlRequest: null,
selectedSql: "",
cursorPos: 0,
blockDangerousRedisCommands: false,
}),
});
app.use(pinia);
app.use(createI18n({ legacy: false, locale: "en", messages: { en: {} }, missingWarn: false, fallbackWarn: false }));
app.mount(host);
mounted.push({ app, host });
await nextTick();
return { host, store, tab };
}
describe("table data header navigation", () => {
it.each([false, true])("opens the connection browser, reusing an existing tab: %s", async (alreadyOpen) => {
const { host, store, tab } = await mountDataTab();
const existingId = alreadyOpen ? store.openDatabaseBrowser(connection.id) : undefined;
store.openDatabaseBrowser("other-connection");
store.switchTab(tab.id);
const button = host.querySelector<HTMLButtonElement>("button[data-data-header-connection]")!;
expect(button.textContent?.trim()).toBe(connection.name);
button.click();
const targets = store.tabs.filter((item) => item.mode === "databases" && item.connectionId === connection.id);
expect(targets).toHaveLength(1);
expect(store.activeTabId).toBe(existingId ?? targets[0]!.id);
expect(store.tabs).toContainEqual(tab);
store.switchTab(tab.id);
button.click();
expect(store.activeTabId).toBe(targets[0]!.id);
expect(store.tabs.filter((item) => item.mode === "databases")).toHaveLength(2);
});
it.each([false, true])("opens the schema object browser, reusing an existing tab: %s", async (alreadyOpen) => {
const { host, store, tab } = await mountDataTab();
const existingId = alreadyOpen ? store.openObjectBrowser(connection.id, tab.database, "dbo") : undefined;
store.openObjectBrowser("other-connection", tab.database, "dbo");
store.openObjectBrowser(connection.id, "other-database", "dbo");
store.openObjectBrowser(connection.id, tab.database, "other-schema");
store.switchTab(tab.id);
const button = host.querySelector<HTMLButtonElement>("button[data-data-header-database]")!;
expect(button.textContent?.trim()).toBe("dbo@Inspharmacy");
button.click();
const target = store.tabs.find((item) => item.id === store.activeTabId)!;
expect(target).toMatchObject({ mode: "objects", connectionId: connection.id, database: tab.database, objectBrowser: { schema: "dbo" } });
if (existingId) expect(target.id).toBe(existingId);
expect(store.tabs.filter((item) => item.mode === "objects")).toHaveLength(4);
store.switchTab(tab.id);
button.click();
expect(store.activeTabId).toBe(target.id);
expect(store.tabs.filter((item) => item.mode === "objects")).toHaveLength(4);
});
it("preserves catalog scope and focuses an existing browser in another group", async () => {
const { host, store, tab } = await mountDataTab();
tab.tableMeta!.catalog = "external-catalog";
const targetId = store.openObjectBrowser(connection.id, tab.database, "dbo", "external-catalog");
expect(store.splitTabRight(targetId)).toBe(true);
store.openObjectBrowser(connection.id, tab.database, "dbo", "other-catalog");
store.switchTab(tab.id);
await nextTick();
host.querySelector<HTMLButtonElement>("button[data-data-header-database]")!.click();
expect(store.activeTabId).toBe(targetId);
expect(store.groups.find((group) => group.id === store.focusedGroupId)?.activeTabId).toBe(targetId);
expect(store.tabs.filter((item) => item.mode === "objects")).toHaveLength(2);
});
});