From 95c94704118c2129f2984d1ffac5b2c00df8409e Mon Sep 17 00:00:00 2001 From: Aleksey Shakhmatov Date: Thu, 7 May 2026 00:21:27 +0300 Subject: [PATCH] feat: scope Saved Queries panel to active connection + clean up on delete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two changes that close the "everything lives in one global pool" gap. UI: Saved Queries sidebar now defaults to "This connection" and lists only entries with `connection_id == activeConnectionId` plus unattached entries (legacy global saves before F2). A small toggle ("All") above the search box brings the previous behavior back when copying queries between databases. Each row in "All" mode shows a tag with the source connection's name; legacy global entries show "unattached". Backend: delete_connection now best-effort cleans persisted state for that connection — removes `memory/.md` (delete_memory_core in memory.rs) and drops every entry in saved_queries.json with the matching `connection_id` (delete_by_connection_core in saved_queries.rs). Entries with `connection_id == None` are deliberately preserved. Cleanup errors are logged but don't block the deletion since connections.json is the source of truth. Memory was already per-connection (F1); query history already filters by connection. This commit makes saved queries behave the same and stops orphan files from accumulating. --- src-tauri/src/commands/connections.rs | 10 + src-tauri/src/commands/memory.rs | 15 ++ src-tauri/src/commands/saved_queries.rs | 23 +++ .../saved-queries/SavedQueriesPanel.tsx | 172 ++++++++++++++---- 4 files changed, 183 insertions(+), 37 deletions(-) diff --git a/src-tauri/src/commands/connections.rs b/src-tauri/src/commands/connections.rs index 31a01a9..38eaa61 100644 --- a/src-tauri/src/commands/connections.rs +++ b/src-tauri/src/commands/connections.rs @@ -118,6 +118,16 @@ pub async fn delete_connection( fs::write(&path, data)?; } close_connection(&state, &id).await; + // Best-effort cleanup of per-connection persisted state; errors are logged + // but don't block the deletion (the connections.json is the source of truth). + if let Err(e) = crate::commands::memory::delete_memory_core(&app, &id) { + log::warn!("failed to delete memory file for {}: {}", id, e); + } + if let Err(e) = + crate::commands::saved_queries::delete_by_connection_core(&app, &id).await + { + log::warn!("failed to clean saved queries for {}: {}", id, e); + } Ok(()) } diff --git a/src-tauri/src/commands/memory.rs b/src-tauri/src/commands/memory.rs index 01664c3..4674a05 100644 --- a/src-tauri/src/commands/memory.rs +++ b/src-tauri/src/commands/memory.rs @@ -146,6 +146,21 @@ pub(crate) fn enforce_size_cap(content: &str, cap: usize) -> String { out } +/// Best-effort delete of a connection's memory file. Returns Ok(()) when the +/// file doesn't exist; only surfaces an error for actual filesystem failures. +/// Used by delete_connection to keep memory/ from filling up with orphan files. +pub(crate) fn delete_memory_core( + app: &AppHandle, + connection_id: &str, +) -> TuskResult<()> { + let path = get_memory_path(app, connection_id)?; + if !path.exists() { + return Ok(()); + } + fs::remove_file(&path)?; + Ok(()) +} + #[tauri::command] pub async fn get_memory(app: AppHandle, connection_id: String) -> TuskResult { read_memory_core(&app, &connection_id) diff --git a/src-tauri/src/commands/saved_queries.rs b/src-tauri/src/commands/saved_queries.rs index 951b029..5462ece 100644 --- a/src-tauri/src/commands/saved_queries.rs +++ b/src-tauri/src/commands/saved_queries.rs @@ -54,6 +54,29 @@ pub(crate) async fn save_query_core(app: &AppHandle, query: SavedQuery) -> TuskR Ok(()) } +/// Drop every saved-query entry tied to a specific connection_id. Entries with +/// `connection_id == None` (older "global" saves) are deliberately preserved +/// so they remain visible across all connections. +pub(crate) async fn delete_by_connection_core( + app: &AppHandle, + connection_id: &str, +) -> TuskResult<()> { + let path = get_saved_queries_path(app)?; + if !path.exists() { + return Ok(()); + } + let data = fs::read_to_string(&path)?; + let mut entries: Vec = serde_json::from_str(&data).unwrap_or_default(); + let before = entries.len(); + entries.retain(|e| e.connection_id.as_deref() != Some(connection_id)); + if entries.len() == before { + return Ok(()); + } + let data = serde_json::to_string_pretty(&entries)?; + fs::write(&path, data)?; + Ok(()) +} + #[tauri::command] pub async fn list_saved_queries( app: AppHandle, diff --git a/src/components/saved-queries/SavedQueriesPanel.tsx b/src/components/saved-queries/SavedQueriesPanel.tsx index fb4cda7..32cefdb 100644 --- a/src/components/saved-queries/SavedQueriesPanel.tsx +++ b/src/components/saved-queries/SavedQueriesPanel.tsx @@ -1,19 +1,41 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; import { useSavedQueries, useDeleteSavedQuery } from "@/hooks/use-saved-queries"; import { useAppStore } from "@/stores/app-store"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Search, Trash2, Bookmark } from "lucide-react"; -import type { Tab } from "@/types"; +import type { SavedQuery, Tab } from "@/types"; + +type Scope = "active" | "all"; export function SavedQueriesPanel() { const [search, setSearch] = useState(""); - const { activeConnectionId, currentDatabase, addTab } = useAppStore(); + const [scope, setScope] = useState("active"); + + const { activeConnectionId, currentDatabase, addTab, connections } = + useAppStore(); const { data: queries } = useSavedQueries(search || undefined); const deleteMutation = useDeleteSavedQuery(); - const handleOpen = (sql: string, connectionId?: string) => { - const cid = activeConnectionId ?? connectionId ?? ""; + // Effective scope: if no connection is active, "active" has nothing to filter + // against, so we silently broaden to all. + const effectiveScope: Scope = activeConnectionId ? scope : "all"; + + const visible = useMemo(() => { + if (!queries) return []; + if (effectiveScope === "all") return queries; + return queries.filter( + (q) => !q.connection_id || q.connection_id === activeConnectionId + ); + }, [queries, effectiveScope, activeConnectionId]); + + const connectionName = (id: string | undefined): string | undefined => { + if (!id) return undefined; + return connections.find((c) => c.id === id)?.name ?? id.slice(0, 8); + }; + + const handleOpen = (sql: string, queryConnectionId?: string) => { + const cid = activeConnectionId ?? queryConnectionId ?? ""; if (!cid) return; const tab: Tab = { id: crypto.randomUUID(), @@ -28,7 +50,7 @@ export function SavedQueriesPanel() { return (
-
+
setSearch(e.target.value)} />
+ {activeConnectionId && ( +
+ setScope("active")} + > + This connection + + setScope("all")} + > + All + +
+ )}
- {queries?.map((query) => ( -
( + handleOpen(query.sql, query.connection_id)} - > -
- - - {query.name} - - -
- - {query.sql.length > 80 ? query.sql.slice(0, 80) + "..." : query.sql} - - - {new Date(query.created_at).toLocaleDateString()} - -
+ query={query} + showConnectionTag={effectiveScope === "all"} + connectionLabel={connectionName(query.connection_id)} + onOpen={() => handleOpen(query.sql, query.connection_id)} + onDelete={() => deleteMutation.mutate(query.id)} + /> ))} - {(!queries || queries.length === 0) && ( + {visible.length === 0 && (
- No saved queries + {effectiveScope === "active" + ? "No saved queries for this connection" + : "No saved queries"}
)}
); } + +function ScopeButton({ + active, + onClick, + children, +}: { + active: boolean; + onClick: () => void; + children: React.ReactNode; +}) { + return ( + + ); +} + +function QueryRow({ + query, + showConnectionTag, + connectionLabel, + onOpen, + onDelete, +}: { + query: SavedQuery; + showConnectionTag: boolean; + connectionLabel?: string; + onOpen: () => void; + onDelete: () => void; +}) { + const tag = !query.connection_id + ? "unattached" + : showConnectionTag + ? connectionLabel + : null; + + return ( +
+
+ + + {query.name} + + {tag && ( + + {tag} + + )} + +
+ + {query.sql.length > 80 ? query.sql.slice(0, 80) + "..." : query.sql} + + + {new Date(query.created_at).toLocaleDateString()} + +
+ ); +}