fix(tabs): isolate tab collision numbering by mode

This commit is contained in:
lxk955
2026-10-17 13:24:05 +08:00
committed by GitHub
parent adbe7e7ab1
commit 5fd7467b7c
4 changed files with 57 additions and 8 deletions
@@ -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> = {}) => 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();
});
});
});
+10 -7
View File
@@ -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;
@@ -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();
});
});
+1 -1
View File
@@ -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" },
);