mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-02 02:34:42 +08:00
feat(query): make data view header pills clickable navigation
This commit is contained in:
@@ -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" />
|
||||
|
||||
+136
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user