diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue
index 8fd79ceff..29bddbbb0 100644
--- a/apps/desktop/src/components/layout/ContentArea.vue
+++ b/apps/desktop/src/components/layout/ContentArea.vue
@@ -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. -->
-
+
+
{{ activeDataTabTableMeta?.tableName || activeTab.title }}
-
+
+
{{ activeDataTabTableMeta.columns.length }} {{ t("tree.columns") }}
diff --git a/apps/desktop/src/components/layout/__tests__/ContentArea.dataHeaderNavigation.spec.ts b/apps/desktop/src/components/layout/__tests__/ContentArea.dataHeaderNavigation.spec.ts
new file mode 100644
index 000000000..6da567023
--- /dev/null
+++ b/apps/desktop/src/components/layout/__tests__/ContentArea.dataHeaderNavigation.spec.ts
@@ -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({
+ 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("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("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("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);
+ });
+});