mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 04:50:12 +00:00
fix(tabs): restore saved sql library content in detached windows
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { DetachedTabHandoff } from "@/lib/app/detachedTabHandoff";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
loadSavedSqlFile: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/backend/api", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("@/lib/backend/api")>()),
|
||||
loadSavedSqlFile: mocks.loadSavedSqlFile,
|
||||
}));
|
||||
|
||||
describe("queryStore adoptDetachedTab", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.unstubAllGlobals();
|
||||
mocks.loadSavedSqlFile.mockReset();
|
||||
vi.stubGlobal("localStorage", {
|
||||
getItem: vi.fn(() => null),
|
||||
setItem: vi.fn(),
|
||||
removeItem: vi.fn(),
|
||||
});
|
||||
setActivePinia(createPinia());
|
||||
});
|
||||
|
||||
function handoffFor(overrides: Partial<DetachedTabHandoff["tab"]> = {}): DetachedTabHandoff {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
tabId: "tab-1",
|
||||
sourceWindowLabel: "main",
|
||||
revision: 1,
|
||||
tab: {
|
||||
id: "tab-1",
|
||||
title: "query_1.sql",
|
||||
customTitle: true,
|
||||
connectionId: "conn-1",
|
||||
database: "main",
|
||||
// A clean saved-SQL-library tab serializes `sql` as "" (see openTabsPersistence's
|
||||
// shouldPersistTabSql) to avoid duplicating on-disk state across app restarts.
|
||||
sql: "",
|
||||
savedSqlId: "saved-1",
|
||||
mode: "query",
|
||||
...overrides,
|
||||
},
|
||||
runtime: {},
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
it("hydrates a clean saved-SQL-library tab's content from the backend instead of opening empty", async () => {
|
||||
mocks.loadSavedSqlFile.mockResolvedValue({
|
||||
id: "saved-1",
|
||||
connectionId: "conn-1",
|
||||
name: "query_1.sql",
|
||||
database: "main",
|
||||
sql: "SELECT 1;",
|
||||
createdAt: "2026-01-01T00:00:00Z",
|
||||
updatedAt: "2026-01-01T00:00:00Z",
|
||||
});
|
||||
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const queryStore = useQueryStore();
|
||||
|
||||
const tabId = await queryStore.adoptDetachedTab(handoffFor());
|
||||
|
||||
expect(mocks.loadSavedSqlFile).toHaveBeenCalledWith("saved-1");
|
||||
const tab = queryStore.tabs.find((candidate) => candidate.id === tabId)!;
|
||||
expect(tab.sql).toBe("SELECT 1;");
|
||||
expect(tab.originalSql).toBe("SELECT 1;");
|
||||
expect(queryStore.isTabDirty(tab)).toBe(false);
|
||||
});
|
||||
|
||||
it("does not touch the backend when the handoff already carries unsaved edits", async () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const queryStore = useQueryStore();
|
||||
|
||||
const tabId = await queryStore.adoptDetachedTab(handoffFor({ sql: "SELECT 2;", originalSql: "SELECT 1;" }));
|
||||
|
||||
expect(mocks.loadSavedSqlFile).not.toHaveBeenCalled();
|
||||
const tab = queryStore.tabs.find((candidate) => candidate.id === tabId)!;
|
||||
expect(tab.sql).toBe("SELECT 2;");
|
||||
expect(queryStore.isTabDirty(tab)).toBe(true);
|
||||
});
|
||||
|
||||
it("leaves an unsaved (never-linked-to-library) tab's empty content as-is", async () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const queryStore = useQueryStore();
|
||||
|
||||
const tabId = await queryStore.adoptDetachedTab(handoffFor({ savedSqlId: undefined }));
|
||||
|
||||
expect(mocks.loadSavedSqlFile).not.toHaveBeenCalled();
|
||||
const tab = queryStore.tabs.find((candidate) => candidate.id === tabId)!;
|
||||
expect(tab.sql).toBe("");
|
||||
});
|
||||
});
|
||||
@@ -2376,6 +2376,21 @@ export const useQueryStore = defineStore("query", () => {
|
||||
const restored = restoreOpenTabsPayload({ tabs: [handoff.tab], activeTabId: handoff.tabId });
|
||||
const restoredTab = restored.tabs[0];
|
||||
if (!restoredTab) throw new Error("Unable to restore detached tab");
|
||||
// serializeOpenTabs() blanks `sql` for a clean saved-SQL-library tab to avoid
|
||||
// duplicating on-disk state across app restarts, relying on hydrateSavedSqlTabs()
|
||||
// to refill it afterwards. The detach handoff goes through the same serialization
|
||||
// but skips that hydration step, so do it here too or the new window opens empty.
|
||||
// A freshly created detached window never runs the main-window bootstrap that
|
||||
// populates savedSqlStore's local file index, so useSavedSqlStore().ensureFileContent()
|
||||
// can't find the file locally either — go straight to the backend instead.
|
||||
if (restoredTab.savedSqlId && restoredTab.mode === "query" && !restoredTab.sql) {
|
||||
const file = await api.loadSavedSqlFile(restoredTab.savedSqlId).catch(() => undefined);
|
||||
if (file) {
|
||||
restoredTab.title = restoredTab.customTitle ? restoredTab.title : file.name;
|
||||
restoredTab.sql = file.sql;
|
||||
restoredTab.originalSql = file.sql;
|
||||
}
|
||||
}
|
||||
Object.assign(restoredTab, handoff.runtime);
|
||||
if (handoff.resultCacheKey) {
|
||||
restoredTab.resultCacheKey = handoff.resultCacheKey;
|
||||
|
||||
Reference in New Issue
Block a user