diff --git a/apps/desktop/src/lib/__tests__/tabs/tabPresentation.spec.ts b/apps/desktop/src/lib/__tests__/tabs/tabPresentation.spec.ts index edd425a75a..924de1df22 100644 --- a/apps/desktop/src/lib/__tests__/tabs/tabPresentation.spec.ts +++ b/apps/desktop/src/lib/__tests__/tabs/tabPresentation.spec.ts @@ -469,6 +469,26 @@ describe("tab group presentation", () => { expect(titles.get("tab-preview")).toBe("SQL"); }); + it("does not collide query tabs with object browser or table data tabs sharing the same title", () => { + const store = useConnectionStore(); + store.connections = [{ id: "conn-1", name: "PostgreSQL", db_type: "postgres", driver_profile: "postgres", database: "app" } as ConnectionConfig]; + useSettingsStore().editorSettings.compactTabTitle = true; + + // In compact mode, object tab displays "test" (schema), data tab displays "test" (tableName), query tab displays "test" (custom title) + const objectTab = queryTab({ id: "tab-obj", mode: "objects", title: "test", database: "test", objectBrowser: { schema: "test" } }); + const dataTab = queryTab({ id: "tab-data", mode: "data", title: "test", tableMeta: { schema: "public", tableName: "test", columns: [], primaryKeys: [] } }); + const queryTab1 = queryTab({ id: "tab-query-1", mode: "query", title: "test", customTitle: true }); + const queryTab2 = queryTab({ id: "tab-query-2", mode: "query", title: "test", customTitle: true }); + + const titles = titlesWithNumbers([objectTab, dataTab, queryTab1, queryTab2]); + // Object and table tabs are not in query mode, so they must not be numbered or affected by query tabs + expect(titles.get("tab-obj")).toBe("test"); + expect(titles.get("tab-data")).toBe("test"); + // Multiple query tabs with the same title still collide with each other and receive numeric suffixes + expect(titles.get("tab-query-1")).toBe("test 1"); + expect(titles.get("tab-query-2")).toBe("test 2"); + }); + describe("custom title suffix", () => { const dataTab = (id: string, overrides: Partial = {}) => queryTab({ id, mode: "data", title: "ticket_bug", tableMeta: { schema: "feedback_v2", tableName: "ticket_bug", columns: [], primaryKeys: [] }, ...overrides }); @@ -581,6 +601,15 @@ describe("tab group presentation", () => { expect(findTabWithSameTitleSuffix(tabs, tabs[1]!, "", translate)).toBeUndefined(); expect(findTabWithSameTitleSuffix(tabs, tabs[1]!, "done", translate)).toBeUndefined(); }); + + it("ignores tabs of a different mode sharing the same base title and suffix", () => { + seedConnection(); + const data = dataTab("tab-data", { titleSuffix: "pending" }); + const query = queryTab({ id: "tab-query", mode: "query", customTitle: true, title: tabDisplayTitle(data, translate) }); + syncTabTitleNumbers([data, query], translate); + + expect(findTabWithSameTitleSuffix([data, query], query, "pending", translate)).toBeUndefined(); + }); }); }); diff --git a/apps/desktop/src/lib/tabs/tabPresentation.ts b/apps/desktop/src/lib/tabs/tabPresentation.ts index 1d7157548c..baffb02269 100644 --- a/apps/desktop/src/lib/tabs/tabPresentation.ts +++ b/apps/desktop/src/lib/tabs/tabPresentation.ts @@ -188,7 +188,8 @@ export function findTabWithSameTitleSuffix(tabs: QueryTab[], target: QueryTab, s const wanted = suffix.trim(); if (!wanted) return undefined; const base = tabDisplayTitle(target, t); - return tabs.find((tab) => tab.id !== target.id && tabDisplayTitle(tab, t) === base && tabTitleSuffix(tab, base) === wanted); + const targetMode = target.mode || "query"; + return tabs.find((tab) => tab.id !== target.id && (tab.mode || "query") === targetMode && tabDisplayTitle(tab, t) === base && tabTitleSuffix(tab, base) === wanted); } function tabTitleNumber(tab: QueryTab, title: string): number | undefined { @@ -222,6 +223,7 @@ export function syncTabTitleNumbers(tabs: QueryTab[], t: Translate): void { for (const tab of tabs) { if (isPreviewTab(tab)) continue; const title = tabDisplayTitle(tab, t); + const groupKey = `${tab.mode || "query"}\u0000${title}`; const custom = customTitleSuffix(tab); if (custom) { // A custom suffix replaces the number, so the tab leaves the numbering @@ -229,17 +231,18 @@ export function syncTabTitleNumbers(tabs: QueryTab[], t: Translate): void { // fresh one instead of resurrecting a stale value. tab.titleNumber = undefined; tab.titleNumberKey = undefined; - const used = customSuffixes.get(title); + const used = customSuffixes.get(groupKey); if (used) used.add(custom); - else customSuffixes.set(title, new Set([custom])); + else customSuffixes.set(groupKey, new Set([custom])); continue; } - const group = groups.get(title); + const group = groups.get(groupKey); if (group) group.push(tab); - else groups.set(title, [tab]); + else groups.set(groupKey, [tab]); } - for (const [title, group] of groups) { + for (const [groupKey, group] of groups) { + const title = groupKey.slice(groupKey.indexOf("\u0000") + 1); const numbered = group.filter((tab) => tabTitleNumber(tab, title) !== undefined); if (group.length < 2 && numbered.length === 0) { // A lone, never-numbered tab shows its plain title; drop a number that a @@ -255,7 +258,7 @@ export function syncTabTitleNumbers(tabs: QueryTab[], t: Translate): void { let next = numbered.reduce((highest, tab) => Math.max(highest, tab.titleNumber ?? 0), 0) + 1; for (const tab of group) { if (tabTitleNumber(tab, title) !== undefined) continue; - while (customSuffixes.get(title)?.has(String(next))) next += 1; + while (customSuffixes.get(groupKey)?.has(String(next))) next += 1; tab.titleNumber = next; tab.titleNumberKey = title; next += 1; diff --git a/apps/desktop/src/stores/__tests__/queryStore.titleSuffix.spec.ts b/apps/desktop/src/stores/__tests__/queryStore.titleSuffix.spec.ts index 312eaac29a..d09947ca8e 100644 --- a/apps/desktop/src/stores/__tests__/queryStore.titleSuffix.spec.ts +++ b/apps/desktop/src/stores/__tests__/queryStore.titleSuffix.spec.ts @@ -96,4 +96,21 @@ describe("queryStore tab title suffix", () => { store.setTabTitleSuffix(second, ""); expect(numberOf(second)).toBe(2); }); + + it("does not number or alter a data tab when a query tab is renamed to match its name", async () => { + const { useQueryStore } = await import("@/stores/queryStore"); + const store = useQueryStore(); + const dataTab = store.createTab("pg-1", "app", "users", "data", "public", undefined, undefined, { forceNew: true }); + const queryTab = store.createTab("pg-1", "app", "scratch", "query", undefined, undefined, undefined, { forceNew: true }); + + const dataTabInstance = store.tabs.find((t) => t.id === dataTab)!; + expect(dataTabInstance.titleNumber).toBeUndefined(); + + store.renameTab(queryTab, dataTabInstance.title); + await nextTick(); + + const queryTabInstance = store.tabs.find((t) => t.id === queryTab)!; + expect(dataTabInstance.titleNumber).toBeUndefined(); + expect(queryTabInstance.titleNumber).toBeUndefined(); + }); }); diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index 396c693f5d..17afeba685 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -1173,7 +1173,7 @@ export const useQueryStore = defineStore("query", () => { // 里而不是渲染函数里,是为了避免在 computed 求值过程中写标签状态;getter 只读 // 参与标题计算的字段(id + 显示标题),分配编号本身不会再次触发它。 watch( - () => tabs.value.map((tab) => `${tab.id}\u0000${tabDisplayTitle(tab, t)}\u0000${tab.titleSuffix ?? ""}`), + () => tabs.value.map((tab) => `${tab.id}\u0000${tab.mode || "query"}\u0000${tabDisplayTitle(tab, t)}\u0000${tab.titleSuffix ?? ""}`), () => syncTabTitleNumbers(tabs.value, t), { immediate: true, flush: "sync" }, );