feat(editor): handle tabs after connection disconnect

This commit is contained in:
javalover123 authored and GitHub committed 2026-09-20 01:12:21 +08:00
1 parent 3cb1faf1c6
commit 28cc1b9768
31 files changed
+1137 -60

No files matched your search

@@ -250,3 +250,28 @@ describe("openTabsPersistence originalSql round-trip", () => {
});
});
});
describe("openTabsPersistence detached connection tabs", () => {
it("preserves the original connection name of a kept SQL tab across a round-trip", () => {
const [restored] = roundTrip([queryTab({ connectionId: "deleted-conn", detachedConnectionName: "prod" })]);
expect(restored.connectionId).toBe("deleted-conn");
expect(restored.detachedConnectionName).toBe("prod");
});
it("keeps a detached SQL tab even though its connection no longer exists", () => {
const saved = serializeOpenTabs([queryTab({ id: "orphan", connectionId: "deleted-conn", detachedConnectionName: "prod" })]);
const { tabs } = restoreOpenTabsPayload({ tabs: saved, activeTabId: "orphan" }, { validConnectionIds: ["other-conn"] });
// SQL 页签不受 validConnectionIds 过滤,删除连接后保留下来的草稿页签才能跨重启存活。
expect(tabs.map((tab) => tab.id)).toEqual(["orphan"]);
expect(tabs[0].detachedConnectionName).toBe("prod");
});
it("omits the detached connection name when the tab is bound to a live connection", () => {
const [saved] = serializeOpenTabs([queryTab({})]);
expect(saved.detachedConnectionName).toBeUndefined();
});
});
@@ -23,6 +23,8 @@ export interface SavedOpenTab {
customTitle?: boolean;
connectionId: string;
database: string;
/** 原连接已被删除但页签被保留时记录的原连接名,用于新建同名连接后重新绑定。 */
detachedConnectionName?: string;
catalog?: string;
schema?: string;
sql: string;
@@ -163,6 +165,7 @@ export function serializeOpenTabs(tabs: QueryTab[]): SavedOpenTab[] {
...(tab.customTitle ? { customTitle: true } : {}),
connectionId: tab.connectionId,
database: tab.database,
...(tab.detachedConnectionName ? { detachedConnectionName: tab.detachedConnectionName } : {}),
...(tab.catalog !== undefined ? { catalog: tab.catalog } : {}),
schema: tab.schema,
sql: shouldPersistTabSql(tab) ? tab.sql : "",
@@ -300,3 +300,8 @@ const DRAFT_VISIBLE_SCHEMAS_PREFIX = "__visible_schema_draft_";
export function buildDraftVisibleSchemasConnectionId(seed: string): string {
return `${DRAFT_VISIBLE_SCHEMAS_PREFIX}${seed}`;
}
/** 可见 schema 选择器用的临时连接:只存在于弹窗交互期间,不是用户保存的连接。 */
export function isDraftVisibleSchemasConnectionId(connectionId: string): boolean {
return connectionId.startsWith(DRAFT_VISIBLE_SCHEMAS_PREFIX);
}
@@ -84,6 +84,8 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
"sidebarBrowseObjectsOnDatabaseActivation",
"openTabsRestoreMode",
"disconnectTabHandlingMode",
"deleteConnectionTabHandlingMode",
"rememberConnectionDatabaseOnDelete",
"dataTabReuseMode",
"openDataTabsNextToActive",
"prefillNewQueryWithSelect",
@@ -198,6 +198,8 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] =
{ id: "navigation-hidden-tables", category: "navigation", titleKey: "settings.sidebarHiddenTablePrefixes", descriptionKey: "settings.sidebarHiddenTablePrefixesDescription", targetId: "navigation" },
{ id: "navigation-table-page-size", category: "navigation", titleKey: "settings.sidebarTablePageSize", descriptionKey: "settings.sidebarTablePageSizeDescription", targetId: "navigation" },
{ id: "navigation-disconnect-tabs", category: "navigation", titleKey: "settings.disconnectTabHandlingMode", descriptionKey: "settings.disconnectTabHandlingModeDescription", targetId: "navigation" },
{ id: "navigation-delete-connection-tabs", category: "navigation", titleKey: "settings.deleteConnectionTabHandlingMode", descriptionKey: "settings.deleteConnectionTabHandlingModeDescription", targetId: "navigation" },
{ id: "navigation-remember-connection-database", category: "navigation", titleKey: "settings.rememberConnectionDatabaseOnDelete", descriptionKey: "settings.rememberConnectionDatabaseOnDeleteDescription", targetId: "navigation" },
{ id: "query-page-size", category: "data", titleKey: "settings.queryPageSize", descriptionKey: "settings.queryPageSizeDescription", targetId: "data" },
{ id: "data-page-size", category: "data", titleKey: "settings.tableOpenPageSize", descriptionKey: "settings.tableOpenPageSizeDescription", targetId: "data" },
{ id: "data-table-open-sort-mode", category: "data", titleKey: "settings.tableOpenSortMode", descriptionKey: "settings.tableOpenSortDescription", targetId: "data" },
@@ -121,6 +121,8 @@ const SETTINGS_TRANSFER_CATEGORY_KEYS: Record<SettingsTransferCategoryId, readon
"sidebarBrowseObjectsOnDatabaseActivation",
"openTabsRestoreMode",
"disconnectTabHandlingMode",
"deleteConnectionTabHandlingMode",
"rememberConnectionDatabaseOnDelete",
"dataTabReuseMode",
"openDataTabsNextToActive",
"clickTableNavigationTarget",
@@ -0,0 +1,11 @@
import { describe, expect, it } from "vitest";
import { deletedConnectionTabKeepMode } from "@/lib/tabs/deletedConnectionTabs";
describe("deletedConnectionTabKeepMode", () => {
it("maps each delete-time tab handling mode to its keep scope", () => {
expect(deletedConnectionTabKeepMode("close-tabs")).toBe("none");
expect(deletedConnectionTabKeepMode("keep-sql-tabs")).toBe("sql");
expect(deletedConnectionTabKeepMode("keep-pinned-sql-tabs")).toBe("pinned-sql");
expect(deletedConnectionTabKeepMode("keep-all-tabs")).toBe("all");
});
});
@@ -0,0 +1,21 @@
import type { DeleteConnectionTabHandlingMode } from "@/stores/settingsStore";
/**
* 删除连接时对已打开页签的保留范围。
*
* 与「断开连接」不同,删除后连接配置已从磁盘移除、无法再重连,因此被保留下来的
* SQL 页签只是草稿工作区;它们会记录原连接名,等新建同名连接时再重新绑定。
*
* - `none`:关闭全部页签
* - `sql`:保留全部 SQL 页签(清空结果),其余关闭
* - `pinned-sql`:只保留固定的 SQL 页签(清空结果),其余关闭
* - `all`:不关闭任何页签,保留 SQL 文本与当前结果,只回滚事务
*/
export type DeletedConnectionTabKeepMode = "none" | "sql" | "pinned-sql" | "all";
export function deletedConnectionTabKeepMode(mode: DeleteConnectionTabHandlingMode): DeletedConnectionTabKeepMode {
if (mode === "keep-sql-tabs") return "sql";
if (mode === "keep-pinned-sql-tabs") return "pinned-sql";
if (mode === "keep-all-tabs") return "all";
return "none";
}