feat(settings): 启动时标签页恢复策略三选项 (all/pinned/none) (#2395)

* feat(settings): 添加启动标签页恢复策略

* i18n: 补齐启动标签页恢复策略 5 个 locale 译文

---------

Co-authored-by: staff <staff@qimaos-MacBook-Pro.local>
Co-authored-by: t8y2 <1156263951@qq.com>
This commit is contained in:
zipg 2026-07-02 12:23:24 +08:00 committed by GitHub
parent 8747df53ef
commit 47dc35ccb2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 193 additions and 8 deletions

View File

@ -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<OpenTabsRestoreMode>(settingsStore.editorSettings.openTabsRestoreMode);
const editDisconnectTabHandlingMode = ref<DisconnectTabHandlingMode>(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);
</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="open-tabs-restore-mode">{{ t("settings.openTabsRestoreMode") }}</Label>
<HelpTooltip :label="t('settings.openTabsRestoreMode')">
{{ t("settings.openTabsRestoreModeDescription") }}
</HelpTooltip>
</div>
<Select :model-value="editOpenTabsRestoreMode" @update:model-value="(value) => (editOpenTabsRestoreMode = value as OpenTabsRestoreMode)">
<SelectTrigger id="open-tabs-restore-mode" class="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">{{ t("settings.openTabsRestoreModeAll") }}</SelectItem>
<SelectItem value="pinned">{{ t("settings.openTabsRestoreModePinned") }}</SelectItem>
<SelectItem value="none">{{ t("settings.openTabsRestoreModeNone") }}</SelectItem>
</SelectContent>
</Select>
<p class="text-xs text-muted-foreground">
{{ t("settings.openTabsRestoreModeHint") }}
</p>
</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>

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -2619,6 +2619,12 @@ export default withEnglishFallback({
sidebarObjectDisplaySimpleDescription: "中間レベルを減らすために、各データベースまたはスキーマの直下にテーブルとビューを一覧表示します。",
autoSelectActiveSidebarNode: "開いた項目を常に選択",
autoSelectActiveSidebarNodeDescription: "タブ切替時に、サイドバーで一致する表示中のテーブル、コレクション、またはSQLファイルを選択します。",
openTabsRestoreMode: "起動時にタブを復元",
openTabsRestoreModeDescription: "DBX 起動時に前回開いていたタブをどのように復元するかを選択します。",
openTabsRestoreModeAll: "すべてのタブを保持",
openTabsRestoreModePinned: "ピン留めタブのみ保持",
openTabsRestoreModeNone: "タブを保持しない",
openTabsRestoreModeHint: "ピン留めタブはタブバーの左側に配置され、頻繁に使う作業の入り口として便利です。",
disconnectTabHandlingMode: "切断後のタブ",
disconnectTabHandlingModeDescription: "接続またはデータベースを閉じた後の関連タブの処理方法を選択します。",
disconnectTabHandlingModeCloseTabs: "すべてのタブを閉じる",

View File

@ -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",

View File

@ -2652,6 +2652,12 @@ export default withEnglishFallback({
sidebarObjectDisplaySimpleDescription: "数据库或 schema 下直接平铺表和视图,减少中间层级。",
autoSelectActiveSidebarNode: "始终选中已打开项目",
autoSelectActiveSidebarNodeDescription: "切换标签页时,在侧边栏选中匹配的可见表、集合或 SQL 文件。",
openTabsRestoreMode: "启动时恢复标签页",
openTabsRestoreModeDescription: "选择 DBX 启动时如何恢复上次打开的标签页。",
openTabsRestoreModeAll: "保留所有标签",
openTabsRestoreModePinned: "仅保留固定标签",
openTabsRestoreModeNone: "不保留标签",
openTabsRestoreModeHint: "固定标签会显示在标签栏左侧,适合作为常用工作入口。",
disconnectTabHandlingMode: "关闭连接后的标签页处理",
disconnectTabHandlingModeDescription: "选择关闭连接或数据库后,相关标签页的保留策略。",
disconnectTabHandlingModeCloseTabs: "关闭全部页签",

View File

@ -2525,6 +2525,12 @@ export default withEnglishFallback({
sidebarObjectDisplaySimpleDescription: "直接在每個資料庫或 schema 下列出資料表與檢視,減少中間層級。",
autoSelectActiveSidebarNode: "一律選取已開啟項目",
autoSelectActiveSidebarNodeDescription: "切換分頁時,在側邊欄選取相符的可見資料表、集合或 SQL 檔案。",
openTabsRestoreMode: "啟動時還原分頁",
openTabsRestoreModeDescription: "選擇 DBX 啟動時如何還原上次開啟的分頁。",
openTabsRestoreModeAll: "保留所有分頁",
openTabsRestoreModePinned: "僅保留置頂分頁",
openTabsRestoreModeNone: "不保留分頁",
openTabsRestoreModeHint: "置頂分頁會顯示在分頁列左側,適合作為常用工作入口。",
disconnectTabHandlingMode: "關閉連線後的分頁處理",
disconnectTabHandlingModeDescription: "選擇關閉連線或資料庫後,相關分頁的保留策略。",
disconnectTabHandlingModeCloseTabs: "關閉全部分頁",

View File

@ -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 =

View File

@ -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<string, string>();
@ -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");
});
});

View File

@ -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");

View File

@ -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<string>(["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 };
}

View File

@ -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<string, ColumnFormatterConfig> {
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
const formatters: Record<string, ColumnFormatterConfig> = {};
@ -664,6 +673,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, 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<EditorSettings>).openTabsRestoreMode, (settings as Partial<EditorSettings> & { restoreOpenTabsOnLaunch?: boolean }).restoreOpenTabsOnLaunch),
disconnectTabHandlingMode: normalizeDisconnectTabHandlingMode((settings as Partial<EditorSettings>).disconnectTabHandlingMode, (settings as Partial<EditorSettings> & { 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;

View File

@ -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({