From af40af603eae188228ffb4e854ac36be6b35aeab Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Wed, 3 Jun 2026 16:53:40 +0800 Subject: [PATCH] feat(editor): add configurable tab handling on disconnect --- .../editor/EditorSettingsDialog.vue | 57 +++++++++++++++++++ apps/desktop/src/i18n/locales/en.ts | 12 ++++ apps/desktop/src/i18n/locales/es.ts | 12 ++++ apps/desktop/src/i18n/locales/zh-CN.ts | 9 +++ apps/desktop/src/i18n/locales/zh-TW.ts | 9 +++ apps/desktop/src/stores/connectionStore.ts | 25 +++++++- apps/desktop/src/stores/queryStore.ts | 22 +++++++ apps/desktop/src/stores/settingsStore.ts | 27 +++++++++ 8 files changed, 171 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index f0b70d4d90..e9aea56230 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -43,6 +43,7 @@ import { type AiApiStyle, type EditorTheme, type DesktopIconTheme, + type DisconnectTabHandlingMode, } from "@/stores/settingsStore"; import { loadEditorTheme, editorFontTheme } from "@/lib/editorThemes"; import { isTauriRuntime } from "@/lib/tauriRuntime"; @@ -115,6 +116,9 @@ const editSidebarActivation = ref(settingsStore.editorSettings.sidebarActivation const editSidebarObjectDisplay = ref(settingsStore.editorSettings.sidebarObjectDisplay); const sidebarObjectDisplayHelp = ref<"grouped" | "simple" | null>(null); const editAutoSelectActiveSidebarNode = ref(settingsStore.editorSettings.autoSelectActiveSidebarNode); +const editDisconnectTabHandlingMode = ref( + settingsStore.editorSettings.disconnectTabHandlingMode, +); const editSidebarHiddenTablePrefixes = ref(settingsStore.editorSettings.sidebarHiddenTablePrefixes.join("\n")); const editSidebarHideTableComments = ref(settingsStore.editorSettings.sidebarHideTableComments); const editSidebarAllowHorizontalScroll = ref(settingsStore.editorSettings.sidebarAllowHorizontalScroll); @@ -123,6 +127,16 @@ const systemFonts = ref([]); const systemFontsLoading = ref(false); const systemFontsLoaded = ref(false); const uiScaleOptions = [0.75, 0.9, 1, 1.1, 1.25, 1.5, 1.75, 2]; +const disconnectTabHandlingModeDescriptionKey = computed(() => { + switch (editDisconnectTabHandlingMode.value) { + case "close-tabs": + return "disconnectTabHandlingModeCloseTabsDescription"; + case "keep-tabs-clear-results": + return "disconnectTabHandlingModeKeepTabsClearResultsDescription"; + case "keep-tabs-keep-results": + return "disconnectTabHandlingModeKeepTabsKeepResultsDescription"; + } +}); // --- Snippet state --- const editSnippets = ref(settingsStore.editorSettings.snippets.map((s) => ({ ...s }))); @@ -251,6 +265,7 @@ watch( editSidebarActivation.value = settingsStore.editorSettings.sidebarActivation; editSidebarObjectDisplay.value = settingsStore.editorSettings.sidebarObjectDisplay; editAutoSelectActiveSidebarNode.value = settingsStore.editorSettings.autoSelectActiveSidebarNode; + editDisconnectTabHandlingMode.value = settingsStore.editorSettings.disconnectTabHandlingMode; editSidebarHiddenTablePrefixes.value = settingsStore.editorSettings.sidebarHiddenTablePrefixes.join("\n"); editSidebarHideTableComments.value = settingsStore.editorSettings.sidebarHideTableComments; editSidebarAllowHorizontalScroll.value = settingsStore.editorSettings.sidebarAllowHorizontalScroll; @@ -291,6 +306,7 @@ function hasChanges(): boolean { editSidebarActivation.value !== settingsStore.editorSettings.sidebarActivation || editSidebarObjectDisplay.value !== settingsStore.editorSettings.sidebarObjectDisplay || editAutoSelectActiveSidebarNode.value !== settingsStore.editorSettings.autoSelectActiveSidebarNode || + editDisconnectTabHandlingMode.value !== settingsStore.editorSettings.disconnectTabHandlingMode || editSidebarHideTableComments.value !== settingsStore.editorSettings.sidebarHideTableComments || editSidebarAllowHorizontalScroll.value !== settingsStore.editorSettings.sidebarAllowHorizontalScroll || JSON.stringify(normalizeSidebarHiddenTablePrefixes(editSidebarHiddenTablePrefixes.value)) !== @@ -318,6 +334,7 @@ async function persistSettings() { sidebarActivation: editSidebarActivation.value, sidebarObjectDisplay: editSidebarObjectDisplay.value, autoSelectActiveSidebarNode: editAutoSelectActiveSidebarNode.value, + disconnectTabHandlingMode: editDisconnectTabHandlingMode.value, sidebarHideTableComments: editSidebarHideTableComments.value, sidebarAllowHorizontalScroll: editSidebarAllowHorizontalScroll.value, sidebarHiddenTablePrefixes: normalizeSidebarHiddenTablePrefixes(editSidebarHiddenTablePrefixes.value), @@ -358,6 +375,7 @@ function resetDefaults() { editSidebarActivation.value = DEFAULT_EDITOR_SETTINGS.sidebarActivation; editSidebarObjectDisplay.value = DEFAULT_EDITOR_SETTINGS.sidebarObjectDisplay; editAutoSelectActiveSidebarNode.value = DEFAULT_EDITOR_SETTINGS.autoSelectActiveSidebarNode; + editDisconnectTabHandlingMode.value = DEFAULT_EDITOR_SETTINGS.disconnectTabHandlingMode; editSidebarHideTableComments.value = DEFAULT_EDITOR_SETTINGS.sidebarHideTableComments; editSidebarAllowHorizontalScroll.value = DEFAULT_EDITOR_SETTINGS.sidebarAllowHorizontalScroll; editSidebarHiddenTablePrefixes.value = DEFAULT_EDITOR_SETTINGS.sidebarHiddenTablePrefixes.join("\n"); @@ -376,6 +394,12 @@ function onThemeChange(v: any) { if (typeof v === "string") editTheme.value = v as typeof DEFAULT_EDITOR_SETTINGS.theme; } +function onDisconnectTabHandlingModeChange(v: any) { + if (v === "close-tabs" || v === "keep-tabs-clear-results" || v === "keep-tabs-keep-results") { + editDisconnectTabHandlingMode.value = v; + } +} + function onLocaleChange(v: any) { if (typeof v === "string") void setLocale(v as Locale); } @@ -1521,6 +1545,39 @@ watch( +
+
+ + + + + + + {{ t("settings.disconnectTabHandlingModeDescription") }} + + +
+ +

+ {{ t(`settings.${disconnectTabHandlingModeDescriptionKey}`) }} +

+
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 3789679efa..662fe32db2 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -1612,6 +1612,18 @@ export default { autoSelectActiveSidebarNode: "Always select opened item", autoSelectActiveSidebarNodeDescription: "When switching tabs, select the matching visible table, collection, or SQL file in the sidebar.", + disconnectTabHandlingMode: "Query tabs after disconnect", + disconnectTabHandlingModeDescription: + "Choose how query tabs and result grids should be handled after closing a connection or database.", + disconnectTabHandlingModeCloseTabs: "Close all tabs", + disconnectTabHandlingModeCloseTabsDescription: + "Close related query tabs entirely, including both SQL text and result grids.", + disconnectTabHandlingModeKeepTabsClearResults: "Close result grids, keep SQL tabs", + disconnectTabHandlingModeKeepTabsClearResultsDescription: + "Close result grids while keeping query tabs and SQL text, and also clear execution state and result sessions.", + disconnectTabHandlingModeKeepTabsKeepResults: "Do not close tabs or result grids", + disconnectTabHandlingModeKeepTabsKeepResultsDescription: + "Keep query tabs, SQL text, and the current result grids without extra cleanup.", sidebarHiddenTablePrefixes: "Hidden table name prefixes", sidebarHiddenTablePrefixesDescription: "One prefix per line. Only sidebar table, view, and collection labels are shortened; tooltips and actions still use the full name.", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 221bce7325..d9a1a7e60e 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -1504,6 +1504,18 @@ export default { autoSelectActiveSidebarNode: "Seleccionar siempre el elemento abierto", autoSelectActiveSidebarNodeDescription: "Al cambiar de pestaña, selecciona la tabla, colección o archivo SQL visible correspondiente en la barra lateral.", + disconnectTabHandlingMode: "Pestañas de consulta tras desconectar", + disconnectTabHandlingModeDescription: + "Elige cómo conservar las pestañas de consulta y las tablas de resultados al cerrar una conexión o base de datos.", + disconnectTabHandlingModeCloseTabs: "Cerrar todas las pestañas", + disconnectTabHandlingModeCloseTabsDescription: + "Cierra por completo las pestañas de consulta relacionadas, incluyendo el SQL y las tablas de resultados.", + disconnectTabHandlingModeKeepTabsClearResults: "Cerrar resultados y conservar pestañas SQL", + disconnectTabHandlingModeKeepTabsClearResultsDescription: + "Cierra las tablas de resultados, conserva las pestañas y el texto SQL, y también limpia el estado de ejecución y las sesiones.", + disconnectTabHandlingModeKeepTabsKeepResults: "No cerrar pestañas ni resultados", + disconnectTabHandlingModeKeepTabsKeepResultsDescription: + "Conserva las pestañas de consulta, el texto SQL y las tablas de resultados actuales sin limpieza adicional.", sidebarHiddenTablePrefixes: "Prefijos ocultos de tablas", sidebarHiddenTablePrefixesDescription: "Un prefijo por linea. Solo acorta etiquetas de tablas, vistas y colecciones en la barra lateral; las acciones y ayudas usan el nombre completo.", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index c4c93893dd..7714bd6515 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -1579,6 +1579,15 @@ export default { sidebarObjectDisplaySimpleDescription: "数据库或 schema 下直接平铺表和视图,减少中间层级。", autoSelectActiveSidebarNode: "始终选中已打开项目", autoSelectActiveSidebarNodeDescription: "切换标签页时,在侧边栏选中匹配的可见表、集合或 SQL 文件。", + disconnectTabHandlingMode: "关闭连接后的查询页签处理", + disconnectTabHandlingModeDescription: "选择关闭连接或数据库后,查询页签和结果表格的保留策略。", + disconnectTabHandlingModeCloseTabs: "关闭全部页签", + disconnectTabHandlingModeCloseTabsDescription: "直接关闭对应的查询页签,页签中的 SQL 和结果表格都会一起移除。", + disconnectTabHandlingModeKeepTabsClearResults: "关闭结果表格,保留 SQL 页签", + disconnectTabHandlingModeKeepTabsClearResultsDescription: + "关闭结果表格,保留查询页签和 SQL 文本,同时清理执行状态与结果会话。", + disconnectTabHandlingModeKeepTabsKeepResults: "不关闭页签和结果表格", + disconnectTabHandlingModeKeepTabsKeepResultsDescription: "保留查询页签、SQL 文本和当前结果表格,不做额外处理。", sidebarHiddenTablePrefixes: "隐藏表名前缀", sidebarHiddenTablePrefixesDescription: "每行一个前缀,仅影响侧边栏表、视图和集合的显示名称,悬浮提示和实际操作仍使用完整名称。", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 90cc15c87a..1af06234d9 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -1566,6 +1566,15 @@ export default { sidebarObjectDisplaySimpleDescription: "直接在每個資料庫或 schema 下列出資料表與檢視,減少中間層級。", autoSelectActiveSidebarNode: "一律選取已開啟項目", autoSelectActiveSidebarNodeDescription: "切換分頁時,在側邊欄選取相符的可見資料表、集合或 SQL 檔案。", + disconnectTabHandlingMode: "關閉連線後的查詢分頁處理", + disconnectTabHandlingModeDescription: "選擇關閉連線或資料庫後,查詢分頁與結果表格的保留策略。", + disconnectTabHandlingModeCloseTabs: "關閉全部分頁", + disconnectTabHandlingModeCloseTabsDescription: "直接關閉對應查詢分頁,分頁中的 SQL 與結果表格都會一併移除。", + disconnectTabHandlingModeKeepTabsClearResults: "關閉結果表格,保留 SQL 分頁", + disconnectTabHandlingModeKeepTabsClearResultsDescription: + "關閉結果表格,保留查詢分頁與 SQL 文字,同時清理執行狀態與結果工作階段。", + disconnectTabHandlingModeKeepTabsKeepResults: "不關閉分頁和結果表格", + disconnectTabHandlingModeKeepTabsKeepResultsDescription: "保留查詢分頁、SQL 文字與目前結果表格,不另外做清理。", sidebarHiddenTablePrefixes: "隱藏資料表名稱字首", sidebarHiddenTablePrefixesDescription: "每行一個字首。只縮短側邊欄中的資料表、檢視和集合標籤;工具提示和實際操作仍使用完整名稱。", diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index 6ea3b96220..359bd3d0d9 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -78,6 +78,7 @@ function redisDbLabel(db: number, loadedKeyCount?: number, totalKeyCount?: numbe } export const useConnectionStore = defineStore("connection", () => { + const settingsStore = useSettingsStore(); const connections = ref([]); const isDesktop = isTauriRuntime(); const activeConnectionId = ref(localStorage.getItem(ACTIVE_CONNECTION_STORAGE_KEY)); @@ -701,7 +702,17 @@ export const useConnectionStore = defineStore("connection", () => { const shouldRemoveOneTimeConnection = getConfig(connectionId)?.one_time === true; await api.disconnectDb(connectionId); const { useQueryStore } = await import("@/stores/queryStore"); - useQueryStore().closeConnectionTabs(connectionId); + const queryStore = useQueryStore(); + switch (settingsStore.editorSettings.disconnectTabHandlingMode) { + case "close-tabs": + queryStore.closeConnectionTabs(connectionId); + break; + case "keep-tabs-clear-results": + queryStore.releaseConnectionTabs(connectionId); + break; + case "keep-tabs-keep-results": + break; + } connectedIds.value.delete(connectionId); const node = findNode(treeNodes.value, connectionId); if (node) { @@ -721,7 +732,17 @@ export const useConnectionStore = defineStore("connection", () => { async function closeDatabaseConnection(connectionId: string, database: string) { await api.closeDatabaseConnection(connectionId, database); const { useQueryStore } = await import("@/stores/queryStore"); - useQueryStore().closeDatabaseTabs(connectionId, database); + const queryStore = useQueryStore(); + switch (settingsStore.editorSettings.disconnectTabHandlingMode) { + case "close-tabs": + queryStore.closeDatabaseTabs(connectionId, database); + break; + case "keep-tabs-clear-results": + queryStore.releaseDatabaseTabs(connectionId, database); + break; + case "keep-tabs-keep-results": + break; + } const node = findDatabaseTreeNode(treeNodes.value, connectionId, database); if (node) { node.isExpanded = false; diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index 18fecb3aa6..b6743a770c 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -388,6 +388,26 @@ export const useQueryStore = defineStore("query", () => { closeTabsWhere((tab) => tab.connectionId === connectionId && tab.database === database); } + function releaseTabsWhere(predicate: (tab: QueryTab) => boolean) { + tabs.value + .filter((tab) => predicate(tab)) + .forEach((tab) => { + if (tab.isExecuting) void cancelTabExecution(tab.id); + if (tab.isExplaining) void cancelTabExplain(tab.id); + void closeResultSession(tab); + void closeClientConnectionSession(tab); + clearResultPayload(tab); + }); + } + + function releaseConnectionTabs(connectionId: string) { + releaseTabsWhere((tab) => tab.connectionId === connectionId); + } + + function releaseDatabaseTabs(connectionId: string, database: string) { + releaseTabsWhere((tab) => tab.connectionId === connectionId && tab.database === database); + } + function updateSql(id: string, sql: string) { const tab = tabs.value.find((t) => t.id === id); if (tab) { @@ -1328,6 +1348,8 @@ export const useQueryStore = defineStore("query", () => { closeAllTabs, closeConnectionTabs, closeDatabaseTabs, + releaseConnectionTabs, + releaseDatabaseTabs, updateSql, renameTab, openObjectBrowser, diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts index 7215d10c8e..13eba9585c 100644 --- a/apps/desktop/src/stores/settingsStore.ts +++ b/apps/desktop/src/stores/settingsStore.ts @@ -187,6 +187,8 @@ const CELL_DETAIL_PANEL_LAYOUTS = ["bottom", "right"] as const; export type CellDetailPanelLayout = (typeof CELL_DETAIL_PANEL_LAYOUTS)[number]; const DATA_GRID_RENDER_MODES = ["dom", "canvas"] as const; export type DataGridRenderMode = (typeof DATA_GRID_RENDER_MODES)[number]; +const DISCONNECT_TAB_HANDLING_MODES = ["close-tabs", "keep-tabs-clear-results", "keep-tabs-keep-results"] as const; +export type DisconnectTabHandlingMode = (typeof DISCONNECT_TAB_HANDLING_MODES)[number]; export interface EditorSettings { fontFamily: string; @@ -211,6 +213,7 @@ export interface EditorSettings { sidebarActivation: SidebarActivation; sidebarObjectDisplay: "grouped" | "simple"; autoSelectActiveSidebarNode: boolean; + disconnectTabHandlingMode: DisconnectTabHandlingMode; sidebarHiddenTablePrefixes: string[]; sidebarHideTableComments: boolean; sidebarAllowHorizontalScroll: boolean; @@ -267,6 +270,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = { sidebarActivation: "single", sidebarObjectDisplay: "grouped", autoSelectActiveSidebarNode: false, + disconnectTabHandlingMode: "close-tabs", sidebarHiddenTablePrefixes: [], sidebarHideTableComments: false, sidebarAllowHorizontalScroll: false, @@ -308,6 +312,21 @@ function normalizeDataGridRenderMode(value: unknown): DataGridRenderMode { : DEFAULT_EDITOR_SETTINGS.dataGridRenderMode; } +function normalizeDisconnectTabHandlingMode( + value: unknown, + legacyCloseTabsOnDisconnect?: unknown, +): DisconnectTabHandlingMode { + if (DISCONNECT_TAB_HANDLING_MODES.includes(value as DisconnectTabHandlingMode)) { + return value as DisconnectTabHandlingMode; + } + if (value === "clear-state") return "keep-tabs-clear-results"; + if (value === "keep-tabs") return "keep-tabs-keep-results"; + if (typeof legacyCloseTabsOnDisconnect === "boolean") { + return legacyCloseTabsOnDisconnect ? "close-tabs" : "keep-tabs-clear-results"; + } + return DEFAULT_EDITOR_SETTINGS.disconnectTabHandlingMode; +} + function normalizeColumnFormatters(value: unknown): Record { if (!value || typeof value !== "object" || Array.isArray(value)) return {}; const formatters: Record = {}; @@ -395,6 +414,10 @@ export function normalizeEditorSettings(settings: Partial, exist : DEFAULT_EDITOR_SETTINGS.sidebarObjectDisplay, autoSelectActiveSidebarNode: settings.autoSelectActiveSidebarNode ?? DEFAULT_EDITOR_SETTINGS.autoSelectActiveSidebarNode, + disconnectTabHandlingMode: normalizeDisconnectTabHandlingMode( + (settings as Partial).disconnectTabHandlingMode, + (settings as Partial & { closeQueryTabsOnDisconnect?: boolean }).closeQueryTabsOnDisconnect, + ), sidebarHiddenTablePrefixes: normalizeSidebarHiddenTablePrefixes(settings.sidebarHiddenTablePrefixes), sidebarHideTableComments: settings.sidebarHideTableComments ?? DEFAULT_EDITOR_SETTINGS.sidebarHideTableComments, sidebarAllowHorizontalScroll: @@ -532,6 +555,10 @@ export const useSettingsStore = defineStore("settings", () => { editorSettings.value.sidebarObjectDisplay = partial.sidebarObjectDisplay; if (partial.autoSelectActiveSidebarNode !== undefined) editorSettings.value.autoSelectActiveSidebarNode = partial.autoSelectActiveSidebarNode; + if (partial.disconnectTabHandlingMode !== undefined) + editorSettings.value.disconnectTabHandlingMode = normalizeDisconnectTabHandlingMode( + partial.disconnectTabHandlingMode, + ); if (partial.sidebarHiddenTablePrefixes !== undefined) editorSettings.value.sidebarHiddenTablePrefixes = normalizeSidebarHiddenTablePrefixes( partial.sidebarHiddenTablePrefixes,