mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 06:00:39 +00:00
fix(query): persist unsaved query tabs
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import type { QueryTab } from "@/types/database";
|
||||
|
||||
export interface SavedOpenTab {
|
||||
id: string;
|
||||
title: string;
|
||||
connectionId: string;
|
||||
database: string;
|
||||
schema?: string;
|
||||
sql: string;
|
||||
pinned?: boolean;
|
||||
mode?: QueryTab["mode"];
|
||||
tableMeta?: QueryTab["tableMeta"];
|
||||
}
|
||||
|
||||
export interface RestoredOpenTabs {
|
||||
tabs: QueryTab[];
|
||||
activeTabId: string | null;
|
||||
}
|
||||
|
||||
export function serializeOpenTabs(tabs: QueryTab[]): SavedOpenTab[] {
|
||||
return tabs.map((tab) => ({
|
||||
id: tab.id,
|
||||
title: tab.title,
|
||||
connectionId: tab.connectionId,
|
||||
database: tab.database,
|
||||
schema: tab.schema,
|
||||
sql: tab.sql,
|
||||
pinned: tab.pinned,
|
||||
mode: tab.mode,
|
||||
tableMeta: tab.tableMeta,
|
||||
}));
|
||||
}
|
||||
|
||||
function isSavedOpenTab(value: unknown): value is SavedOpenTab {
|
||||
if (!value || typeof value !== "object") return false;
|
||||
const tab = value as Record<string, unknown>;
|
||||
return (
|
||||
typeof tab.id === "string" &&
|
||||
typeof tab.title === "string" &&
|
||||
typeof tab.connectionId === "string" &&
|
||||
typeof tab.database === "string" &&
|
||||
typeof tab.sql === "string"
|
||||
);
|
||||
}
|
||||
|
||||
export function restoreOpenTabsState(
|
||||
rawTabs: string | null,
|
||||
rawActiveTabId: string | null,
|
||||
options: { queryOnly?: boolean } = {},
|
||||
): RestoredOpenTabs {
|
||||
if (!rawTabs) 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 = options.queryOnly ? saved.filter((tab) => (tab.mode ?? "query") === "query") : saved;
|
||||
const tabs: QueryTab[] = filtered.map((tab) => ({
|
||||
...tab,
|
||||
mode: tab.mode ?? "query",
|
||||
isExecuting: false,
|
||||
isCancelling: false,
|
||||
isExplaining: false,
|
||||
}));
|
||||
const activeTabId = rawActiveTabId || null;
|
||||
|
||||
return {
|
||||
tabs,
|
||||
activeTabId: tabs.some((tab) => tab.id === activeTabId) ? activeTabId : tabs[0]?.id || null,
|
||||
};
|
||||
} catch {
|
||||
return { tabs: [], activeTabId: null };
|
||||
}
|
||||
}
|
||||
@@ -7,58 +7,26 @@ import { canCancelQueryExecution } from "@/lib/queryExecutionState";
|
||||
import { closeAllTabsState, closeOtherTabsState } from "@/lib/tabCloseActions";
|
||||
import { buildExplainSql, parseExplainResult } from "@/lib/explainPlan";
|
||||
import { analyzeEditableQuery, allPrimaryKeysPresent } from "@/lib/sqlAnalysis";
|
||||
import { restoreOpenTabsState, serializeOpenTabs } from "@/lib/openTabsPersistence";
|
||||
import * as api from "@/lib/api";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { isTauriRuntime } from "@/lib/tauriRuntime";
|
||||
|
||||
interface SavedTab {
|
||||
id: string;
|
||||
title: string;
|
||||
connectionId: string;
|
||||
database: string;
|
||||
sql: string;
|
||||
pinned?: boolean;
|
||||
mode: "data" | "query" | "redis" | "mongo";
|
||||
tableMeta?: QueryTab["tableMeta"];
|
||||
}
|
||||
|
||||
const STORAGE_KEY = "dbx-open-tabs";
|
||||
const ACTIVE_TAB_KEY = "dbx-active-tab";
|
||||
|
||||
function saveTabs(tabs: QueryTab[], activeTabId: string | null) {
|
||||
try {
|
||||
const saved: SavedTab[] = tabs.map((t) => ({
|
||||
id: t.id,
|
||||
title: t.title,
|
||||
connectionId: t.connectionId,
|
||||
database: t.database,
|
||||
sql: t.sql,
|
||||
pinned: t.pinned,
|
||||
mode: t.mode,
|
||||
tableMeta: t.tableMeta,
|
||||
}));
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(saved));
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(serializeOpenTabs(tabs)));
|
||||
localStorage.setItem(ACTIVE_TAB_KEY, activeTabId || "");
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function loadSavedTabs(): { tabs: QueryTab[]; activeTabId: string | null } {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return { tabs: [], activeTabId: null };
|
||||
const saved: SavedTab[] = JSON.parse(raw);
|
||||
const filtered = isTauriRuntime() ? saved.filter((s) => s.mode === "query") : saved;
|
||||
const tabs: QueryTab[] = filtered.map((s) => ({
|
||||
...s,
|
||||
isExecuting: false,
|
||||
isCancelling: false,
|
||||
isExplaining: false,
|
||||
}));
|
||||
const activeTabId = localStorage.getItem(ACTIVE_TAB_KEY) || null;
|
||||
return {
|
||||
tabs,
|
||||
activeTabId: tabs.some((t) => t.id === activeTabId) ? activeTabId : tabs[0]?.id || null,
|
||||
};
|
||||
return restoreOpenTabsState(localStorage.getItem(STORAGE_KEY), localStorage.getItem(ACTIVE_TAB_KEY), {
|
||||
queryOnly: isTauriRuntime(),
|
||||
});
|
||||
} catch {
|
||||
return { tabs: [], activeTabId: null };
|
||||
}
|
||||
@@ -70,7 +38,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
const activeTabId = ref<string | null>(restored.activeTabId);
|
||||
const MAX_CACHED_RESULTS = 10;
|
||||
|
||||
watch([tabs, activeTabId], () => saveTabs(tabs.value, activeTabId.value), { deep: true });
|
||||
watch([tabs, activeTabId], () => saveTabs(tabs.value, activeTabId.value), { deep: true, flush: "sync" });
|
||||
|
||||
function findTabByTitle(connectionId: string, database: string, title: string) {
|
||||
return tabs.value.find((t) => t.connectionId === connectionId && t.database === database && t.title === title);
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
import { restoreOpenTabsState, serializeOpenTabs } from "../src/lib/openTabsPersistence.ts";
|
||||
import type { QueryTab } from "../src/types/database.ts";
|
||||
|
||||
function queryTab(overrides: Partial<QueryTab> = {}): QueryTab {
|
||||
return {
|
||||
id: "tab-1",
|
||||
title: "Query 1",
|
||||
connectionId: "conn-1",
|
||||
database: "app",
|
||||
schema: "public",
|
||||
sql: "select * from users",
|
||||
pinned: true,
|
||||
isExecuting: false,
|
||||
isCancelling: false,
|
||||
isExplaining: false,
|
||||
mode: "query",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test("serializes unsaved query tabs with editor context", () => {
|
||||
const saved = serializeOpenTabs([queryTab()]);
|
||||
|
||||
assert.deepEqual(saved, [
|
||||
{
|
||||
id: "tab-1",
|
||||
title: "Query 1",
|
||||
connectionId: "conn-1",
|
||||
database: "app",
|
||||
schema: "public",
|
||||
sql: "select * from users",
|
||||
pinned: true,
|
||||
mode: "query",
|
||||
tableMeta: undefined,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("restores unsaved query tabs and active tab after restart", () => {
|
||||
const raw = JSON.stringify([
|
||||
queryTab({ id: "tab-1", sql: "select 1" }),
|
||||
queryTab({ id: "tab-2", title: "Query 2", sql: "select 2" }),
|
||||
]);
|
||||
|
||||
const restored = restoreOpenTabsState(raw, "tab-2");
|
||||
|
||||
assert.deepEqual(
|
||||
restored.tabs.map((tab) => ({ id: tab.id, sql: tab.sql, isExecuting: tab.isExecuting })),
|
||||
[
|
||||
{ id: "tab-1", sql: "select 1", isExecuting: false },
|
||||
{ id: "tab-2", sql: "select 2", isExecuting: false },
|
||||
],
|
||||
);
|
||||
assert.equal(restored.activeTabId, "tab-2");
|
||||
});
|
||||
|
||||
test("desktop restore keeps legacy query tabs without a mode", () => {
|
||||
const raw = JSON.stringify([
|
||||
{
|
||||
id: "legacy",
|
||||
title: "Query 1",
|
||||
connectionId: "conn-1",
|
||||
database: "app",
|
||||
sql: "select now()",
|
||||
},
|
||||
queryTab({ id: "data", title: "users", mode: "data" }),
|
||||
]);
|
||||
|
||||
const restored = restoreOpenTabsState(raw, "data", { queryOnly: true });
|
||||
|
||||
assert.deepEqual(
|
||||
restored.tabs.map((tab) => ({ id: tab.id, mode: tab.mode })),
|
||||
[{ id: "legacy", mode: "query" }],
|
||||
);
|
||||
assert.equal(restored.activeTabId, "legacy");
|
||||
});
|
||||
|
||||
test("ignores invalid persisted tab payloads", () => {
|
||||
const restored = restoreOpenTabsState(JSON.stringify([{ id: "missing-fields" }, queryTab()]), "missing-fields");
|
||||
|
||||
assert.deepEqual(
|
||||
restored.tabs.map((tab) => tab.id),
|
||||
["tab-1"],
|
||||
);
|
||||
assert.equal(restored.activeTabId, "tab-1");
|
||||
});
|
||||
Reference in New Issue
Block a user