import { useState, useCallback, useMemo } from "react"; import { useTableData } from "@/hooks/use-table-data"; import { ResultsTable } from "@/components/results/ResultsTable"; import { ResultsJsonView } from "@/components/results/ResultsJsonView"; import { PaginationControls } from "./PaginationControls"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { updateRow as updateRowApi } from "@/lib/tauri"; 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, Download, Plus, Table2, Braces } from "lucide-react"; import { InsertRowDialog } from "./InsertRowDialog"; 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; schema: string; table: string; } export function TableDataView({ connectionId, schema, table }: Props) { const readOnlyMap = useAppStore((s) => s.readOnlyMap); const isReadOnly = readOnlyMap[connectionId] ?? true; const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(50); const [sortColumn, setSortColumn] = useState(); const [sortDirection, setSortDirection] = useState(); const [filter, setFilter] = useState(""); const [appliedFilter, setAppliedFilter] = useState(); const [pendingChanges, setPendingChanges] = useState< Map >(new Map()); const [isSaving, setIsSaving] = useState(false); const [insertDialogOpen, setInsertDialogOpen] = useState(false); const [viewMode, setViewMode] = useState<"table" | "json">("table"); const queryClient = useQueryClient(); const { data, isLoading, error } = useTableData({ connectionId, schema, table, page, pageSize, sortColumn, sortDirection, filter: appliedFilter, }); const { data: columnsInfo } = useQuery({ queryKey: ["table-columns", connectionId, schema, table], queryFn: () => getTableColumns(connectionId, schema, table), }); const pkColumns = useMemo( () => columnsInfo?.filter((c) => c.is_primary_key).map((c) => c.name) ?? [], [columnsInfo] ); const highlightedCells = useMemo( () => new Set(pendingChanges.keys()), [pendingChanges] ); const handleCellDoubleClick = useCallback( (rowIndex: number, colIndex: number, value: unknown) => { if (isReadOnly) { toast.warning("Read-only mode is active", { description: "Switch to Read-Write mode to edit data.", }); return; } const key = `${rowIndex}:${colIndex}`; const currentValue = pendingChanges.get(key)?.value ?? value; const newVal = prompt("Edit value (leave empty and click NULL for null):", currentValue === null ? "" : String(currentValue)); if (newVal !== null) { setPendingChanges((prev) => { const next = new Map(prev); next.set(key, { rowIndex, colIndex, value: newVal === "" ? null : newVal }); return next; }); } }, [pendingChanges, isReadOnly] ); const usesCtid = pkColumns.length === 0; const handleCommit = async () => { if (!data) return; if (pkColumns.length === 0 && (!data.ctids || data.ctids.length === 0)) { toast.error("Cannot save: no primary key and no ctid available"); return; } setIsSaving(true); try { for (const [, change] of pendingChanges) { const row = data.rows[change.rowIndex]; const colName = data.columns[change.colIndex]; if (usesCtid) { await updateRowApi({ connectionId, schema, table, pkColumns: [], pkValues: [], column: colName, value: change.value, ctid: data.ctids[change.rowIndex], }); } else { const pkValues = pkColumns.map((pkCol) => { const idx = data.columns.indexOf(pkCol); return row[idx]; }); await updateRowApi({ connectionId, schema, table, pkColumns, pkValues: pkValues as unknown[], column: colName, value: change.value, }); } } setPendingChanges(new Map()); queryClient.invalidateQueries({ queryKey: ["table-data", connectionId], }); toast.success(`${pendingChanges.size} change(s) saved`); } catch (err) { toast.error("Save failed", { description: String(err) }); } finally { setIsSaving(false); } }; const handleRollback = () => { 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 handleSort = useCallback( (column: string | undefined, direction: string | undefined) => { setSortColumn(column); setSortDirection(direction); setPage(1); }, [] ); const handleApplyFilter = () => { setAppliedFilter(filter || undefined); setPage(1); }; return (
{isReadOnly && ( Read-Only )} {!isReadOnly && usesCtid && ( No PK — using ctid )} setFilter(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleApplyFilter()} /> {data && data.columns.length > 0 && ( <> handleExport("csv")}> Export CSV handleExport("json")}> Export JSON
)} {!isReadOnly && ( )} {pendingChanges.size > 0 && ( <> )}
{isLoading && !data ? (
Loading...
) : error ? (
{String(error)}
) : data ? ( viewMode === "json" ? ( ) : ( ) ) : null}
{data && ( { setPageSize(size); setPage(1); }} /> )} { queryClient.invalidateQueries({ queryKey: ["table-data", connectionId], }); }} />
); }