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:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user