feat: add CSV/JSON export buttons to query results and table data view

Export dropdown (CSV/JSON) appears in the WorkspacePanel toolbar when
query results are available, and in the TableDataView toolbar for table
data. Uses the Tauri save dialog for file path selection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-11 21:23:07 +03:00
parent 3b3e225e8f
commit ebc6a7e51a
2 changed files with 111 additions and 2 deletions

View File

@@ -9,7 +9,15 @@ import { getTableColumns } from "@/lib/tauri";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useAppStore } from "@/stores/app-store";
import { toast } from "sonner";
import { Save, RotateCcw, Filter, Loader2, Lock } from "lucide-react";
import { Save, RotateCcw, Filter, Loader2, Lock, Download } from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { exportCsv, exportJson } from "@/lib/tauri";
import { save } from "@tauri-apps/plugin-dialog";
interface Props {
connectionId: string;
@@ -123,6 +131,30 @@ export function TableDataView({ connectionId, schema, table }: Props) {
setPendingChanges(new Map());
};
const handleExport = useCallback(
async (format: "csv" | "json") => {
if (!data || data.columns.length === 0) return;
const ext = format === "csv" ? "csv" : "json";
const path = await save({
title: `Export as ${ext.toUpperCase()}`,
defaultPath: `${table}.${ext}`,
filters: [{ name: ext.toUpperCase(), extensions: [ext] }],
});
if (!path) return;
try {
if (format === "csv") {
await exportCsv(path, data.columns, data.rows as unknown[][]);
} else {
await exportJson(path, data.columns, data.rows as unknown[][]);
}
toast.success(`Exported to ${path}`);
} catch (err) {
toast.error("Export failed", { description: String(err) });
}
},
[data, table]
);
const handleApplyFilter = () => {
setAppliedFilter(filter || undefined);
setPage(1);
@@ -153,6 +185,28 @@ export function TableDataView({ connectionId, schema, table }: Props) {
>
Apply
</Button>
{data && data.columns.length > 0 && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="sm"
variant="ghost"
className="h-6 gap-1 text-xs"
>
<Download className="h-3 w-3" />
Export
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => handleExport("csv")}>
Export CSV
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleExport("json")}>
Export JSON
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
{pendingChanges.size > 0 && (
<>
<Button

View File

@@ -13,7 +13,16 @@ import { useCompletionSchema } from "@/hooks/use-completion-schema";
import { useConnections } from "@/hooks/use-connections";
import { useAppStore } from "@/stores/app-store";
import { Button } from "@/components/ui/button";
import { Play, Loader2, Lock, BarChart3 } from "lucide-react";
import { Play, Loader2, Lock, BarChart3, Download } from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { exportCsv, exportJson } from "@/lib/tauri";
import { save } from "@tauri-apps/plugin-dialog";
import { toast } from "sonner";
import type { QueryResult, ExplainResult } from "@/types";
interface Props {
@@ -141,6 +150,30 @@ export function WorkspacePanel({
);
}, [connectionId, sqlValue, queryMutation]);
const handleExport = useCallback(
async (format: "csv" | "json") => {
if (!result || result.columns.length === 0) return;
const ext = format === "csv" ? "csv" : "json";
const path = await save({
title: `Export as ${ext.toUpperCase()}`,
defaultPath: `query_result.${ext}`,
filters: [{ name: ext.toUpperCase(), extensions: [ext] }],
});
if (!path) return;
try {
if (format === "csv") {
await exportCsv(path, result.columns, result.rows as unknown[][]);
} else {
await exportJson(path, result.columns, result.rows as unknown[][]);
}
toast.success(`Exported to ${path}`);
} catch (err) {
toast.error("Export failed", { description: String(err) });
}
},
[result]
);
return (
<ResizablePanelGroup orientation="vertical">
<ResizablePanel id="editor" defaultSize="40%" minSize="15%">
@@ -174,6 +207,28 @@ export function WorkspacePanel({
)}
Explain
</Button>
{result && result.columns.length > 0 && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="sm"
variant="ghost"
className="h-6 gap-1 text-xs"
>
<Download className="h-3 w-3" />
Export
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => handleExport("csv")}>
Export CSV
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleExport("json")}>
Export JSON
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
<span className="text-[11px] text-muted-foreground">
Ctrl+Enter to execute
</span>