fix(desktop): persist UI state asynchronously
This commit is contained in:
parent
693eb0dbb9
commit
6b13564180
|
|
@ -76,6 +76,7 @@ import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStor
|
|||
import { apiUrl, webPath } from "@/lib/common/webPath";
|
||||
import { APP_FONT_SANS_CSS_VAR, DEFAULT_UI_FONT_FAMILY } from "@/lib/app/appFonts";
|
||||
import { rankSavedSqlHistory } from "@/lib/savedSql/savedSqlHistory";
|
||||
import { initSavedSqlEditorPositions } from "@/lib/app/savedSqlEditorPosition";
|
||||
import { isSchemaAware, isSingleDatabase, usesTreeSchemaMode } from "@/lib/database/databaseFeatureSupport";
|
||||
import { codeMirrorSqlDialect, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
|
||||
import { detectDatabaseFileType } from "@/lib/database/databaseFileDetection";
|
||||
|
|
@ -562,8 +563,10 @@ function finishPendingAppClose(action: AppCloseAction) {
|
|||
}
|
||||
pendingAppCloseAction.value = null;
|
||||
pendingSaveShouldCloseTab.value = true;
|
||||
queryStore.flushPendingPersist();
|
||||
void performCloseAction(action);
|
||||
void queryStore
|
||||
.flushPendingPersist()
|
||||
.catch(() => {})
|
||||
.finally(() => performCloseAction(action));
|
||||
}
|
||||
|
||||
function continuePendingAppCloseAfterSave() {
|
||||
|
|
@ -1529,35 +1532,35 @@ function onLoginSuccess() {
|
|||
setupRequired.value = false;
|
||||
needsAuth.value = true;
|
||||
window.history.replaceState(null, "", webPath("/"));
|
||||
initApp();
|
||||
void initApp();
|
||||
}
|
||||
|
||||
function initApp() {
|
||||
async function initApp() {
|
||||
const t0 = performance.now();
|
||||
console.log("[STARTUP] initApp begin");
|
||||
settingsStore
|
||||
.initDesktopSettings()
|
||||
.catch(() => {})
|
||||
.then(() => {
|
||||
void savedSqlStore
|
||||
.initFromStorage()
|
||||
.then(() => {
|
||||
console.log(`[STARTUP] savedSqlStore.initFromStorage: ${(performance.now() - t0).toFixed(0)}ms`);
|
||||
void queryStore.hydrateSavedSqlTabs();
|
||||
})
|
||||
.catch((e: any) => {
|
||||
toast(t("connection.loadFailed", { message: e?.message || String(e) }), 5000);
|
||||
});
|
||||
return connectionStore.initFromDisk();
|
||||
})
|
||||
.then(() => {
|
||||
console.log(`[STARTUP] connectionStore.initFromDisk: ${(performance.now() - t0).toFixed(0)}ms`);
|
||||
restoreActiveConnectionContext();
|
||||
})
|
||||
.catch((e: any) => {
|
||||
toast(t("connection.loadFailed", { message: e?.message || String(e) }), 5000);
|
||||
});
|
||||
settingsStore.initAiConfig();
|
||||
try {
|
||||
await settingsStore.initEditorSettings();
|
||||
console.log(`[STARTUP] settingsStore.initEditorSettings: ${(performance.now() - t0).toFixed(0)}ms`);
|
||||
await queryStore.initOpenTabs();
|
||||
console.log(`[STARTUP] queryStore.initOpenTabs: ${(performance.now() - t0).toFixed(0)}ms`);
|
||||
await settingsStore.initDesktopSettings().catch(() => {});
|
||||
|
||||
void Promise.all([initSavedSqlEditorPositions(), savedSqlStore.initFromStorage()])
|
||||
.then(() => {
|
||||
console.log(`[STARTUP] savedSqlStore.initFromStorage: ${(performance.now() - t0).toFixed(0)}ms`);
|
||||
void queryStore.hydrateSavedSqlTabs();
|
||||
})
|
||||
.catch((e: any) => {
|
||||
toast(t("connection.loadFailed", { message: e?.message || String(e) }), 5000);
|
||||
});
|
||||
|
||||
await connectionStore.initFromDisk();
|
||||
console.log(`[STARTUP] connectionStore.initFromDisk: ${(performance.now() - t0).toFixed(0)}ms`);
|
||||
restoreActiveConnectionContext();
|
||||
} catch (e: any) {
|
||||
toast(t("connection.loadFailed", { message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function restoreActiveConnectionContext() {
|
||||
|
|
@ -1662,7 +1665,7 @@ onMounted(async () => {
|
|||
if (needsAuth.value && !authenticated.value) {
|
||||
history.replaceState(null, "", webPath("/login"));
|
||||
}
|
||||
if (!setupRequired.value && (!needsAuth.value || authenticated.value)) initApp();
|
||||
if (!setupRequired.value && (!needsAuth.value || authenticated.value)) void initApp();
|
||||
api
|
||||
.getAppVersion()
|
||||
.then((v) => {
|
||||
|
|
@ -1671,7 +1674,7 @@ onMounted(async () => {
|
|||
.catch(() => {});
|
||||
return;
|
||||
}
|
||||
initApp();
|
||||
void initApp();
|
||||
setupFileDrop().catch(() => {});
|
||||
setTimeout(() => {
|
||||
runUpdateNotificationChecks();
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createSavedSqlEditorPosition, forgetSavedSqlEditorPosition, restoreSavedSqlEditorPosition, saveSavedSqlEditorPosition, SAVED_SQL_EDITOR_POSITIONS_STORAGE_KEY } from "../app/savedSqlEditorPosition";
|
||||
import { __resetSavedSqlEditorPositionsForTests, createSavedSqlEditorPosition, forgetSavedSqlEditorPosition, restoreSavedSqlEditorPosition, saveSavedSqlEditorPosition } from "../app/savedSqlEditorPosition";
|
||||
|
||||
vi.mock("@/lib/backend/api", () => ({
|
||||
loadSavedSqlEditorPositions: vi.fn().mockResolvedValue(null),
|
||||
saveSavedSqlEditorPositions: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const storage = new Map<string, string>();
|
||||
|
||||
beforeEach(() => {
|
||||
storage.clear();
|
||||
__resetSavedSqlEditorPositionsForTests();
|
||||
vi.stubGlobal("localStorage", {
|
||||
getItem: (key: string) => storage.get(key) ?? null,
|
||||
setItem: (key: string, value: string) => storage.set(key, value),
|
||||
|
|
@ -100,10 +106,7 @@ describe("savedSqlEditorPosition", () => {
|
|||
);
|
||||
}
|
||||
|
||||
const stored = JSON.parse(storage.get(SAVED_SQL_EDITOR_POSITIONS_STORAGE_KEY) ?? "[]") as Array<{ savedSqlId: string }>;
|
||||
|
||||
expect(stored).toHaveLength(200);
|
||||
expect(stored.some((item) => item.savedSqlId === "file-0")).toBe(false);
|
||||
expect(stored[0]?.savedSqlId).toBe("file-204");
|
||||
expect(restoreSavedSqlEditorPosition("file-0", "select 1;")).toEqual({});
|
||||
expect(restoreSavedSqlEditorPosition("file-204", "select 1;").selection).toEqual({ anchor: 0, head: 0 });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -132,13 +132,10 @@ 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; filter?: OpenTabsRestoreFilter } = {}): RestoredOpenTabs {
|
||||
if (!rawTabs) return { tabs: [], activeTabId: null };
|
||||
function restoreOpenTabsArray(parsed: unknown, rawActiveTabId: string | null, options: { queryOnly?: boolean; filter?: OpenTabsRestoreFilter } = {}): RestoredOpenTabs {
|
||||
if (!Array.isArray(parsed)) return { tabs: [], activeTabId: null };
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(rawTabs);
|
||||
if (!Array.isArray(parsed)) return { tabs: [], activeTabId: null };
|
||||
|
||||
const saved = parsed.filter(isSavedOpenTab);
|
||||
const filtered = saved.filter((tab) => {
|
||||
if (options.queryOnly && (tab.mode ?? "query") !== "query") return false;
|
||||
|
|
@ -185,3 +182,18 @@ export function restoreOpenTabsState(rawTabs: string | null, rawActiveTabId: str
|
|||
return { tabs: [], activeTabId: null };
|
||||
}
|
||||
}
|
||||
|
||||
export function restoreOpenTabsPayload(payload: { tabs?: unknown; activeTabId?: unknown } | null | undefined, options: { queryOnly?: boolean; filter?: OpenTabsRestoreFilter } = {}): RestoredOpenTabs {
|
||||
if (!payload) return { tabs: [], activeTabId: null };
|
||||
return restoreOpenTabsArray(payload.tabs, typeof payload.activeTabId === "string" ? payload.activeTabId : null, options);
|
||||
}
|
||||
|
||||
export function restoreOpenTabsState(rawTabs: string | null, rawActiveTabId: string | null, options: { queryOnly?: boolean; filter?: OpenTabsRestoreFilter } = {}): RestoredOpenTabs {
|
||||
if (!rawTabs) return { tabs: [], activeTabId: null };
|
||||
|
||||
try {
|
||||
return restoreOpenTabsArray(JSON.parse(rawTabs), rawActiveTabId, options);
|
||||
} catch {
|
||||
return { tabs: [], activeTabId: null };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
import * as api from "@/lib/backend/api";
|
||||
import { safeLocalStorageGet, safeLocalStorageRemove } from "@/lib/backend/safeStorage";
|
||||
|
||||
export const SAVED_SQL_EDITOR_POSITIONS_STORAGE_KEY = "dbx-saved-sql-editor-positions";
|
||||
|
||||
const MAX_SAVED_SQL_EDITOR_POSITIONS = 200;
|
||||
|
|
@ -83,44 +86,82 @@ function restoredHeadFromAnchor(position: SavedSqlEditorPosition, sql: string):
|
|||
return targetHead;
|
||||
}
|
||||
|
||||
function parseSavedPositions(raw: string | null): SavedSqlEditorPosition[] {
|
||||
function normalizeSavedPositions(value: unknown): SavedSqlEditorPosition[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value.filter((item): item is SavedSqlEditorPosition => {
|
||||
return (
|
||||
!!item &&
|
||||
typeof item === "object" &&
|
||||
typeof item.savedSqlId === "string" &&
|
||||
!!item.selection &&
|
||||
typeof item.selection.anchor === "number" &&
|
||||
typeof item.selection.head === "number" &&
|
||||
!!item.anchor &&
|
||||
typeof item.anchor.before === "string" &&
|
||||
typeof item.anchor.after === "string" &&
|
||||
typeof item.anchor.head === "number" &&
|
||||
typeof item.anchor.docLength === "number" &&
|
||||
typeof item.updatedAt === "number"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function parseLegacySavedPositions(): SavedSqlEditorPosition[] {
|
||||
const raw = safeLocalStorageGet(SAVED_SQL_EDITOR_POSITIONS_STORAGE_KEY);
|
||||
if (!raw) return [];
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!Array.isArray(parsed)) return [];
|
||||
return parsed.filter((item): item is SavedSqlEditorPosition => {
|
||||
return (
|
||||
!!item &&
|
||||
typeof item === "object" &&
|
||||
typeof item.savedSqlId === "string" &&
|
||||
!!item.selection &&
|
||||
typeof item.selection.anchor === "number" &&
|
||||
typeof item.selection.head === "number" &&
|
||||
!!item.anchor &&
|
||||
typeof item.anchor.before === "string" &&
|
||||
typeof item.anchor.after === "string" &&
|
||||
typeof item.anchor.head === "number" &&
|
||||
typeof item.anchor.docLength === "number" &&
|
||||
typeof item.updatedAt === "number"
|
||||
);
|
||||
});
|
||||
return normalizeSavedPositions(JSON.parse(raw));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
let savedPositions: SavedSqlEditorPosition[] = [];
|
||||
let savedPositionsLoaded = false;
|
||||
let savedPositionsInitPromise: Promise<void> | undefined;
|
||||
|
||||
export async function initSavedSqlEditorPositions() {
|
||||
if (savedPositionsLoaded) return;
|
||||
if (savedPositionsInitPromise) return savedPositionsInitPromise;
|
||||
|
||||
savedPositionsInitPromise = (async () => {
|
||||
const storedValue = await api.loadSavedSqlEditorPositions().catch(() => null);
|
||||
if (Array.isArray(storedValue)) {
|
||||
savedPositions = normalizeSavedPositions(storedValue);
|
||||
return;
|
||||
}
|
||||
|
||||
const legacy = parseLegacySavedPositions();
|
||||
if (legacy.length > 0) {
|
||||
savedPositions = legacy;
|
||||
try {
|
||||
await api.saveSavedSqlEditorPositions(trimSavedPositions(legacy));
|
||||
// Remove the synchronous startup payload only after the async store
|
||||
// accepts the migrated positions.
|
||||
safeLocalStorageRemove(SAVED_SQL_EDITOR_POSITIONS_STORAGE_KEY);
|
||||
} catch {
|
||||
/* keep legacy values for a later migration attempt */
|
||||
}
|
||||
}
|
||||
})().finally(() => {
|
||||
savedPositionsLoaded = true;
|
||||
savedPositionsInitPromise = undefined;
|
||||
});
|
||||
|
||||
return savedPositionsInitPromise;
|
||||
}
|
||||
|
||||
function readSavedPositions(): SavedSqlEditorPosition[] {
|
||||
try {
|
||||
return parseSavedPositions(localStorage.getItem(SAVED_SQL_EDITOR_POSITIONS_STORAGE_KEY));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
return savedPositions;
|
||||
}
|
||||
|
||||
function trimSavedPositions(positions: SavedSqlEditorPosition[]) {
|
||||
return [...positions].sort((a, b) => b.updatedAt - a.updatedAt).slice(0, MAX_SAVED_SQL_EDITOR_POSITIONS);
|
||||
}
|
||||
|
||||
function writeSavedPositions(positions: SavedSqlEditorPosition[]) {
|
||||
try {
|
||||
localStorage.setItem(SAVED_SQL_EDITOR_POSITIONS_STORAGE_KEY, JSON.stringify([...positions].sort((a, b) => b.updatedAt - a.updatedAt).slice(0, MAX_SAVED_SQL_EDITOR_POSITIONS)));
|
||||
} catch {}
|
||||
savedPositions = trimSavedPositions(positions);
|
||||
void api.saveSavedSqlEditorPositions(savedPositions).catch(() => {});
|
||||
}
|
||||
|
||||
export function createSavedSqlEditorPosition(input: { savedSqlId: string; sql: string; selection?: SavedSqlEditorSelection; viewport?: SavedSqlEditorViewport; now?: number }): SavedSqlEditorPosition {
|
||||
|
|
@ -166,3 +207,9 @@ export function restoreSavedSqlEditorPosition(savedSqlId: string, sql: string):
|
|||
export function forgetSavedSqlEditorPosition(savedSqlId: string) {
|
||||
writeSavedPositions(readSavedPositions().filter((item) => item.savedSqlId !== savedSqlId));
|
||||
}
|
||||
|
||||
export function __resetSavedSqlEditorPositionsForTests(positions: SavedSqlEditorPosition[] = []) {
|
||||
savedPositions = trimSavedPositions(positions);
|
||||
savedPositionsLoaded = true;
|
||||
savedPositionsInitPromise = undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -219,6 +219,12 @@ export const setAgentStoreDir = forward("setAgentStoreDir");
|
|||
export const getDriverStorePath = forward("getDriverStorePath");
|
||||
export const loadPinnedTreeNodeIds = forward("loadPinnedTreeNodeIds");
|
||||
export const savePinnedTreeNodeIds = forward("savePinnedTreeNodeIds");
|
||||
export const loadEditorSettings = forward("loadEditorSettings");
|
||||
export const saveEditorSettings = forward("saveEditorSettings");
|
||||
export const loadOpenTabsState = forward("loadOpenTabsState");
|
||||
export const saveOpenTabsState = forward("saveOpenTabsState");
|
||||
export const loadSavedSqlEditorPositions = forward("loadSavedSqlEditorPositions");
|
||||
export const saveSavedSqlEditorPositions = forward("saveSavedSqlEditorPositions");
|
||||
export const webdavSyncTest = forward("webdavSyncTest");
|
||||
export const webdavPasswordStatus = forward("webdavPasswordStatus");
|
||||
export const saveWebdavSavedPassword = forward("saveWebdavSavedPassword");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStorage";
|
||||
|
||||
const DB_NAME = "dbx-app-state";
|
||||
const DB_VERSION = 1;
|
||||
const STORE_NAME = "state";
|
||||
const LOCAL_STORAGE_PREFIX = "dbx-app-state:";
|
||||
|
||||
function indexedDb(): IDBFactory | undefined {
|
||||
return typeof globalThis.indexedDB === "undefined" ? undefined : globalThis.indexedDB;
|
||||
}
|
||||
|
||||
function requestToPromise<T>(request: IDBRequest<T>): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error ?? new Error("IndexedDB request failed"));
|
||||
});
|
||||
}
|
||||
|
||||
let dbPromise: Promise<IDBDatabase | null> | undefined;
|
||||
|
||||
function openDb(): Promise<IDBDatabase | null> {
|
||||
if (dbPromise) return dbPromise;
|
||||
const idb = indexedDb();
|
||||
if (!idb) return Promise.resolve(null);
|
||||
|
||||
dbPromise = new Promise((resolve) => {
|
||||
const request = idb.open(DB_NAME, DB_VERSION);
|
||||
request.onupgradeneeded = () => {
|
||||
const db = request.result;
|
||||
if (!db.objectStoreNames.contains(STORE_NAME)) db.createObjectStore(STORE_NAME);
|
||||
};
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => resolve(null);
|
||||
request.onblocked = () => resolve(null);
|
||||
});
|
||||
return dbPromise;
|
||||
}
|
||||
|
||||
function fallbackKey(key: string) {
|
||||
return `${LOCAL_STORAGE_PREFIX}${key}`;
|
||||
}
|
||||
|
||||
async function withStore<T>(mode: IDBTransactionMode, run: (store: IDBObjectStore) => IDBRequest<T>): Promise<T | null> {
|
||||
const db = await openDb();
|
||||
if (!db) return null;
|
||||
try {
|
||||
return await requestToPromise(run(db.transaction(STORE_NAME, mode).objectStore(STORE_NAME)));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadBrowserAppState(key: string): Promise<unknown | null> {
|
||||
const value = await withStore("readonly", (store) => store.get(key));
|
||||
if (value !== null && value !== undefined) return value;
|
||||
|
||||
const fallback = safeLocalStorageGet(fallbackKey(key));
|
||||
if (!fallback) return null;
|
||||
try {
|
||||
return JSON.parse(fallback);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveBrowserAppState(key: string, value: unknown): Promise<void> {
|
||||
const result = await withStore("readwrite", (store) => store.put(value, key));
|
||||
if (result !== null) return;
|
||||
safeLocalStorageSet(fallbackKey(key), JSON.stringify(value));
|
||||
}
|
||||
|
|
@ -113,6 +113,7 @@ import type { BuildRenameObjectSqlOptions } from "@/lib/table/objectRenameSql";
|
|||
import type { CreateDatabaseSqlOptions } from "@/lib/database/createDatabaseSql";
|
||||
import type { DatabaseNameSqlOptions, DropTableChildObjectSqlOptions, DropObjectSqlOptions, DuplicateTableStructureSqlOptions, CopyTableDataSqlOptions, SchemaNameSqlOptions, TableAdminSqlOptions } from "@/lib/database/dbAdminSql";
|
||||
import type { BuildDatabaseSqlExportOptions, BuildExportInsertStatementsOptions } from "@/lib/export/databaseExport";
|
||||
import { loadBrowserAppState, saveBrowserAppState } from "@/lib/backend/browserAppStateStorage";
|
||||
import type { DataCompareFromTablesOptions, DataCompareFromTablesPreparation, DataCompareSyncPlan, DataCompareSyncPlanOptions, DataComparePreparation, DataComparePreparationOptions } from "@/lib/dataGrid/dataCompare";
|
||||
import { apiUrl, apiWebSocketUrl } from "@/lib/common/webPath";
|
||||
import type { DataGridSavePreparation } from "@/lib/backend/tauri";
|
||||
|
|
@ -1026,6 +1027,39 @@ export async function saveDesktopSettings(settings: DesktopSettings): Promise<vo
|
|||
safeLocalStorageSet(DESKTOP_SETTINGS_STORAGE_KEY, JSON.stringify({ ...DEFAULT_DESKTOP_SETTINGS, ...settings }));
|
||||
}
|
||||
|
||||
export interface OpenTabsStatePayload {
|
||||
tabs: unknown[];
|
||||
activeTabId: string | null;
|
||||
}
|
||||
|
||||
export async function loadEditorSettings(): Promise<unknown | null> {
|
||||
return loadBrowserAppState("editor_settings");
|
||||
}
|
||||
|
||||
export async function saveEditorSettings(settings: unknown): Promise<void> {
|
||||
await saveBrowserAppState("editor_settings", settings);
|
||||
}
|
||||
|
||||
export async function loadOpenTabsState(): Promise<OpenTabsStatePayload | null> {
|
||||
const value = await loadBrowserAppState("open_tabs");
|
||||
if (!value || typeof value !== "object") return null;
|
||||
const payload = value as Partial<OpenTabsStatePayload>;
|
||||
return Array.isArray(payload.tabs) ? { tabs: payload.tabs, activeTabId: typeof payload.activeTabId === "string" ? payload.activeTabId : null } : null;
|
||||
}
|
||||
|
||||
export async function saveOpenTabsState(payload: OpenTabsStatePayload): Promise<void> {
|
||||
await saveBrowserAppState("open_tabs", payload);
|
||||
}
|
||||
|
||||
export async function loadSavedSqlEditorPositions(): Promise<unknown[] | null> {
|
||||
const value = await loadBrowserAppState("saved_sql_editor_positions");
|
||||
return Array.isArray(value) ? value : null;
|
||||
}
|
||||
|
||||
export async function saveSavedSqlEditorPositions(positions: unknown[]): Promise<void> {
|
||||
await saveBrowserAppState("saved_sql_editor_positions", positions);
|
||||
}
|
||||
|
||||
export async function completeAppClose(_action: "quit" | "hide"): Promise<void> {
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -376,6 +376,35 @@ export async function saveDesktopSettings(settings: DesktopSettings): Promise<vo
|
|||
return invoke("save_desktop_settings", { settings });
|
||||
}
|
||||
|
||||
export interface OpenTabsStatePayload {
|
||||
tabs: unknown[];
|
||||
activeTabId: string | null;
|
||||
}
|
||||
|
||||
export async function loadEditorSettings(): Promise<unknown | null> {
|
||||
return invoke("load_editor_settings");
|
||||
}
|
||||
|
||||
export async function saveEditorSettings(settings: unknown): Promise<void> {
|
||||
return invoke("save_editor_settings", { settings });
|
||||
}
|
||||
|
||||
export async function loadOpenTabsState(): Promise<OpenTabsStatePayload | null> {
|
||||
return invoke("load_open_tabs_state");
|
||||
}
|
||||
|
||||
export async function saveOpenTabsState(payload: OpenTabsStatePayload): Promise<void> {
|
||||
return invoke("save_open_tabs_state", { payload });
|
||||
}
|
||||
|
||||
export async function loadSavedSqlEditorPositions(): Promise<unknown[] | null> {
|
||||
return invoke("load_saved_sql_editor_positions");
|
||||
}
|
||||
|
||||
export async function saveSavedSqlEditorPositions(positions: unknown[]): Promise<void> {
|
||||
return invoke("save_saved_sql_editor_positions", { positions });
|
||||
}
|
||||
|
||||
export async function completeAppClose(action: "quit" | "hide"): Promise<void> {
|
||||
return invoke("complete_app_close", { action });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -160,13 +160,16 @@ describe("queryStore database open state", () => {
|
|||
storage.set(ACTIVE_TAB_STORAGE_KEY, "tab-1");
|
||||
setActivePinia(createPinia());
|
||||
|
||||
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
await useSettingsStore().initEditorSettings();
|
||||
const store = useQueryStore();
|
||||
await store.initOpenTabs();
|
||||
|
||||
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");
|
||||
expect(storage.get(OPEN_TABS_STORAGE_KEY)).toBeUndefined();
|
||||
expect(storage.get(ACTIVE_TAB_STORAGE_KEY)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("restores only pinned tabs when launch restore mode is pinned", async () => {
|
||||
|
|
@ -196,8 +199,11 @@ describe("queryStore database open state", () => {
|
|||
storage.set(ACTIVE_TAB_STORAGE_KEY, "tab-2");
|
||||
setActivePinia(createPinia());
|
||||
|
||||
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
await useSettingsStore().initEditorSettings();
|
||||
const store = useQueryStore();
|
||||
await store.initOpenTabs();
|
||||
|
||||
expect(store.tabs.map((tab) => tab.id)).toEqual(["tab-1"]);
|
||||
expect(store.activeTabId).toBe("tab-1");
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { orderPinnedFirst } from "@/lib/app/pinnedItems";
|
|||
import { canCancelQueryExecution } from "@/lib/sql/queryExecutionState";
|
||||
import { buildExplainSql, parseExplainResult, parseDamengExplainText } from "@/lib/diagram/explainPlan";
|
||||
import { allEditableColumnsWriteable, allPrimaryKeysPresent, analyzeEditableQuery, sourceColumnsForResult, type EditableQueryInfo } from "@/lib/sql/sqlAnalysis";
|
||||
import { ACTIVE_TAB_STORAGE_KEY, OPEN_TABS_STORAGE_KEY, restoreOpenTabsState, serializeOpenTabs } from "@/lib/app/openTabsPersistence";
|
||||
import { ACTIVE_TAB_STORAGE_KEY, OPEN_TABS_STORAGE_KEY, restoreOpenTabsPayload, restoreOpenTabsState, serializeOpenTabs } from "@/lib/app/openTabsPersistence";
|
||||
import {
|
||||
evaluateMongoAggregateSafety,
|
||||
evaluateMongoWriteSafety,
|
||||
|
|
@ -44,7 +44,8 @@ import * as api from "@/lib/backend/api";
|
|||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { useSavedSqlStore } from "@/stores/savedSqlStore";
|
||||
import { createSavedSqlEditorPosition, restoreSavedSqlEditorPosition, saveSavedSqlEditorPosition } from "@/lib/app/savedSqlEditorPosition";
|
||||
import { createSavedSqlEditorPosition, initSavedSqlEditorPositions, restoreSavedSqlEditorPosition, saveSavedSqlEditorPosition } from "@/lib/app/savedSqlEditorPosition";
|
||||
import { safeLocalStorageGet, safeLocalStorageRemove } from "@/lib/backend/safeStorage";
|
||||
import type { SavedSqlFile } from "@/types/database";
|
||||
|
||||
const ORACLE_LIKE_METADATA_TYPES = new Set<string>(["oracle", "dameng", "oceanbase-oracle"]);
|
||||
|
|
@ -179,25 +180,41 @@ function normalizeOracleLikeQueryAnalysis(dbType: string, analysis: EditableQuer
|
|||
};
|
||||
}
|
||||
|
||||
function saveTabs(tabs: QueryTab[], activeTabId: string | null) {
|
||||
try {
|
||||
localStorage.setItem(OPEN_TABS_STORAGE_KEY, JSON.stringify(serializeOpenTabs(tabs)));
|
||||
localStorage.setItem(ACTIVE_TAB_STORAGE_KEY, activeTabId || "");
|
||||
} catch {}
|
||||
let saveTabsQueue = Promise.resolve();
|
||||
|
||||
function saveTabs(tabs: QueryTab[], activeTabId: string | null): Promise<void> {
|
||||
const payload = { tabs: serializeOpenTabs(tabs), activeTabId };
|
||||
saveTabsQueue = saveTabsQueue.catch(() => undefined).then(() => api.saveOpenTabsState(payload));
|
||||
return saveTabsQueue;
|
||||
}
|
||||
|
||||
function loadSavedTabs(): { tabs: QueryTab[]; activeTabId: string | null } {
|
||||
try {
|
||||
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 };
|
||||
}
|
||||
function loadLegacySavedTabs(): { rawTabs: string | null; rawActiveTabId: string | null } {
|
||||
return {
|
||||
rawTabs: safeLocalStorageGet(OPEN_TABS_STORAGE_KEY),
|
||||
rawActiveTabId: safeLocalStorageGet(ACTIVE_TAB_STORAGE_KEY),
|
||||
};
|
||||
}
|
||||
|
||||
function clearLegacySavedTabs() {
|
||||
safeLocalStorageRemove(OPEN_TABS_STORAGE_KEY);
|
||||
safeLocalStorageRemove(ACTIVE_TAB_STORAGE_KEY);
|
||||
}
|
||||
|
||||
function restoreSavedTabsFromPayload(payload: { tabs?: unknown; activeTabId?: unknown } | null | undefined): { tabs: QueryTab[]; activeTabId: string | null } {
|
||||
const restoreMode = useSettingsStore().editorSettings.openTabsRestoreMode;
|
||||
if (restoreMode === "none") return { tabs: [], activeTabId: null };
|
||||
return restoreOpenTabsPayload(payload, {
|
||||
filter: restoreMode === "pinned" ? "pinned" : "all",
|
||||
});
|
||||
}
|
||||
|
||||
function restoreLegacySavedTabs(): { tabs: QueryTab[]; activeTabId: string | null } {
|
||||
const restoreMode = useSettingsStore().editorSettings.openTabsRestoreMode;
|
||||
if (restoreMode === "none") return { tabs: [], activeTabId: null };
|
||||
const legacy = loadLegacySavedTabs();
|
||||
return restoreOpenTabsState(legacy.rawTabs, legacy.rawActiveTabId, {
|
||||
filter: restoreMode === "pinned" ? "pinned" : "all",
|
||||
});
|
||||
}
|
||||
|
||||
function getI18nT() {
|
||||
|
|
@ -210,19 +227,16 @@ function getI18nT() {
|
|||
|
||||
export const useQueryStore = defineStore("query", () => {
|
||||
const t = getI18nT();
|
||||
const restored = loadSavedTabs();
|
||||
const tabs = ref<QueryTab[]>(restored.tabs);
|
||||
const activeTabId = ref<string | null>(restored.activeTabId);
|
||||
const activeTabHistory = ref<string[]>(restored.activeTabId ? [restored.activeTabId] : []);
|
||||
const tabs = ref<QueryTab[]>([]);
|
||||
const activeTabId = ref<string | null>(null);
|
||||
const isOpenTabsLoaded = ref(false);
|
||||
const activeTabHistory = ref<string[]>([]);
|
||||
const showCloseConfirm = ref(false);
|
||||
const pendingCloseTabId = ref<string | null>(null);
|
||||
const pendingBatchCloseTabIds = ref<string[] | null>(null);
|
||||
const pendingBatchCloseFinalActiveTabId = ref<string | null | undefined>(undefined);
|
||||
const isConfirmingAppClose = ref(false);
|
||||
const closeConfirmContext = ref<CloseConfirmContext>("tab");
|
||||
for (const tab of restored.tabs) {
|
||||
if (tab.mode === "data") void deleteTabResultSnapshot(tabResultCacheKey(tab.id));
|
||||
}
|
||||
const tableStructureRefreshVersions = ref<Record<string, number>>({});
|
||||
const savedSqlEditorPositionTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
|
|
@ -569,6 +583,41 @@ export const useQueryStore = defineStore("query", () => {
|
|||
clearResultPayload(tab, { evicted: true });
|
||||
}
|
||||
|
||||
function applyRestoredOpenTabs(restored: { tabs: QueryTab[]; activeTabId: string | null }) {
|
||||
tabs.value = restored.tabs;
|
||||
activeTabId.value = restored.activeTabId;
|
||||
activeTabHistory.value = restored.activeTabId ? [restored.activeTabId] : [];
|
||||
for (const tab of restored.tabs) {
|
||||
if (tab.mode === "data") void deleteTabResultSnapshot(tabResultCacheKey(tab.id));
|
||||
}
|
||||
}
|
||||
|
||||
async function initOpenTabs() {
|
||||
if (isOpenTabsLoaded.value) return;
|
||||
const saved = await api.loadOpenTabsState().catch(() => null);
|
||||
if (saved?.tabs && Array.isArray(saved.tabs)) {
|
||||
const restored = restoreSavedTabsFromPayload(saved);
|
||||
applyRestoredOpenTabs(restored);
|
||||
isOpenTabsLoaded.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const legacy = loadLegacySavedTabs();
|
||||
if (legacy.rawTabs || legacy.rawActiveTabId) {
|
||||
const restored = restoreLegacySavedTabs();
|
||||
applyRestoredOpenTabs(restored);
|
||||
try {
|
||||
await saveTabs(tabs.value, activeTabId.value);
|
||||
// Keep old desktop installs readable until the async store has the
|
||||
// migrated state; only then remove the synchronous startup payload.
|
||||
clearLegacySavedTabs();
|
||||
} catch {
|
||||
/* keep legacy values for a later migration attempt */
|
||||
}
|
||||
}
|
||||
isOpenTabsLoaded.value = true;
|
||||
}
|
||||
|
||||
const _persistSnapshot = computed(() =>
|
||||
tabs.value.map((t) => ({
|
||||
id: t.id,
|
||||
|
|
@ -609,7 +658,7 @@ export const useQueryStore = defineStore("query", () => {
|
|||
() => {
|
||||
if (_persistTimer) clearTimeout(_persistTimer);
|
||||
_persistTimer = setTimeout(() => {
|
||||
saveTabs(tabs.value, activeTabId.value);
|
||||
void saveTabs(tabs.value, activeTabId.value).catch(() => {});
|
||||
_persistTimer = null;
|
||||
}, 300);
|
||||
},
|
||||
|
|
@ -620,12 +669,12 @@ export const useQueryStore = defineStore("query", () => {
|
|||
// reflects the latest in-memory tabs without waiting for the 300ms debounce.
|
||||
// Lets callers (e.g. tests that reload the store) read back persisted state
|
||||
// deterministically instead of racing the debounce timer.
|
||||
function flushPendingPersist() {
|
||||
function flushPendingPersist(): Promise<void> {
|
||||
if (_persistTimer) {
|
||||
clearTimeout(_persistTimer);
|
||||
_persistTimer = null;
|
||||
}
|
||||
saveTabs(tabs.value, activeTabId.value);
|
||||
return saveTabs(tabs.value, activeTabId.value);
|
||||
}
|
||||
|
||||
function findTabByIdentity(connectionId: string, database: string, title: string, mode: QueryTab["mode"], schema?: string) {
|
||||
|
|
@ -1473,6 +1522,7 @@ export const useQueryStore = defineStore("query", () => {
|
|||
}
|
||||
|
||||
async function hydrateSavedSqlTabs() {
|
||||
await initSavedSqlEditorPositions();
|
||||
const savedSqlStore = useSavedSqlStore();
|
||||
const linkedTabs = tabs.value.filter((tab) => tab.savedSqlId && tab.sql === "");
|
||||
for (const tab of linkedTabs) {
|
||||
|
|
@ -2921,6 +2971,8 @@ export const useQueryStore = defineStore("query", () => {
|
|||
return {
|
||||
tabs,
|
||||
activeTabId,
|
||||
isOpenTabsLoaded,
|
||||
initOpenTabs,
|
||||
showCloseConfirm,
|
||||
pendingCloseTabId,
|
||||
closeConfirmContext,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { DEFAULT_SQL_SNIPPETS } from "@/lib/sql/sqlCompletion";
|
|||
import { setDebugLoggingEnabled } from "@/lib/backend/debugLog";
|
||||
import { DEFAULT_TABLE_COLUMN_TEMPLATE_FIELDS, normalizeTableColumnTemplateFields } from "@/lib/table/tableColumnTemplates";
|
||||
import { DEFAULT_UI_FONT_FAMILY } from "@/lib/app/appFonts";
|
||||
import { safeLocalStorageGet, safeLocalStorageRemove } from "@/lib/backend/safeStorage";
|
||||
|
||||
export type AiProvider = "claude" | "openai" | "gemini" | "deepseek" | "qwen" | "ollama" | "openai-compatible" | "codex-cli" | "custom";
|
||||
export type AiApiStyle = "completions" | "responses" | "anthropic-messages";
|
||||
|
|
@ -751,46 +752,34 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
|
|||
};
|
||||
}
|
||||
|
||||
function loadEditorSettings(): EditorSettings {
|
||||
// Try new format first
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) {
|
||||
function loadLegacyEditorSettings(): EditorSettings | null {
|
||||
const raw = safeLocalStorageGet(STORAGE_KEY);
|
||||
if (raw) {
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as Partial<EditorSettings>;
|
||||
if (parsed.exportBatchSize === LEGACY_DEFAULT_EXPORT_BATCH_SIZE && localStorage.getItem(EXPORT_BATCH_SIZE_DEFAULT_MIGRATION_KEY) !== "1") {
|
||||
if (parsed.exportBatchSize === LEGACY_DEFAULT_EXPORT_BATCH_SIZE && safeLocalStorageGet(EXPORT_BATCH_SIZE_DEFAULT_MIGRATION_KEY) !== "1") {
|
||||
parsed.exportBatchSize = DEFAULT_EDITOR_SETTINGS.exportBatchSize;
|
||||
localStorage.setItem(EXPORT_BATCH_SIZE_DEFAULT_MIGRATION_KEY, "1");
|
||||
const migrated = normalizeEditorSettings(parsed);
|
||||
saveEditorSettings(migrated);
|
||||
return migrated;
|
||||
}
|
||||
return normalizeEditorSettings(parsed);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
// Migrate old font-size key if new settings don't exist
|
||||
try {
|
||||
const oldSize = localStorage.getItem(OLD_FONT_SIZE_KEY);
|
||||
if (oldSize) {
|
||||
const parsed = parseInt(oldSize, 10);
|
||||
if (!isNaN(parsed)) {
|
||||
const migrated = normalizeEditorSettings({ fontSize: parsed });
|
||||
saveEditorSettings(migrated);
|
||||
localStorage.removeItem(OLD_FONT_SIZE_KEY);
|
||||
return migrated;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
const oldSize = safeLocalStorageGet(OLD_FONT_SIZE_KEY);
|
||||
if (!oldSize) return null;
|
||||
const parsed = parseInt(oldSize, 10);
|
||||
return Number.isNaN(parsed) ? null : normalizeEditorSettings({ fontSize: parsed });
|
||||
}
|
||||
|
||||
return normalizeEditorSettings({});
|
||||
function clearLegacyEditorSettings() {
|
||||
safeLocalStorageRemove(STORAGE_KEY);
|
||||
safeLocalStorageRemove(OLD_FONT_SIZE_KEY);
|
||||
safeLocalStorageRemove(EXPORT_BATCH_SIZE_DEFAULT_MIGRATION_KEY);
|
||||
}
|
||||
|
||||
function saveEditorSettings(settings: EditorSettings) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
|
||||
void api.saveEditorSettings(settings).catch(() => {});
|
||||
}
|
||||
|
||||
export const useSettingsStore = defineStore("settings", () => {
|
||||
|
|
@ -799,8 +788,33 @@ export const useSettingsStore = defineStore("settings", () => {
|
|||
const aiProviderConfigs = ref<Partial<Record<AiProvider, AiConfig>>>({});
|
||||
const desktopSettings = ref<DesktopSettings>({ ...DEFAULT_DESKTOP_SETTINGS });
|
||||
const isDesktopSettingsLoaded = ref(false);
|
||||
const isEditorSettingsLoaded = ref(false);
|
||||
|
||||
const editorSettings = ref<EditorSettings>(loadEditorSettings());
|
||||
const editorSettings = ref<EditorSettings>(normalizeEditorSettings({}));
|
||||
|
||||
async function initEditorSettings() {
|
||||
if (isEditorSettingsLoaded.value) return;
|
||||
const saved = await api.loadEditorSettings().catch(() => null);
|
||||
if (saved && typeof saved === "object" && !Array.isArray(saved)) {
|
||||
editorSettings.value = normalizeEditorSettings(saved as Partial<EditorSettings>);
|
||||
isEditorSettingsLoaded.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const legacy = loadLegacyEditorSettings();
|
||||
if (legacy) {
|
||||
editorSettings.value = legacy;
|
||||
try {
|
||||
await api.saveEditorSettings(legacy);
|
||||
// Existing desktop users keep settings in localStorage; remove them only
|
||||
// after the async store has accepted the migrated value.
|
||||
clearLegacyEditorSettings();
|
||||
} catch {
|
||||
/* keep legacy values for a later migration attempt */
|
||||
}
|
||||
}
|
||||
isEditorSettingsLoaded.value = true;
|
||||
}
|
||||
|
||||
async function initDesktopSettings() {
|
||||
if (isDesktopSettingsLoaded.value) return;
|
||||
|
|
@ -1016,8 +1030,10 @@ export const useSettingsStore = defineStore("settings", () => {
|
|||
updateAiConfig,
|
||||
isConfigured,
|
||||
isAiProviderConfigured,
|
||||
isEditorSettingsLoaded,
|
||||
editorSettings,
|
||||
desktopSettings,
|
||||
initEditorSettings,
|
||||
updateEditorSettings,
|
||||
initDesktopSettings,
|
||||
updateDesktopSettings,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ use crate::saved_sql::{SavedSqlFile, SavedSqlFolder, SavedSqlLibrary};
|
|||
const SSH_TUNNEL_SECRET_PREFIX: &str = "ssh_tunnels.";
|
||||
const TRANSPORT_LAYER_SECRET_PREFIX: &str = "transport_layers.";
|
||||
const STORAGE_DB_FILE_NAME: &str = "dbx.db";
|
||||
const APP_STATE_EDITOR_SETTINGS_KEY: &str = "editor_settings";
|
||||
const APP_STATE_OPEN_TABS_KEY: &str = "open_tabs";
|
||||
const APP_STATE_SAVED_SQL_EDITOR_POSITIONS_KEY: &str = "saved_sql_editor_positions";
|
||||
const USER_DATA_TABLES: &[&str] = &[
|
||||
"connections",
|
||||
"connection_secrets",
|
||||
|
|
@ -223,6 +226,10 @@ const SCHEMA_STATEMENTS: &[&str] = &[
|
|||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
settings_json TEXT NOT NULL
|
||||
)",
|
||||
"CREATE TABLE IF NOT EXISTS app_state (
|
||||
key TEXT PRIMARY KEY,
|
||||
value_json TEXT NOT NULL
|
||||
)",
|
||||
"CREATE TABLE IF NOT EXISTS schema_cache (
|
||||
cache_key TEXT PRIMARY KEY,
|
||||
payload_json TEXT NOT NULL,
|
||||
|
|
@ -918,6 +925,53 @@ impl Storage {
|
|||
Ok(array.iter().filter_map(|item| item.as_str().map(|value| value.to_string())).collect())
|
||||
}
|
||||
|
||||
async fn save_app_state_value(&self, key: &str, value: &serde_json::Value) -> Result<(), String> {
|
||||
let key = key.to_string();
|
||||
let value_json = serde_json::to_string(value).map_err(|e| e.to_string())?;
|
||||
self.with_conn(move |conn| {
|
||||
conn.execute("INSERT OR REPLACE INTO app_state (key, value_json) VALUES (?1, ?2)", params![key, value_json])
|
||||
.map(|_| ())
|
||||
.map_err(|e| e.to_string())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn load_app_state_value(&self, key: &str) -> Result<Option<serde_json::Value>, String> {
|
||||
let key = key.to_string();
|
||||
let json: Option<String> = self
|
||||
.with_conn(move |conn| {
|
||||
conn.query_row("SELECT value_json FROM app_state WHERE key = ?1", [key], |row| row.get(0))
|
||||
.optional()
|
||||
.map_err(|e| e.to_string())
|
||||
})
|
||||
.await?;
|
||||
json.map(|value| serde_json::from_str(&value).map_err(|e| e.to_string())).transpose()
|
||||
}
|
||||
|
||||
pub async fn save_editor_settings(&self, settings: &serde_json::Value) -> Result<(), String> {
|
||||
self.save_app_state_value(APP_STATE_EDITOR_SETTINGS_KEY, settings).await
|
||||
}
|
||||
|
||||
pub async fn load_editor_settings(&self) -> Result<Option<serde_json::Value>, String> {
|
||||
self.load_app_state_value(APP_STATE_EDITOR_SETTINGS_KEY).await
|
||||
}
|
||||
|
||||
pub async fn save_open_tabs_state(&self, state: &serde_json::Value) -> Result<(), String> {
|
||||
self.save_app_state_value(APP_STATE_OPEN_TABS_KEY, state).await
|
||||
}
|
||||
|
||||
pub async fn load_open_tabs_state(&self) -> Result<Option<serde_json::Value>, String> {
|
||||
self.load_app_state_value(APP_STATE_OPEN_TABS_KEY).await
|
||||
}
|
||||
|
||||
pub async fn save_saved_sql_editor_positions(&self, positions: &serde_json::Value) -> Result<(), String> {
|
||||
self.save_app_state_value(APP_STATE_SAVED_SQL_EDITOR_POSITIONS_KEY, positions).await
|
||||
}
|
||||
|
||||
pub async fn load_saved_sql_editor_positions(&self) -> Result<Option<serde_json::Value>, String> {
|
||||
self.load_app_state_value(APP_STATE_SAVED_SQL_EDITOR_POSITIONS_KEY).await
|
||||
}
|
||||
|
||||
pub async fn load_or_create_local_device_secret(&self) -> Result<String, String> {
|
||||
let mut settings = self.load_app_settings_json().await?;
|
||||
if let Some(secret) = settings.get("local_device_secret").and_then(|value| value.as_str()) {
|
||||
|
|
@ -2867,6 +2921,53 @@ mod tests {
|
|||
assert_eq!(storage.load_password_hash().await.unwrap(), Some("hash-3".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn app_state_roundtrips_without_polluting_app_settings() {
|
||||
let path = temp_db_path("app-state-roundtrip");
|
||||
let storage = Storage::open(&path).await.unwrap();
|
||||
|
||||
storage.save_password_hash("hash-4").await.unwrap();
|
||||
storage
|
||||
.save_desktop_settings(&DesktopSettings {
|
||||
icon_theme: DesktopIconTheme::Black,
|
||||
..DesktopSettings::default()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
storage.save_editor_settings(&serde_json::json!({ "openTabsRestoreMode": "pinned" })).await.unwrap();
|
||||
storage
|
||||
.save_open_tabs_state(&serde_json::json!({
|
||||
"tabs": [{ "id": "tab-1", "title": "Pinned", "connectionId": "pg", "database": "app", "sql": "select 1", "pinned": true }],
|
||||
"activeTabId": "tab-1"
|
||||
}))
|
||||
.await
|
||||
.unwrap();
|
||||
storage
|
||||
.save_saved_sql_editor_positions(&serde_json::json!([{ "savedSqlId": "file-1", "updatedAt": 1 }]))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
storage.load_editor_settings().await.unwrap(),
|
||||
Some(serde_json::json!({ "openTabsRestoreMode": "pinned" }))
|
||||
);
|
||||
assert_eq!(
|
||||
storage.load_open_tabs_state().await.unwrap().and_then(|value| value.get("activeTabId").cloned()),
|
||||
Some(serde_json::json!("tab-1"))
|
||||
);
|
||||
assert_eq!(
|
||||
storage.load_saved_sql_editor_positions().await.unwrap(),
|
||||
Some(serde_json::json!([{ "savedSqlId": "file-1", "updatedAt": 1 }]))
|
||||
);
|
||||
assert_eq!(storage.load_password_hash().await.unwrap(), Some("hash-4".to_string()));
|
||||
assert_eq!(
|
||||
storage.load_desktop_settings().await.unwrap(),
|
||||
DesktopSettings { icon_theme: DesktopIconTheme::Black, ..DesktopSettings::default() }
|
||||
);
|
||||
assert_eq!(storage.load_app_settings_json().await.unwrap().get("open_tabs"), None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tab_runtime_cache_roundtrips_binary_payloads() {
|
||||
let path = temp_db_path("tab-runtime-cache");
|
||||
|
|
|
|||
|
|
@ -75,6 +75,41 @@ pub async fn save_pinned_tree_node_ids(state: State<'_, Arc<AppState>>, ids: Vec
|
|||
state.storage.save_pinned_tree_node_ids(&ids).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn load_editor_settings(state: State<'_, Arc<AppState>>) -> Result<Option<serde_json::Value>, String> {
|
||||
state.storage.load_editor_settings().await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_editor_settings(state: State<'_, Arc<AppState>>, settings: serde_json::Value) -> Result<(), String> {
|
||||
state.storage.save_editor_settings(&settings).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn load_open_tabs_state(state: State<'_, Arc<AppState>>) -> Result<Option<serde_json::Value>, String> {
|
||||
state.storage.load_open_tabs_state().await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_open_tabs_state(state: State<'_, Arc<AppState>>, payload: serde_json::Value) -> Result<(), String> {
|
||||
state.storage.save_open_tabs_state(&payload).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn load_saved_sql_editor_positions(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
) -> Result<Option<serde_json::Value>, String> {
|
||||
state.storage.load_saved_sql_editor_positions().await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_saved_sql_editor_positions(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
positions: serde_json::Value,
|
||||
) -> Result<(), String> {
|
||||
state.storage.save_saved_sql_editor_positions(&positions).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn load_native_debug_logs(app: AppHandle) -> Result<String, String> {
|
||||
let log_dir = app.path().app_log_dir().map_err(|e| e.to_string())?;
|
||||
|
|
|
|||
|
|
@ -736,6 +736,12 @@ pub fn run() {
|
|||
commands::app_settings::get_driver_store_path,
|
||||
commands::app_settings::load_pinned_tree_node_ids,
|
||||
commands::app_settings::save_pinned_tree_node_ids,
|
||||
commands::app_settings::load_editor_settings,
|
||||
commands::app_settings::save_editor_settings,
|
||||
commands::app_settings::load_open_tabs_state,
|
||||
commands::app_settings::save_open_tabs_state,
|
||||
commands::app_settings::load_saved_sql_editor_positions,
|
||||
commands::app_settings::save_saved_sql_editor_positions,
|
||||
commands::app_settings::load_native_debug_logs,
|
||||
commands::cloud_sync::webdav_sync_test,
|
||||
commands::cloud_sync::webdav_password_status,
|
||||
|
|
|
|||
Loading…
Reference in New Issue