From 47dc35ccb2a5b9efe60cb77cbc162c849a8daf23 Mon Sep 17 00:00:00 2001 From: zipg Date: Thu, 2 Jul 2026 12:23:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(settings):=20=E5=90=AF=E5=8A=A8=E6=97=B6?= =?UTF-8?q?=E6=A0=87=E7=AD=BE=E9=A1=B5=E6=81=A2=E5=A4=8D=E7=AD=96=E7=95=A5?= =?UTF-8?q?=E4=B8=89=E9=80=89=E9=A1=B9=20(all/pinned/none)=20(#2395)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(settings): 添加启动标签页恢复策略 * i18n: 补齐启动标签页恢复策略 5 个 locale 译文 --------- Co-authored-by: staff Co-authored-by: t8y2 <1156263951@qq.com> --- .../editor/EditorSettingsDialog.vue | 28 ++++++++ apps/desktop/src/i18n/locales/en.ts | 6 ++ apps/desktop/src/i18n/locales/es.ts | 6 ++ apps/desktop/src/i18n/locales/it.ts | 6 ++ apps/desktop/src/i18n/locales/ja.ts | 6 ++ apps/desktop/src/i18n/locales/pt-BR.ts | 6 ++ apps/desktop/src/i18n/locales/zh-CN.ts | 6 ++ apps/desktop/src/i18n/locales/zh-TW.ts | 6 ++ apps/desktop/src/lib/openTabsPersistence.ts | 13 +++- .../queryStore.database-open.spec.ts | 64 +++++++++++++++++++ .../stores/__tests__/settingsStore.spec.ts | 15 +++++ apps/desktop/src/stores/queryStore.ts | 16 +++-- apps/desktop/src/stores/settingsStore.ts | 11 ++++ .../app-tests/openTabsPersistence.test.ts | 12 ++++ 14 files changed, 193 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index cf62a47f5..b212bcbb6 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -34,6 +34,7 @@ import { type DesktopIconTheme, type InterfaceLayout, type DisconnectTabHandlingMode, + type OpenTabsRestoreMode, type SqlSemanticDiagnosticsMode, type UpdateDownloadSource, type CustomThemeColors, @@ -241,6 +242,7 @@ 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 editOpenTabsRestoreMode = ref(settingsStore.editorSettings.openTabsRestoreMode); const editDisconnectTabHandlingMode = ref(settingsStore.editorSettings.disconnectTabHandlingMode); const editReuseDataTab = ref(settingsStore.editorSettings.reuseDataTab); const editUpdateNotificationsEnabled = ref(settingsStore.editorSettings.updateNotificationsEnabled); @@ -512,6 +514,7 @@ watch( editSidebarActivation.value = settingsStore.editorSettings.sidebarActivation; editSidebarObjectDisplay.value = settingsStore.editorSettings.sidebarObjectDisplay; editAutoSelectActiveSidebarNode.value = settingsStore.editorSettings.autoSelectActiveSidebarNode; + editOpenTabsRestoreMode.value = settingsStore.editorSettings.openTabsRestoreMode; editDisconnectTabHandlingMode.value = settingsStore.editorSettings.disconnectTabHandlingMode; editReuseDataTab.value = settingsStore.editorSettings.reuseDataTab; editUpdateNotificationsEnabled.value = settingsStore.editorSettings.updateNotificationsEnabled; @@ -577,6 +580,7 @@ function hasChanges(): boolean { editSidebarActivation.value !== settingsStore.editorSettings.sidebarActivation || editSidebarObjectDisplay.value !== settingsStore.editorSettings.sidebarObjectDisplay || editAutoSelectActiveSidebarNode.value !== settingsStore.editorSettings.autoSelectActiveSidebarNode || + editOpenTabsRestoreMode.value !== settingsStore.editorSettings.openTabsRestoreMode || editDisconnectTabHandlingMode.value !== settingsStore.editorSettings.disconnectTabHandlingMode || editReuseDataTab.value !== settingsStore.editorSettings.reuseDataTab || editUpdateNotificationsEnabled.value !== settingsStore.editorSettings.updateNotificationsEnabled || @@ -623,6 +627,7 @@ async function persistSettings() { sidebarActivation: editSidebarActivation.value, sidebarObjectDisplay: editSidebarObjectDisplay.value, autoSelectActiveSidebarNode: editAutoSelectActiveSidebarNode.value, + openTabsRestoreMode: editOpenTabsRestoreMode.value, disconnectTabHandlingMode: editDisconnectTabHandlingMode.value, reuseDataTab: editReuseDataTab.value, updateNotificationsEnabled: editUpdateNotificationsEnabled.value, @@ -692,6 +697,7 @@ function resetDefaultsForTab(tab: SettingsCategory) { editSidebarActivation.value = DEFAULT_EDITOR_SETTINGS.sidebarActivation; editSidebarObjectDisplay.value = DEFAULT_EDITOR_SETTINGS.sidebarObjectDisplay; editAutoSelectActiveSidebarNode.value = DEFAULT_EDITOR_SETTINGS.autoSelectActiveSidebarNode; + editOpenTabsRestoreMode.value = DEFAULT_EDITOR_SETTINGS.openTabsRestoreMode; editDisconnectTabHandlingMode.value = DEFAULT_EDITOR_SETTINGS.disconnectTabHandlingMode; editReuseDataTab.value = DEFAULT_EDITOR_SETTINGS.reuseDataTab; editUpdateNotificationsEnabled.value = DEFAULT_EDITOR_SETTINGS.updateNotificationsEnabled; @@ -754,6 +760,7 @@ function resetAllDefaults() { editSidebarActivation.value = DEFAULT_EDITOR_SETTINGS.sidebarActivation; editSidebarObjectDisplay.value = DEFAULT_EDITOR_SETTINGS.sidebarObjectDisplay; editAutoSelectActiveSidebarNode.value = DEFAULT_EDITOR_SETTINGS.autoSelectActiveSidebarNode; + editOpenTabsRestoreMode.value = DEFAULT_EDITOR_SETTINGS.openTabsRestoreMode; editDisconnectTabHandlingMode.value = DEFAULT_EDITOR_SETTINGS.disconnectTabHandlingMode; editReuseDataTab.value = DEFAULT_EDITOR_SETTINGS.reuseDataTab; editUpdateNotificationsEnabled.value = DEFAULT_EDITOR_SETTINGS.updateNotificationsEnabled; @@ -2551,6 +2558,27 @@ onUnmounted(cleanupPreviewEditor); +
+
+ + + {{ t("settings.openTabsRestoreModeDescription") }} + +
+ +

+ {{ t("settings.openTabsRestoreModeHint") }} +

+
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 7c1f4e5b9..7ff986eac 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -2645,6 +2645,12 @@ export default { sidebarObjectDisplaySimpleDescription: "List tables and views directly under each database or schema to reduce intermediate levels.", autoSelectActiveSidebarNode: "Always select opened item", autoSelectActiveSidebarNodeDescription: "When switching tabs, select the matching visible table, collection, or SQL file in the sidebar.", + openTabsRestoreMode: "Restore tabs on launch", + openTabsRestoreModeDescription: "Choose how DBX restores tabs that were open last time.", + openTabsRestoreModeAll: "Keep all tabs", + openTabsRestoreModePinned: "Keep pinned tabs only", + openTabsRestoreModeNone: "Do not keep tabs", + openTabsRestoreModeHint: "Pinned tabs stay on the left side of the tab bar and work well as frequent entry points.", disconnectTabHandlingMode: "Tabs after disconnect", disconnectTabHandlingModeDescription: "Choose how related tabs should be handled after closing a connection or database.", disconnectTabHandlingModeCloseTabs: "Close all tabs", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index da30bcd4b..541302353 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -2626,6 +2626,12 @@ export default withEnglishFallback({ sidebarObjectDisplaySimpleDescription: "Muestra tablas y vistas directamente bajo cada base de datos o esquema para reducir niveles intermedios.", 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.", + openTabsRestoreMode: "Restaurar pestañas al iniciar", + openTabsRestoreModeDescription: "Elige cómo DBX restaura las pestañas que estaban abiertas la última vez.", + openTabsRestoreModeAll: "Conservar todas las pestañas", + openTabsRestoreModePinned: "Solo pestañas fijadas", + openTabsRestoreModeNone: "No conservar pestañas", + openTabsRestoreModeHint: "Las pestañas fijadas permanecen en el lado izquierdo de la barra de pestañas y son útiles como puntos de entrada frecuentes.", disconnectTabHandlingMode: "Pestañas tras desconectar", disconnectTabHandlingModeDescription: "Elige cómo conservar las pestañas relacionadas al cerrar una conexión o base de datos.", disconnectTabHandlingModeCloseTabs: "Cerrar todas las pestañas", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 677fd5418..8ee853ff1 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -2646,6 +2646,12 @@ export default withEnglishFallback({ sidebarObjectDisplaySimpleDescription: "Elenca tabelle e viste direttamente sotto ogni database o schema per ridurre i livelli intermedi.", autoSelectActiveSidebarNode: "Seleziona sempre l'elemento aperto", autoSelectActiveSidebarNodeDescription: "Quando passi da una scheda all'altra, seleziona la tabella, la collezione o il file SQL visibile corrispondente nella barra laterale.", + openTabsRestoreMode: "Ripristina schede all'avvio", + openTabsRestoreModeDescription: "Scegli come DBX ripristina le schede aperte l'ultima volta.", + openTabsRestoreModeAll: "Mantieni tutte le schede", + openTabsRestoreModePinned: "Solo schede fissate", + openTabsRestoreModeNone: "Non mantenere schede", + openTabsRestoreModeHint: "Le schede fissate restano sul lato sinistro della barra delle schede e sono utili come punti di accesso frequenti.", disconnectTabHandlingMode: "Schede dopo la disconnessione", disconnectTabHandlingModeDescription: "Scegli come gestire le schede correlate dopo la chiusura di una connessione o di un database.", disconnectTabHandlingModeCloseTabs: "Chiudi tutte le schede", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 0db74cd62..8ebe4edc6 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -2619,6 +2619,12 @@ export default withEnglishFallback({ sidebarObjectDisplaySimpleDescription: "中間レベルを減らすために、各データベースまたはスキーマの直下にテーブルとビューを一覧表示します。", autoSelectActiveSidebarNode: "開いた項目を常に選択", autoSelectActiveSidebarNodeDescription: "タブ切替時に、サイドバーで一致する表示中のテーブル、コレクション、またはSQLファイルを選択します。", + openTabsRestoreMode: "起動時にタブを復元", + openTabsRestoreModeDescription: "DBX 起動時に前回開いていたタブをどのように復元するかを選択します。", + openTabsRestoreModeAll: "すべてのタブを保持", + openTabsRestoreModePinned: "ピン留めタブのみ保持", + openTabsRestoreModeNone: "タブを保持しない", + openTabsRestoreModeHint: "ピン留めタブはタブバーの左側に配置され、頻繁に使う作業の入り口として便利です。", disconnectTabHandlingMode: "切断後のタブ", disconnectTabHandlingModeDescription: "接続またはデータベースを閉じた後の関連タブの処理方法を選択します。", disconnectTabHandlingModeCloseTabs: "すべてのタブを閉じる", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index e6fdcabc9..e5da08355 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -2638,6 +2638,12 @@ export default withEnglishFallback({ sidebarObjectDisplaySimpleDescription: "Listar tabelas e views diretamente sob cada banco de dados ou schema para reduzir os níveis intermediários.", autoSelectActiveSidebarNode: "Sempre selecionar o item aberto", autoSelectActiveSidebarNodeDescription: "Ao alternar abas, selecionar a tabela, coleção ou arquivo SQL correspondente visível na barra lateral.", + openTabsRestoreMode: "Restaurar abas ao iniciar", + openTabsRestoreModeDescription: "Escolha como o DBX restaura as abas que estavam abertas na última vez.", + openTabsRestoreModeAll: "Manter todas as abas", + openTabsRestoreModePinned: "Apenas abas fixadas", + openTabsRestoreModeNone: "Não manter abas", + openTabsRestoreModeHint: "As abas fixadas permanecem no lado esquerdo da barra de abas e funcionam bem como pontos de entrada frequentes.", disconnectTabHandlingMode: "Abas após desconectar", disconnectTabHandlingModeDescription: "Escolha como as abas relacionadas devem ser tratadas após fechar uma conexão ou banco de dados.", disconnectTabHandlingModeCloseTabs: "Fechar todas as abas", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 02f34f9ff..7bf4c9f6b 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -2652,6 +2652,12 @@ export default withEnglishFallback({ sidebarObjectDisplaySimpleDescription: "数据库或 schema 下直接平铺表和视图,减少中间层级。", autoSelectActiveSidebarNode: "始终选中已打开项目", autoSelectActiveSidebarNodeDescription: "切换标签页时,在侧边栏选中匹配的可见表、集合或 SQL 文件。", + openTabsRestoreMode: "启动时恢复标签页", + openTabsRestoreModeDescription: "选择 DBX 启动时如何恢复上次打开的标签页。", + openTabsRestoreModeAll: "保留所有标签", + openTabsRestoreModePinned: "仅保留固定标签", + openTabsRestoreModeNone: "不保留标签", + openTabsRestoreModeHint: "固定标签会显示在标签栏左侧,适合作为常用工作入口。", disconnectTabHandlingMode: "关闭连接后的标签页处理", disconnectTabHandlingModeDescription: "选择关闭连接或数据库后,相关标签页的保留策略。", disconnectTabHandlingModeCloseTabs: "关闭全部页签", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index c903e4b14..bef2f0ca2 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -2525,6 +2525,12 @@ export default withEnglishFallback({ sidebarObjectDisplaySimpleDescription: "直接在每個資料庫或 schema 下列出資料表與檢視,減少中間層級。", autoSelectActiveSidebarNode: "一律選取已開啟項目", autoSelectActiveSidebarNodeDescription: "切換分頁時,在側邊欄選取相符的可見資料表、集合或 SQL 檔案。", + openTabsRestoreMode: "啟動時還原分頁", + openTabsRestoreModeDescription: "選擇 DBX 啟動時如何還原上次開啟的分頁。", + openTabsRestoreModeAll: "保留所有分頁", + openTabsRestoreModePinned: "僅保留置頂分頁", + openTabsRestoreModeNone: "不保留分頁", + openTabsRestoreModeHint: "置頂分頁會顯示在分頁列左側,適合作為常用工作入口。", disconnectTabHandlingMode: "關閉連線後的分頁處理", disconnectTabHandlingModeDescription: "選擇關閉連線或資料庫後,相關分頁的保留策略。", disconnectTabHandlingModeCloseTabs: "關閉全部分頁", diff --git a/apps/desktop/src/lib/openTabsPersistence.ts b/apps/desktop/src/lib/openTabsPersistence.ts index 37e38ae0d..3f1f2a7e1 100644 --- a/apps/desktop/src/lib/openTabsPersistence.ts +++ b/apps/desktop/src/lib/openTabsPersistence.ts @@ -1,5 +1,8 @@ import type { QueryTab } from "@/types/database"; +export const OPEN_TABS_STORAGE_KEY = "dbx-open-tabs"; +export const ACTIVE_TAB_STORAGE_KEY = "dbx-active-tab"; + export interface SavedQueryResultRun { id: string; title: string; @@ -55,6 +58,8 @@ export interface RestoredOpenTabs { activeTabId: string | null; } +export type OpenTabsRestoreFilter = "all" | "pinned"; + function shouldPersistTabSql(tab: QueryTab) { if (!tab.savedSqlId) return true; return tab.originalSql !== undefined && tab.sql !== tab.originalSql; @@ -120,7 +125,7 @@ function isSavedOpenTab(value: unknown): value is SavedOpenTab { return typeof tab.id === "string" && typeof tab.title === "string" && typeof tab.connectionId === "string" && typeof tab.database === "string" && (typeof tab.sql === "string" || typeof tab.savedSqlId === "string"); } -export function restoreOpenTabsState(rawTabs: string | null, rawActiveTabId: string | null, options: { queryOnly?: boolean } = {}): RestoredOpenTabs { +export function restoreOpenTabsState(rawTabs: string | null, rawActiveTabId: string | null, options: { queryOnly?: boolean; filter?: OpenTabsRestoreFilter } = {}): RestoredOpenTabs { if (!rawTabs) return { tabs: [], activeTabId: null }; try { @@ -128,7 +133,11 @@ export function restoreOpenTabsState(rawTabs: string | null, rawActiveTabId: str if (!Array.isArray(parsed)) return { tabs: [], activeTabId: null }; const saved = parsed.filter(isSavedOpenTab); - const filtered = options.queryOnly ? saved.filter((tab) => (tab.mode ?? "query") === "query") : saved; + const filtered = saved.filter((tab) => { + if (options.queryOnly && (tab.mode ?? "query") !== "query") return false; + if (options.filter === "pinned" && !tab.pinned) return false; + return true; + }); const tabs: QueryTab[] = filtered.map((tab) => { const mode = tab.mode ?? "query"; const resultRuns = diff --git a/apps/desktop/src/stores/__tests__/queryStore.database-open.spec.ts b/apps/desktop/src/stores/__tests__/queryStore.database-open.spec.ts index 47b2f88c3..38cfa3460 100644 --- a/apps/desktop/src/stores/__tests__/queryStore.database-open.spec.ts +++ b/apps/desktop/src/stores/__tests__/queryStore.database-open.spec.ts @@ -1,5 +1,6 @@ import { createPinia, setActivePinia } from "pinia"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import { ACTIVE_TAB_STORAGE_KEY, OPEN_TABS_STORAGE_KEY } from "@/lib/openTabsPersistence"; function installLocalStorage() { const data = new Map(); @@ -8,6 +9,7 @@ function installLocalStorage() { setItem: vi.fn((key: string, value: string) => data.set(key, value)), removeItem: vi.fn((key: string) => data.delete(key)), }); + return data; } describe("queryStore database open state", () => { @@ -37,4 +39,66 @@ describe("queryStore database open state", () => { expect(store.isDatabaseOpen("pg-1", "analytics")).toBe(false); }); + + it("does not restore open tabs when launch restore mode is none", async () => { + const persistedTabs = JSON.stringify([ + { + id: "tab-1", + title: "Query 1", + connectionId: "pg-1", + database: "app", + sql: "select 1", + }, + ]); + + vi.resetModules(); + vi.unstubAllGlobals(); + const storage = installLocalStorage(); + storage.set("dbx-editor-settings", JSON.stringify({ openTabsRestoreMode: "none" })); + storage.set(OPEN_TABS_STORAGE_KEY, persistedTabs); + storage.set(ACTIVE_TAB_STORAGE_KEY, "tab-1"); + setActivePinia(createPinia()); + + const { useQueryStore } = await import("@/stores/queryStore"); + const store = useQueryStore(); + + expect(store.tabs).toEqual([]); + expect(store.activeTabId).toBeNull(); + expect(storage.get(OPEN_TABS_STORAGE_KEY)).toBe(persistedTabs); + expect(storage.get(ACTIVE_TAB_STORAGE_KEY)).toBe("tab-1"); + }); + + it("restores only pinned tabs when launch restore mode is pinned", async () => { + const persistedTabs = JSON.stringify([ + { + id: "tab-1", + title: "Pinned", + connectionId: "pg-1", + database: "app", + sql: "select 1", + pinned: true, + }, + { + id: "tab-2", + title: "Regular", + connectionId: "pg-1", + database: "app", + sql: "select 2", + }, + ]); + + vi.resetModules(); + vi.unstubAllGlobals(); + const storage = installLocalStorage(); + storage.set("dbx-editor-settings", JSON.stringify({ openTabsRestoreMode: "pinned" })); + storage.set(OPEN_TABS_STORAGE_KEY, persistedTabs); + storage.set(ACTIVE_TAB_STORAGE_KEY, "tab-2"); + setActivePinia(createPinia()); + + const { useQueryStore } = await import("@/stores/queryStore"); + const store = useQueryStore(); + + expect(store.tabs.map((tab) => tab.id)).toEqual(["tab-1"]); + expect(store.activeTabId).toBe("tab-1"); + }); }); diff --git a/apps/desktop/src/stores/__tests__/settingsStore.spec.ts b/apps/desktop/src/stores/__tests__/settingsStore.spec.ts index 19782e107..e73a47e18 100644 --- a/apps/desktop/src/stores/__tests__/settingsStore.spec.ts +++ b/apps/desktop/src/stores/__tests__/settingsStore.spec.ts @@ -30,6 +30,21 @@ describe("normalizeEditorSettings", () => { expect(normalizeEditorSettings({}).updateDownloadSource).toBe("official"); }); + it("restores all open tabs on launch by default", () => { + expect(normalizeEditorSettings({}).openTabsRestoreMode).toBe("all"); + }); + + it("preserves explicit open tab restore modes", () => { + expect(normalizeEditorSettings({ openTabsRestoreMode: "pinned" }).openTabsRestoreMode).toBe("pinned"); + expect(normalizeEditorSettings({ openTabsRestoreMode: "none" }).openTabsRestoreMode).toBe("none"); + expect(normalizeEditorSettings({ openTabsRestoreMode: "invalid" as any }).openTabsRestoreMode).toBe("all"); + }); + + it("migrates legacy open tab restore booleans", () => { + expect(normalizeEditorSettings({ restoreOpenTabsOnLaunch: false } as any).openTabsRestoreMode).toBe("none"); + expect(normalizeEditorSettings({ restoreOpenTabsOnLaunch: true } as any).openTabsRestoreMode).toBe("all"); + }); + it("preserves CNB update download source and rejects invalid values", () => { expect(normalizeEditorSettings({ updateDownloadSource: "cnb" }).updateDownloadSource).toBe("cnb"); expect(normalizeEditorSettings({ updateDownloadSource: "mirror" as any }).updateDownloadSource).toBe("official"); diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index a78d52c33..b722e7c33 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -7,7 +7,7 @@ import { orderPinnedFirst } from "@/lib/pinnedItems"; import { canCancelQueryExecution } from "@/lib/queryExecutionState"; import { buildExplainSql, parseExplainResult, parseDamengExplainText } from "@/lib/explainPlan"; import { allEditableColumnsWriteable, allPrimaryKeysPresent, analyzeEditableQuery, sourceColumnsForResult, type EditableQueryInfo } from "@/lib/sqlAnalysis"; -import { restoreOpenTabsState, serializeOpenTabs } from "@/lib/openTabsPersistence"; +import { ACTIVE_TAB_STORAGE_KEY, OPEN_TABS_STORAGE_KEY, restoreOpenTabsState, serializeOpenTabs } from "@/lib/openTabsPersistence"; import { evaluateMongoAggregateSafety, evaluateMongoWriteSafety, @@ -45,8 +45,6 @@ import { useSettingsStore } from "@/stores/settingsStore"; import { useSavedSqlStore } from "@/stores/savedSqlStore"; import type { SavedSqlFile } from "@/types/database"; -const STORAGE_KEY = "dbx-open-tabs"; -const ACTIVE_TAB_KEY = "dbx-active-tab"; const ORACLE_LIKE_METADATA_TYPES = new Set(["oracle", "dameng", "oceanbase-oracle"]); const BACKGROUND_CLIENT_SESSION_SUFFIXES = ["count", "explain", "export"] as const; const CANCEL_QUERY_TIMEOUT_MS = 10_000; @@ -158,14 +156,20 @@ function normalizeOracleLikeQueryAnalysis(dbType: string, analysis: EditableQuer function saveTabs(tabs: QueryTab[], activeTabId: string | null) { try { - localStorage.setItem(STORAGE_KEY, JSON.stringify(serializeOpenTabs(tabs))); - localStorage.setItem(ACTIVE_TAB_KEY, activeTabId || ""); + localStorage.setItem(OPEN_TABS_STORAGE_KEY, JSON.stringify(serializeOpenTabs(tabs))); + localStorage.setItem(ACTIVE_TAB_STORAGE_KEY, activeTabId || ""); } catch {} } function loadSavedTabs(): { tabs: QueryTab[]; activeTabId: string | null } { try { - return restoreOpenTabsState(localStorage.getItem(STORAGE_KEY), localStorage.getItem(ACTIVE_TAB_KEY)); + const restoreMode = useSettingsStore().editorSettings.openTabsRestoreMode; + if (restoreMode === "none") { + return { tabs: [], activeTabId: null }; + } + return restoreOpenTabsState(localStorage.getItem(OPEN_TABS_STORAGE_KEY), localStorage.getItem(ACTIVE_TAB_STORAGE_KEY), { + filter: restoreMode === "pinned" ? "pinned" : "all", + }); } catch { return { tabs: [], activeTabId: null }; } diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts index 0fd4e8946..67d9382f3 100644 --- a/apps/desktop/src/stores/settingsStore.ts +++ b/apps/desktop/src/stores/settingsStore.ts @@ -60,6 +60,7 @@ export type InterfaceLayout = "separated" | "classic"; export type UpdateDownloadSource = "official" | "cnb"; export type SqlSemanticDiagnosticsMode = "auto" | "enabled" | "disabled"; +export type OpenTabsRestoreMode = "all" | "pinned" | "none"; export const DEFAULT_SIDEBAR_TABLE_PAGE_SIZE = 1000; const SQL_SEMANTIC_DIAGNOSTICS_AUTO_ENABLED = false; @@ -357,6 +358,7 @@ export interface EditorSettings { sidebarActivation: SidebarActivation; sidebarObjectDisplay: "grouped" | "simple"; autoSelectActiveSidebarNode: boolean; + openTabsRestoreMode: OpenTabsRestoreMode; disconnectTabHandlingMode: DisconnectTabHandlingMode; reuseDataTab: boolean; updateNotificationsEnabled: boolean; @@ -466,6 +468,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = { sidebarActivation: "single", sidebarObjectDisplay: "grouped", autoSelectActiveSidebarNode: false, + openTabsRestoreMode: "all", disconnectTabHandlingMode: "close-tabs", reuseDataTab: false, updateNotificationsEnabled: true, @@ -546,6 +549,12 @@ function normalizeDisconnectTabHandlingMode(value: unknown, legacyCloseTabsOnDis return DEFAULT_EDITOR_SETTINGS.disconnectTabHandlingMode; } +function normalizeOpenTabsRestoreMode(value: unknown, legacyRestoreOpenTabsOnLaunch?: unknown): OpenTabsRestoreMode { + if (value === "all" || value === "pinned" || value === "none") return value; + if (typeof legacyRestoreOpenTabsOnLaunch === "boolean") return legacyRestoreOpenTabsOnLaunch ? "all" : "none"; + return DEFAULT_EDITOR_SETTINGS.openTabsRestoreMode; +} + function normalizeColumnFormatters(value: unknown): Record { if (!value || typeof value !== "object" || Array.isArray(value)) return {}; const formatters: Record = {}; @@ -664,6 +673,7 @@ export function normalizeEditorSettings(settings: Partial, exist sidebarActivation: settings.sidebarActivation === "single" || settings.sidebarActivation === "double" ? settings.sidebarActivation : DEFAULT_EDITOR_SETTINGS.sidebarActivation, sidebarObjectDisplay: settings.sidebarObjectDisplay === "simple" || settings.sidebarObjectDisplay === "grouped" ? settings.sidebarObjectDisplay : DEFAULT_EDITOR_SETTINGS.sidebarObjectDisplay, autoSelectActiveSidebarNode: settings.autoSelectActiveSidebarNode ?? DEFAULT_EDITOR_SETTINGS.autoSelectActiveSidebarNode, + openTabsRestoreMode: normalizeOpenTabsRestoreMode((settings as Partial).openTabsRestoreMode, (settings as Partial & { restoreOpenTabsOnLaunch?: boolean }).restoreOpenTabsOnLaunch), disconnectTabHandlingMode: normalizeDisconnectTabHandlingMode((settings as Partial).disconnectTabHandlingMode, (settings as Partial & { closeQueryTabsOnDisconnect?: boolean }).closeQueryTabsOnDisconnect), reuseDataTab: settings.reuseDataTab ?? DEFAULT_EDITOR_SETTINGS.reuseDataTab, updateNotificationsEnabled: settings.updateNotificationsEnabled ?? DEFAULT_EDITOR_SETTINGS.updateNotificationsEnabled, @@ -844,6 +854,7 @@ export const useSettingsStore = defineStore("settings", () => { if (partial.sidebarActivation !== undefined) editorSettings.value.sidebarActivation = partial.sidebarActivation; if (partial.sidebarObjectDisplay !== undefined) editorSettings.value.sidebarObjectDisplay = partial.sidebarObjectDisplay; if (partial.autoSelectActiveSidebarNode !== undefined) editorSettings.value.autoSelectActiveSidebarNode = partial.autoSelectActiveSidebarNode; + if (partial.openTabsRestoreMode !== undefined) editorSettings.value.openTabsRestoreMode = normalizeOpenTabsRestoreMode(partial.openTabsRestoreMode); if (partial.disconnectTabHandlingMode !== undefined) editorSettings.value.disconnectTabHandlingMode = normalizeDisconnectTabHandlingMode(partial.disconnectTabHandlingMode); if (partial.reuseDataTab !== undefined) editorSettings.value.reuseDataTab = partial.reuseDataTab; if (partial.updateNotificationsEnabled !== undefined) editorSettings.value.updateNotificationsEnabled = partial.updateNotificationsEnabled; diff --git a/packages/app-tests/openTabsPersistence.test.ts b/packages/app-tests/openTabsPersistence.test.ts index e88351f0d..06abfc700 100644 --- a/packages/app-tests/openTabsPersistence.test.ts +++ b/packages/app-tests/openTabsPersistence.test.ts @@ -253,6 +253,18 @@ test("restores unsaved query tabs and active tab after restart", () => { assert.equal(restored.activeTabId, "tab-2"); }); +test("restores only pinned tabs when requested", () => { + const raw = JSON.stringify([queryTab({ id: "tab-1", pinned: true }), queryTab({ id: "tab-2", pinned: false }), queryTab({ id: "tab-3", pinned: true })]); + + const restored = restoreOpenTabsState(raw, "tab-2", { filter: "pinned" }); + + assert.deepEqual( + restored.tabs.map((tab) => tab.id), + ["tab-1", "tab-3"], + ); + assert.equal(restored.activeTabId, "tab-1"); +}); + test("restores object source save context", () => { const raw = JSON.stringify([ queryTab({