From ad33db088691effec0db21c668c76e85b008c99e Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Mon, 1 Jun 2026 23:06:54 +0800 Subject: [PATCH] fix(desktop): restore table tabs on startup --- apps/desktop/src/App.vue | 16 ++-- apps/desktop/src/lib/openTabsPersistence.ts | 20 +++++ apps/desktop/src/stores/connectionStore.ts | 8 +- apps/desktop/src/stores/queryStore.ts | 15 +++- .../app-tests/openTabsPersistence.test.ts | 87 ++++++++++++++++++- 5 files changed, 129 insertions(+), 17 deletions(-) diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 28a6b0327..6c6314485 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -847,17 +847,17 @@ function initApp() { } async function reconnectRestoredTabs() { - if (isDesktop) return; - const connectionIds = new Set(queryStore.tabs.map((t) => t.connectionId).filter(Boolean)); - for (const id of connectionIds) { + const activeConnectionId = activeTab.value?.connectionId || connectionStore.activeConnectionId; + if (activeConnectionId && connectionStore.getConfig(activeConnectionId)) { + connectionStore.activeConnectionId = activeConnectionId; try { - await connectionStore.ensureConnected(id); + await connectionStore.ensureConnected(activeConnectionId); } catch {} } - for (const tab of queryStore.tabs) { - if (tab.mode === "data" && tab.tableMeta && tab.sql) { - queryStore.executeTabSql(tab.id, tab.sql).catch(() => {}); - } + + const tab = activeTab.value; + if (tab?.mode === "data" && tab.tableMeta && tab.sql) { + queryStore.executeTabSql(tab.id, tab.sql).catch(() => {}); } } diff --git a/apps/desktop/src/lib/openTabsPersistence.ts b/apps/desktop/src/lib/openTabsPersistence.ts index cd1bc1d9c..21b0b313b 100644 --- a/apps/desktop/src/lib/openTabsPersistence.ts +++ b/apps/desktop/src/lib/openTabsPersistence.ts @@ -9,8 +9,18 @@ export interface SavedOpenTab { schema?: string; sql: string; savedSqlId?: string; + lastExecutedSql?: string; + resultBaseSql?: string; + resultSortedSql?: string; + resultSortColumn?: string; + resultSortColumnIndex?: number; + resultSortDirection?: QueryTab["resultSortDirection"]; + resultPageLimit?: number; + resultPageOffset?: number; + whereInput?: string; pinned?: boolean; mode?: QueryTab["mode"]; + structureTableName?: string; objectBrowser?: QueryTab["objectBrowser"]; objectSource?: QueryTab["objectSource"]; tableMeta?: QueryTab["tableMeta"]; @@ -31,8 +41,18 @@ export function serializeOpenTabs(tabs: QueryTab[]): SavedOpenTab[] { schema: tab.schema, sql: tab.sql, savedSqlId: tab.savedSqlId, + ...(tab.lastExecutedSql !== undefined ? { lastExecutedSql: tab.lastExecutedSql } : {}), + ...(tab.resultBaseSql !== undefined ? { resultBaseSql: tab.resultBaseSql } : {}), + ...(tab.resultSortedSql !== undefined ? { resultSortedSql: tab.resultSortedSql } : {}), + ...(tab.resultSortColumn !== undefined ? { resultSortColumn: tab.resultSortColumn } : {}), + ...(tab.resultSortColumnIndex !== undefined ? { resultSortColumnIndex: tab.resultSortColumnIndex } : {}), + ...(tab.resultSortDirection !== undefined ? { resultSortDirection: tab.resultSortDirection } : {}), + ...(tab.resultPageLimit !== undefined ? { resultPageLimit: tab.resultPageLimit } : {}), + ...(tab.resultPageOffset !== undefined ? { resultPageOffset: tab.resultPageOffset } : {}), + ...(tab.whereInput !== undefined ? { whereInput: tab.whereInput } : {}), pinned: tab.pinned, mode: tab.mode, + ...(tab.structureTableName !== undefined ? { structureTableName: tab.structureTableName } : {}), objectBrowser: tab.objectBrowser, objectSource: tab.objectSource, tableMeta: tab.tableMeta, diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index 14e3b0d12..8e37ac6f7 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -48,6 +48,7 @@ import { useSavedSqlStore } from "@/stores/savedSqlStore"; import { useSettingsStore } from "@/stores/settingsStore"; const PINNED_TREE_NODES_STORAGE_KEY = "dbx-pinned-tree-nodes"; +const ACTIVE_CONNECTION_STORAGE_KEY = "dbx-active-connection"; type ImportSource = "dbx" | "navicat" | "dbeaver"; interface LoadTreeOptions { @@ -69,13 +70,12 @@ function redisDbLabel(db: number, loadedKeyCount?: number, totalKeyCount?: numbe export const useConnectionStore = defineStore("connection", () => { const connections = ref([]); const isDesktop = isTauriRuntime(); - const activeConnectionId = ref(!isDesktop ? localStorage.getItem("dbx-active-connection") : null); + const activeConnectionId = ref(localStorage.getItem(ACTIVE_CONNECTION_STORAGE_KEY)); const selectedTreeNodeId = ref(null); watch(activeConnectionId, (id) => { - if (isDesktop) return; - if (id) localStorage.setItem("dbx-active-connection", id); - else localStorage.removeItem("dbx-active-connection"); + if (id) localStorage.setItem(ACTIVE_CONNECTION_STORAGE_KEY, id); + else localStorage.removeItem(ACTIVE_CONNECTION_STORAGE_KEY); }); const treeNodes = ref([]); const pinnedTreeNodeIds = ref>(new Set()); diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index c71bc1617..862074033 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -26,7 +26,6 @@ import { TABLE_DATA_EXPORT_PAGE_SIZE } from "@/lib/tableDataExport"; import * as api from "@/lib/api"; import { useConnectionStore } from "@/stores/connectionStore"; import { useSettingsStore } from "@/stores/settingsStore"; -import { isTauriRuntime } from "@/lib/tauriRuntime"; import type { SavedSqlFile } from "@/types/database"; const STORAGE_KEY = "dbx-open-tabs"; @@ -41,9 +40,7 @@ function saveTabs(tabs: QueryTab[], activeTabId: string | null) { function loadSavedTabs(): { tabs: QueryTab[]; activeTabId: string | null } { try { - return restoreOpenTabsState(localStorage.getItem(STORAGE_KEY), localStorage.getItem(ACTIVE_TAB_KEY), { - queryOnly: isTauriRuntime(), - }); + return restoreOpenTabsState(localStorage.getItem(STORAGE_KEY), localStorage.getItem(ACTIVE_TAB_KEY)); } catch { return { tabs: [], activeTabId: null }; } @@ -117,8 +114,18 @@ export const useQueryStore = defineStore("query", () => { schema: t.schema, sql: t.sql, savedSqlId: t.savedSqlId, + lastExecutedSql: t.lastExecutedSql, + resultBaseSql: t.resultBaseSql, + resultSortedSql: t.resultSortedSql, + resultSortColumn: t.resultSortColumn, + resultSortColumnIndex: t.resultSortColumnIndex, + resultSortDirection: t.resultSortDirection, + resultPageLimit: t.resultPageLimit, + resultPageOffset: t.resultPageOffset, + whereInput: t.whereInput, pinned: t.pinned, mode: t.mode, + structureTableName: t.structureTableName, objectBrowser: t.objectBrowser, objectSource: t.objectSource, tableMeta: t.tableMeta, diff --git a/packages/app-tests/openTabsPersistence.test.ts b/packages/app-tests/openTabsPersistence.test.ts index caf5aabfc..904889767 100644 --- a/packages/app-tests/openTabsPersistence.test.ts +++ b/packages/app-tests/openTabsPersistence.test.ts @@ -62,6 +62,38 @@ test("serializes object source query tabs with save context", () => { assert.equal(saved[0]?.customTitle, true); }); +test("serializes table tabs with reload context", () => { + const saved = serializeOpenTabs([ + queryTab({ + mode: "data", + lastExecutedSql: "select * from users limit 100 offset 100", + resultPageLimit: 100, + resultPageOffset: 100, + whereInput: "active = true", + tableMeta: { schema: "public", tableName: "users", columns: [], primaryKeys: [] }, + }), + ]); + + assert.deepEqual( + { + mode: saved[0]?.mode, + lastExecutedSql: saved[0]?.lastExecutedSql, + resultPageLimit: saved[0]?.resultPageLimit, + resultPageOffset: saved[0]?.resultPageOffset, + whereInput: saved[0]?.whereInput, + tableMeta: saved[0]?.tableMeta, + }, + { + mode: "data", + lastExecutedSql: "select * from users limit 100 offset 100", + resultPageLimit: 100, + resultPageOffset: 100, + whereInput: "active = true", + tableMeta: { schema: "public", tableName: "users", columns: [], primaryKeys: [] }, + }, + ); +}); + test("restores unsaved query tabs and active tab after restart", () => { const raw = JSON.stringify([ queryTab({ id: "tab-1", sql: "select 1" }), @@ -102,7 +134,60 @@ test("restores object source save context", () => { assert.equal(restored.tabs[0]?.customTitle, true); }); -test("desktop restore keeps legacy query tabs without a mode", () => { +test("restores data and structure tabs with table state", () => { + const raw = JSON.stringify([ + queryTab({ + id: "data", + title: "public.users", + mode: "data", + sql: 'SELECT * FROM "public"."users" LIMIT 50 OFFSET 50;', + lastExecutedSql: 'SELECT * FROM "public"."users" LIMIT 50 OFFSET 50;', + resultPageLimit: 50, + resultPageOffset: 50, + whereInput: "id > 10", + tableMeta: { + schema: "public", + tableName: "users", + columns: [ + { + name: "id", + data_type: "integer", + is_nullable: false, + column_default: null, + is_primary_key: true, + extra: null, + }, + ], + primaryKeys: ["id"], + }, + }), + queryTab({ + id: "structure", + title: "Edit users", + mode: "structure", + sql: "", + structureTableName: "users", + }), + ]); + + const restored = restoreOpenTabsState(raw, "data"); + + assert.deepEqual( + restored.tabs.map((tab) => ({ id: tab.id, mode: tab.mode })), + [ + { id: "data", mode: "data" }, + { id: "structure", mode: "structure" }, + ], + ); + assert.equal(restored.activeTabId, "data"); + assert.equal(restored.tabs[0]?.tableMeta?.tableName, "users"); + assert.equal(restored.tabs[0]?.resultPageLimit, 50); + assert.equal(restored.tabs[0]?.resultPageOffset, 50); + assert.equal(restored.tabs[0]?.whereInput, "id > 10"); + assert.equal(restored.tabs[1]?.structureTableName, "users"); +}); + +test("query-only restore keeps legacy query tabs without a mode", () => { const raw = JSON.stringify([ { id: "legacy",