feat(editor): add configurable tab handling on disconnect

This commit is contained in:
t8y2
2026-06-03 16:53:40 +08:00
parent 29118700b6
commit af40af603e
8 changed files with 171 additions and 2 deletions
@@ -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<DisconnectTabHandlingMode>(
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<string[]>([]);
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<SqlSnippet[]>(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(
</div>
<Switch id="auto-select-active-sidebar-node" v-model="editAutoSelectActiveSidebarNode" />
</div>
<div class="space-y-2 rounded-md border bg-muted/20 px-3 py-2">
<div class="flex items-center gap-2">
<Label for="disconnect-tab-handling-mode">{{ t("settings.disconnectTabHandlingMode") }}</Label>
<Tooltip>
<TooltipTrigger as-child>
<CircleHelp class="h-3.5 w-3.5 cursor-help text-muted-foreground hover:text-foreground" />
</TooltipTrigger>
<TooltipContent class="max-w-[320px] text-xs leading-relaxed" side="top" align="start">
{{ t("settings.disconnectTabHandlingModeDescription") }}
</TooltipContent>
</Tooltip>
</div>
<Select
:model-value="editDisconnectTabHandlingMode"
@update:model-value="onDisconnectTabHandlingModeChange"
>
<SelectTrigger id="disconnect-tab-handling-mode" class="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="close-tabs">{{ t("settings.disconnectTabHandlingModeCloseTabs") }}</SelectItem>
<SelectItem value="keep-tabs-clear-results">
{{ t("settings.disconnectTabHandlingModeKeepTabsClearResults") }}
</SelectItem>
<SelectItem value="keep-tabs-keep-results">
{{ t("settings.disconnectTabHandlingModeKeepTabsKeepResults") }}
</SelectItem>
</SelectContent>
</Select>
<p class="text-xs text-muted-foreground">
{{ t(`settings.${disconnectTabHandlingModeDescriptionKey}`) }}
</p>
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="flex items-center gap-2">
<Label for="sidebar-hide-table-comments">{{ t("settings.sidebarHideTableComments") }}</Label>
+12
View File
@@ -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.",
+12
View File
@@ -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.",
+9
View File
@@ -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:
"每行一个前缀,仅影响侧边栏表、视图和集合的显示名称,悬浮提示和实际操作仍使用完整名称。",
+9
View File
@@ -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:
"每行一個字首。只縮短側邊欄中的資料表、檢視和集合標籤;工具提示和實際操作仍使用完整名稱。",
+23 -2
View File
@@ -78,6 +78,7 @@ function redisDbLabel(db: number, loadedKeyCount?: number, totalKeyCount?: numbe
}
export const useConnectionStore = defineStore("connection", () => {
const settingsStore = useSettingsStore();
const connections = ref<ConnectionConfig[]>([]);
const isDesktop = isTauriRuntime();
const activeConnectionId = ref<string | null>(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;
+22
View File
@@ -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,
+27
View File
@@ -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<string, ColumnFormatterConfig> {
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
const formatters: Record<string, ColumnFormatterConfig> = {};
@@ -395,6 +414,10 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
: DEFAULT_EDITOR_SETTINGS.sidebarObjectDisplay,
autoSelectActiveSidebarNode:
settings.autoSelectActiveSidebarNode ?? DEFAULT_EDITOR_SETTINGS.autoSelectActiveSidebarNode,
disconnectTabHandlingMode: normalizeDisconnectTabHandlingMode(
(settings as Partial<EditorSettings>).disconnectTabHandlingMode,
(settings as Partial<EditorSettings> & { 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,