feat: Query history + saved queries (#5)
* fix: restore frontend test baseline (vitest jsdom env + tsc + mock fixes) * feat: v6 migration — favorite column + queries table (Task 1) * feat: extend TS types for favorites + saved queries (Task 2) * feat: dedup consecutive + prune to 500 in insert_query_history (Task 3) * feat: favorite column threading + set_history_favorite command (Task 4) * feat: saved queries store CRUD (Task 5) * feat: saved query commands + registration (Task 6) * feat: queryStore — Zustand cache for history + saved queries (Task 7) * feat: QueryHistoryDropdown — toolbar history dropdown (Task 8) * feat: SaveQueryDialog — save query modal (Task 9) * feat: QueryToolbar — add History + Save icons (Task 10) * feat: QueriesPanel — History + Saved tabs (Task 11) * feat: wire Queries view + toolbar to panel (Task 12) * fix: global scope wrappers, empty/spinner states, cache invalidation (Task 13) * feat: Queries view — history sidebar + tabbed query workspace (PR feedback) * fix: toolbar action order + view-specific empty state (PR feedback) * fix: view-specific empty state icon (PR feedback) * fix: portal Tooltip to body so it is never clipped by overflow containers (PR feedback) * feat: Queries sidebar — Explorer-style header + per-connection scoping (PR feedback) * fix: move History/Saved dropdown to right side of Queries header (PR feedback) * fix: History/Saved dropdown on its own row in Queries header (PR feedback) * fix: Queries header order (search above dropdown) + sidebar width matches Explorer (PR feedback) * fix: Queries header spacing — tight rows, pb-3 on container (PR feedback) * fix: Queries header spacing — space-y-2 on container, no mb on title row (PR feedback) * feat: history/saved rows click-to-load, remove sub-buttons (PR feedback) * feat: merge Functions/Triggers/Sequences/Enums/Extensions into single Objects view (PR feedback) * fix: Schema Visualizer nav icon — node graph glyph (PR feedback) * fix: Objects view — type dropdown replaces static header label (PR feedback) * fix: Objects empty state — remove bg circle, larger icon (PR feedback) * fix: center icon in Objects empty state (PR feedback) * feat: merge Backup/Restore/DB Sync into single Tools view (PR feedback) * docs: update AGENTS.md + README for query history/saved queries, merged Objects + Tools views
This commit is contained in:
+134
-1
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||
|
||||
vi.mock("@tauri-apps/api/core", () => ({
|
||||
invoke: vi.fn().mockResolvedValue({ tables: [], relationships: [] }),
|
||||
@@ -19,8 +19,14 @@ import {
|
||||
executeQuery,
|
||||
getQueryHistory,
|
||||
clearQueryHistory,
|
||||
setHistoryFavorite,
|
||||
saveQuery,
|
||||
getSavedQueries,
|
||||
updateSavedQuery,
|
||||
deleteSavedQuery,
|
||||
} from "./commands";
|
||||
import type { SchemaGraph } from "./types";
|
||||
import type { QueryHistoryEntry } from "./commands";
|
||||
|
||||
describe("commands", () => {
|
||||
it("testConnection has correct signature", () => {
|
||||
@@ -102,4 +108,131 @@ describe("query commands", () => {
|
||||
await clearQueryHistory("conn-1");
|
||||
expect(invoke).toHaveBeenCalledWith("clear_query_history", { connectionId: "conn-1" });
|
||||
});
|
||||
|
||||
it("getQueryHistory with null calls invoke with null connectionId", async () => {
|
||||
vi.mocked(invoke).mockResolvedValueOnce([]);
|
||||
await getQueryHistory(null, 50, 0);
|
||||
expect(invoke).toHaveBeenCalledWith("get_query_history", {
|
||||
connectionId: null,
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("clearQueryHistory with null clears all", async () => {
|
||||
vi.mocked(invoke).mockResolvedValueOnce(undefined);
|
||||
await clearQueryHistory(null);
|
||||
expect(invoke).toHaveBeenCalledWith("clear_query_history", {
|
||||
connectionId: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Query History — v6", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("QueryHistoryEntry includes favorite field", () => {
|
||||
const entry: QueryHistoryEntry = {
|
||||
id: "h1",
|
||||
connection_id: "c1",
|
||||
query_text: "SELECT 1",
|
||||
execution_time_ms: 42,
|
||||
row_count: 5,
|
||||
status: "success",
|
||||
error_message: null,
|
||||
executed_at: "2026-01-01T00:00:00Z",
|
||||
favorite: false, // NEW — must be accepted
|
||||
};
|
||||
expect(entry.favorite).toBe(false);
|
||||
});
|
||||
|
||||
it("setHistoryFavorite calls invoke with correct params", async () => {
|
||||
const mockInvoke = vi.fn().mockResolvedValue(undefined);
|
||||
vi.mocked(invoke).mockImplementation(mockInvoke);
|
||||
|
||||
await setHistoryFavorite("entry-id-1", "conn-abc");
|
||||
expect(mockInvoke).toHaveBeenCalledWith("set_history_favorite", {
|
||||
id: "entry-id-1",
|
||||
connectionId: "conn-abc",
|
||||
});
|
||||
});
|
||||
|
||||
it("saveQuery calls invoke with correct params", async () => {
|
||||
const mockInvoke = vi.fn().mockResolvedValue(undefined);
|
||||
vi.mocked(invoke).mockImplementation(mockInvoke);
|
||||
|
||||
await saveQuery({
|
||||
connectionId: "conn-xyz",
|
||||
name: "My Saved Query",
|
||||
queryText: "SELECT * FROM users",
|
||||
folder: "reports",
|
||||
});
|
||||
expect(mockInvoke).toHaveBeenCalledWith("save_query", {
|
||||
connectionId: "conn-xyz",
|
||||
name: "My Saved Query",
|
||||
queryText: "SELECT * FROM users",
|
||||
folder: "reports",
|
||||
});
|
||||
});
|
||||
|
||||
it("saveQuery accepts null connectionId for global queries", async () => {
|
||||
const mockInvoke = vi.fn().mockResolvedValue(undefined);
|
||||
vi.mocked(invoke).mockImplementation(mockInvoke);
|
||||
|
||||
await saveQuery({
|
||||
connectionId: null,
|
||||
name: "Global query",
|
||||
queryText: "SELECT 1",
|
||||
folder: "",
|
||||
});
|
||||
expect(mockInvoke).toHaveBeenCalledWith("save_query", {
|
||||
connectionId: null,
|
||||
name: "Global query",
|
||||
queryText: "SELECT 1",
|
||||
folder: "",
|
||||
});
|
||||
});
|
||||
|
||||
it("getSavedQueries calls invoke with correct params", async () => {
|
||||
const mockInvoke = vi.fn().mockResolvedValue([]);
|
||||
vi.mocked(invoke).mockImplementation(mockInvoke);
|
||||
|
||||
await getSavedQueries("conn-123");
|
||||
expect(mockInvoke).toHaveBeenCalledWith("get_saved_queries", {
|
||||
connectionId: "conn-123",
|
||||
});
|
||||
});
|
||||
|
||||
it("getSavedQueries accepts null for all-connections", async () => {
|
||||
const mockInvoke = vi.fn().mockResolvedValue([]);
|
||||
vi.mocked(invoke).mockImplementation(mockInvoke);
|
||||
|
||||
await getSavedQueries(null);
|
||||
expect(mockInvoke).toHaveBeenCalledWith("get_saved_queries", {
|
||||
connectionId: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("updateSavedQuery calls invoke with correct params", async () => {
|
||||
const mockInvoke = vi.fn().mockResolvedValue(undefined);
|
||||
vi.mocked(invoke).mockImplementation(mockInvoke);
|
||||
|
||||
await updateSavedQuery("q-id", { name: "Renamed" });
|
||||
expect(mockInvoke).toHaveBeenCalledWith("update_saved_query", {
|
||||
id: "q-id",
|
||||
patch: { name: "Renamed" },
|
||||
});
|
||||
});
|
||||
|
||||
it("deleteSavedQuery calls invoke with correct params", async () => {
|
||||
const mockInvoke = vi.fn().mockResolvedValue(undefined);
|
||||
vi.mocked(invoke).mockImplementation(mockInvoke);
|
||||
|
||||
await deleteSavedQuery("q-id");
|
||||
expect(mockInvoke).toHaveBeenCalledWith("delete_saved_query", {
|
||||
id: "q-id",
|
||||
});
|
||||
});
|
||||
});
|
||||
+59
-2
@@ -159,6 +159,30 @@ export interface QueryHistoryEntry {
|
||||
status: string;
|
||||
error_message: string | null;
|
||||
executed_at: string;
|
||||
favorite: boolean; // NEW — v6
|
||||
}
|
||||
|
||||
export interface SavedQuery {
|
||||
id: string;
|
||||
connection_id: string | null;
|
||||
name: string;
|
||||
query_text: string;
|
||||
folder: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface SaveQueryInput {
|
||||
connectionId: string | null;
|
||||
name: string;
|
||||
queryText: string;
|
||||
folder: string;
|
||||
}
|
||||
|
||||
export interface UpdateSavedQueryPatch {
|
||||
name?: string;
|
||||
queryText?: string;
|
||||
folder?: string;
|
||||
}
|
||||
|
||||
export async function executeQuery(
|
||||
@@ -171,13 +195,46 @@ export async function executeQuery(
|
||||
}
|
||||
|
||||
export async function getQueryHistory(
|
||||
connectionId: string,
|
||||
connectionId: string | null,
|
||||
limit: number,
|
||||
offset: number,
|
||||
): Promise<QueryHistoryEntry[]> {
|
||||
return invoke<QueryHistoryEntry[]>("get_query_history", { connectionId, limit, offset });
|
||||
}
|
||||
|
||||
export async function clearQueryHistory(connectionId: string): Promise<void> {
|
||||
export async function clearQueryHistory(connectionId: string | null): Promise<void> {
|
||||
return invoke<void>("clear_query_history", { connectionId });
|
||||
}
|
||||
|
||||
export async function setHistoryFavorite(
|
||||
id: string,
|
||||
connectionId: string,
|
||||
): Promise<void> {
|
||||
return invoke<void>("set_history_favorite", { id, connectionId });
|
||||
}
|
||||
|
||||
export async function saveQuery(input: SaveQueryInput): Promise<SavedQuery> {
|
||||
return invoke<SavedQuery>("save_query", {
|
||||
connectionId: input.connectionId,
|
||||
name: input.name,
|
||||
queryText: input.queryText,
|
||||
folder: input.folder,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getSavedQueries(
|
||||
connectionId: string | null,
|
||||
): Promise<SavedQuery[]> {
|
||||
return invoke<SavedQuery[]>("get_saved_queries", { connectionId });
|
||||
}
|
||||
|
||||
export async function updateSavedQuery(
|
||||
id: string,
|
||||
patch: UpdateSavedQueryPatch,
|
||||
): Promise<void> {
|
||||
return invoke<void>("update_saved_query", { id, patch });
|
||||
}
|
||||
|
||||
export async function deleteSavedQuery(id: string): Promise<void> {
|
||||
return invoke<void>("delete_saved_query", { id });
|
||||
}
|
||||
@@ -372,6 +372,7 @@ describe("Schema graph types", () => {
|
||||
is_pk: false,
|
||||
is_fk: true,
|
||||
is_unique: false,
|
||||
is_nullable: false,
|
||||
fk_ref: ["public", "users", "id"],
|
||||
};
|
||||
expect(col.name).toBe("user_id");
|
||||
@@ -385,8 +386,8 @@ describe("Schema graph types", () => {
|
||||
schema: "public",
|
||||
table_type: "TABLE",
|
||||
columns: [
|
||||
{ name: "id", data_type: "integer", is_pk: true, is_fk: false, is_unique: true, fk_ref: null },
|
||||
{ name: "user_id", data_type: "integer", is_pk: false, is_fk: true, is_unique: false, fk_ref: ["public", "users", "id"] },
|
||||
{ name: "id", data_type: "integer", is_pk: true, is_fk: false, is_unique: true, is_nullable: false, fk_ref: null },
|
||||
{ name: "user_id", data_type: "integer", is_pk: false, is_fk: true, is_unique: false, is_nullable: false, fk_ref: ["public", "users", "id"] },
|
||||
],
|
||||
};
|
||||
expect(node.name).toBe("orders");
|
||||
@@ -424,6 +425,7 @@ describe("Schema graph types", () => {
|
||||
is_pk: false,
|
||||
is_fk: false,
|
||||
is_unique: false,
|
||||
is_nullable: true,
|
||||
fk_ref: null,
|
||||
};
|
||||
expect(col.fk_ref).toBeNull();
|
||||
|
||||
Reference in New Issue
Block a user