|
|
@@ -12,6 +12,19 @@ import {
|
|
|
RowSelectionState,
|
|
|
} from "@tanstack/react-table";
|
|
|
import { useQuery } from "@tanstack/react-query";
|
|
|
+import { Button } from "@/components/ui/button";
|
|
|
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
|
+import { Checkbox } from "@/components/ui/checkbox";
|
|
|
+import {
|
|
|
+ Table,
|
|
|
+ TableBody,
|
|
|
+ TableCell,
|
|
|
+ TableHead,
|
|
|
+ TableHeader,
|
|
|
+ TableRow,
|
|
|
+} from "@/components/ui/table";
|
|
|
+import { Skeleton } from "@/components/ui/skeleton";
|
|
|
+import { AlertCircle, Download, Trash2, RefreshCw } from "lucide-react";
|
|
|
|
|
|
interface FileData {
|
|
|
id: string;
|
|
|
@@ -26,7 +39,7 @@ interface FilesTableProps {
|
|
|
onFileAdded?: (file: FileData) => void;
|
|
|
}
|
|
|
|
|
|
-export function FilesTable({ }: FilesTableProps) {
|
|
|
+export function FilesTable({}: FilesTableProps) {
|
|
|
const [sorting, setSorting] = useState<SortingState>([]);
|
|
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
|
|
const [files, setFiles] = useState<FileData[]>([]);
|
|
|
@@ -54,24 +67,21 @@ export function FilesTable({ }: FilesTableProps) {
|
|
|
{
|
|
|
id: "select",
|
|
|
header: ({ table }) => (
|
|
|
- <div className="px-6 py-3">
|
|
|
- <input
|
|
|
- type="checkbox"
|
|
|
- checked={table.getIsAllPageRowsSelected()}
|
|
|
- onChange={(e) => table.toggleAllPageRowsSelected(!!e.target.checked)}
|
|
|
- className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
|
|
- />
|
|
|
- </div>
|
|
|
+ <Checkbox
|
|
|
+ checked={
|
|
|
+ table.getIsAllPageRowsSelected() ||
|
|
|
+ (table.getIsSomePageRowsSelected() && "indeterminate")
|
|
|
+ }
|
|
|
+ onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
|
|
+ aria-label="Select all"
|
|
|
+ />
|
|
|
),
|
|
|
cell: ({ row }) => (
|
|
|
- <div className="px-6 py-4">
|
|
|
- <input
|
|
|
- type="checkbox"
|
|
|
- checked={row.getIsSelected()}
|
|
|
- onChange={(e) => row.toggleSelected(!!e.target.checked)}
|
|
|
- className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
|
|
- />
|
|
|
- </div>
|
|
|
+ <Checkbox
|
|
|
+ checked={row.getIsSelected()}
|
|
|
+ onCheckedChange={(value) => row.toggleSelected(!!value)}
|
|
|
+ aria-label="Select row"
|
|
|
+ />
|
|
|
),
|
|
|
enableSorting: false,
|
|
|
enableHiding: false,
|
|
|
@@ -80,14 +90,14 @@ export function FilesTable({ }: FilesTableProps) {
|
|
|
accessorKey: "filename",
|
|
|
header: "File Name",
|
|
|
cell: ({ row }) => (
|
|
|
- <div className="font-medium text-gray-900">{row.getValue("filename")}</div>
|
|
|
+ <div className="font-medium">{row.getValue("filename")}</div>
|
|
|
),
|
|
|
},
|
|
|
{
|
|
|
accessorKey: "mimetype",
|
|
|
header: "Type",
|
|
|
cell: ({ row }) => (
|
|
|
- <div className="text-sm text-gray-600">{row.getValue("mimetype")}</div>
|
|
|
+ <div className="text-sm text-muted-foreground">{row.getValue("mimetype")}</div>
|
|
|
),
|
|
|
},
|
|
|
{
|
|
|
@@ -137,44 +147,22 @@ export function FilesTable({ }: FilesTableProps) {
|
|
|
header: "Actions",
|
|
|
cell: ({ row }) => (
|
|
|
<div className="flex gap-2">
|
|
|
- <button
|
|
|
+ <Button
|
|
|
+ variant="ghost"
|
|
|
+ size="sm"
|
|
|
onClick={() => handleDownload(row.original.id, row.original.filename)}
|
|
|
- className="text-green-600 hover:text-green-800 transition-colors"
|
|
|
title="Download file"
|
|
|
>
|
|
|
- <svg
|
|
|
- className="w-5 h-5"
|
|
|
- fill="none"
|
|
|
- stroke="currentColor"
|
|
|
- viewBox="0 0 24 24"
|
|
|
- >
|
|
|
- <path
|
|
|
- strokeLinecap="round"
|
|
|
- strokeLinejoin="round"
|
|
|
- strokeWidth={2}
|
|
|
- d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
|
|
- />
|
|
|
- </svg>
|
|
|
- </button>
|
|
|
- <button
|
|
|
+ <Download className="h-4 w-4" />
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ variant="ghost"
|
|
|
+ size="sm"
|
|
|
onClick={() => handleDeleteFile(row.original.id, row.original.filename)}
|
|
|
- className="text-red-600 hover:text-red-800 transition-colors"
|
|
|
title="Delete file"
|
|
|
>
|
|
|
- <svg
|
|
|
- className="w-5 h-5"
|
|
|
- fill="none"
|
|
|
- stroke="currentColor"
|
|
|
- viewBox="0 0 24 24"
|
|
|
- >
|
|
|
- <path
|
|
|
- strokeLinecap="round"
|
|
|
- strokeLinejoin="round"
|
|
|
- strokeWidth={2}
|
|
|
- d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
|
|
- />
|
|
|
- </svg>
|
|
|
- </button>
|
|
|
+ <Trash2 className="h-4 w-4" />
|
|
|
+ </Button>
|
|
|
</div>
|
|
|
),
|
|
|
enableSorting: false,
|
|
|
@@ -215,15 +203,13 @@ export function FilesTable({ }: FilesTableProps) {
|
|
|
if (selectedRows.length === 0) return;
|
|
|
|
|
|
if (selectedRows.length === 1) {
|
|
|
- // Single file download
|
|
|
const selected = selectedRows[0];
|
|
|
handleDownload(selected.original.id, selected.original.filename);
|
|
|
} else {
|
|
|
- // Multiple files - download each one
|
|
|
selectedRows.forEach((row, index) => {
|
|
|
setTimeout(() => {
|
|
|
handleDownload(row.original.id, row.original.filename);
|
|
|
- }, index * 500); // Stagger downloads by 500ms to avoid browser blocking
|
|
|
+ }, index * 500);
|
|
|
});
|
|
|
}
|
|
|
};
|
|
|
@@ -239,12 +225,6 @@ export function FilesTable({ }: FilesTableProps) {
|
|
|
if (!confirmDelete) return;
|
|
|
|
|
|
try {
|
|
|
- const fileIds = selectedRows.map(row => row.original.id);
|
|
|
- const fileNames = selectedRows.map(row => row.original.filename);
|
|
|
-
|
|
|
- console.log(`Deleting ${fileIds.length} files:`, fileNames);
|
|
|
-
|
|
|
- // Process deletions sequentially to avoid race conditions
|
|
|
const results = [];
|
|
|
for (const row of selectedRows) {
|
|
|
try {
|
|
|
@@ -254,14 +234,11 @@ export function FilesTable({ }: FilesTableProps) {
|
|
|
|
|
|
if (!response.ok) {
|
|
|
const errorText = await response.text();
|
|
|
- console.error(`Failed to delete ${row.original.filename}:`, errorText);
|
|
|
results.push({ id: row.original.id, success: false, error: errorText });
|
|
|
} else {
|
|
|
- console.log(`Successfully deleted: ${row.original.filename}`);
|
|
|
results.push({ id: row.original.id, success: true });
|
|
|
}
|
|
|
} catch (error) {
|
|
|
- console.error(`Error deleting ${row.original.filename}:`, error);
|
|
|
results.push({ id: row.original.id, success: false, error: String(error) });
|
|
|
}
|
|
|
}
|
|
|
@@ -270,12 +247,11 @@ export function FilesTable({ }: FilesTableProps) {
|
|
|
const failed = results.filter(r => !r.success).length;
|
|
|
|
|
|
if (failed > 0) {
|
|
|
- alert(`${successful} file(s) deleted successfully, ${failed} failed. Check console for details.`);
|
|
|
+ alert(`${successful} file(s) deleted successfully, ${failed} failed.`);
|
|
|
} else {
|
|
|
alert(`${successful} file(s) deleted successfully.`);
|
|
|
}
|
|
|
|
|
|
- // Clear selection and refresh the table
|
|
|
setRowSelection({});
|
|
|
refetch();
|
|
|
|
|
|
@@ -311,188 +287,160 @@ export function FilesTable({ }: FilesTableProps) {
|
|
|
|
|
|
if (isLoading) {
|
|
|
return (
|
|
|
- <div className="bg-white rounded-lg shadow-sm border border-gray-200">
|
|
|
- <div className="p-6">
|
|
|
- <div className="flex justify-between items-center mb-4">
|
|
|
- <h2 className="text-xl font-semibold text-gray-900">Files in Database</h2>
|
|
|
- <div className="h-8 w-24 bg-gray-200 rounded animate-pulse"></div>
|
|
|
- </div>
|
|
|
+ <Card>
|
|
|
+ <CardHeader>
|
|
|
+ <CardTitle>Files in Database</CardTitle>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
<div className="space-y-3">
|
|
|
{[...Array(5)].map((_, i) => (
|
|
|
- <div key={i} className="h-12 bg-gray-100 rounded animate-pulse"></div>
|
|
|
+ <div key={i} className="flex items-center space-x-4">
|
|
|
+ <Skeleton className="h-12 w-full" />
|
|
|
+ </div>
|
|
|
))}
|
|
|
</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
if (isError) {
|
|
|
return (
|
|
|
- <div className="bg-red-50 border border-red-200 rounded-lg p-6">
|
|
|
- <div className="flex items-center">
|
|
|
- <svg className="w-5 h-5 text-red-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
|
|
- <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
|
|
- </svg>
|
|
|
- <span className="text-red-700">Error: {error?.message || "Failed to load files"}</span>
|
|
|
- </div>
|
|
|
- <button
|
|
|
- onClick={handleRefresh}
|
|
|
- className="mt-3 px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors"
|
|
|
- >
|
|
|
- Retry
|
|
|
- </button>
|
|
|
- </div>
|
|
|
+ <Card className="border-red-200">
|
|
|
+ <CardContent className="pt-6">
|
|
|
+ <div className="flex items-center text-red-700">
|
|
|
+ <AlertCircle className="h-5 w-5 mr-2" />
|
|
|
+ <span>Error: {error?.message || "Failed to load files"}</span>
|
|
|
+ </div>
|
|
|
+ <Button onClick={handleRefresh} className="mt-3" variant="outline">
|
|
|
+ Retry
|
|
|
+ </Button>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
if (!data || data.length === 0) {
|
|
|
return (
|
|
|
- <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
|
- <div className="text-center">
|
|
|
- <svg className="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 13h6m-3-3v6m-9 1V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z" />
|
|
|
- </svg>
|
|
|
- <h3 className="mt-2 text-sm font-medium text-gray-900">No files found</h3>
|
|
|
- <p className="mt-1 text-sm text-gray-500">Upload some files to get started.</p>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <Card>
|
|
|
+ <CardContent className="pt-6">
|
|
|
+ <div className="text-center">
|
|
|
+ <div className="mx-auto h-12 w-12 text-gray-400">
|
|
|
+ <svg fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 13h6m-3-3v6m-9 1V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z" />
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ <h3 className="mt-2 text-sm font-medium">No files found</h3>
|
|
|
+ <p className="mt-1 text-sm text-muted-foreground">Upload some files to get started.</p>
|
|
|
+ </div>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
return (
|
|
|
- <div className="bg-white rounded-lg shadow-sm border border-gray-200">
|
|
|
- <div className="p-6">
|
|
|
- <div className="flex justify-between items-center mb-4">
|
|
|
- <h2 className="text-xl font-semibold text-gray-900">Files in Database</h2>
|
|
|
+ <Card>
|
|
|
+ <CardHeader>
|
|
|
+ <div className="flex justify-between items-center">
|
|
|
+ <CardTitle>Files in Database</CardTitle>
|
|
|
<div className="flex gap-2">
|
|
|
- <button
|
|
|
+ <Button
|
|
|
onClick={handleDownloadSelected}
|
|
|
disabled={table.getSelectedRowModel().rows.length === 0 || isLoading}
|
|
|
- className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center"
|
|
|
+ variant="outline"
|
|
|
+ size="sm"
|
|
|
>
|
|
|
- <svg
|
|
|
- className="w-4 h-4 mr-2"
|
|
|
- fill="none"
|
|
|
- stroke="currentColor"
|
|
|
- viewBox="0 0 24 24"
|
|
|
- >
|
|
|
- <path
|
|
|
- strokeLinecap="round"
|
|
|
- strokeLinejoin="round"
|
|
|
- strokeWidth={2}
|
|
|
- d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
|
|
- />
|
|
|
- </svg>
|
|
|
+ <Download className="h-4 w-4 mr-2" />
|
|
|
Download Selected
|
|
|
- </button>
|
|
|
- <button
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
onClick={handleDeleteSelected}
|
|
|
disabled={table.getSelectedRowModel().rows.length === 0 || isLoading}
|
|
|
- className="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center"
|
|
|
+ variant="destructive"
|
|
|
+ size="sm"
|
|
|
>
|
|
|
- <svg
|
|
|
- className="w-4 h-4 mr-2"
|
|
|
- fill="none"
|
|
|
- stroke="currentColor"
|
|
|
- viewBox="0 0 24 24"
|
|
|
- >
|
|
|
- <path
|
|
|
- strokeLinecap="round"
|
|
|
- strokeLinejoin="round"
|
|
|
- strokeWidth={2}
|
|
|
- d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
|
|
- />
|
|
|
- </svg>
|
|
|
+ <Trash2 className="h-4 w-4 mr-2" />
|
|
|
Delete Selected
|
|
|
- </button>
|
|
|
- <button
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
onClick={handleRefresh}
|
|
|
disabled={isLoading}
|
|
|
- className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center"
|
|
|
+ variant="outline"
|
|
|
+ size="sm"
|
|
|
>
|
|
|
- <svg
|
|
|
- className={`w-4 h-4 mr-2 ${isLoading ? "animate-spin" : ""}`}
|
|
|
- fill="none"
|
|
|
- stroke="currentColor"
|
|
|
- viewBox="0 0 24 24"
|
|
|
- >
|
|
|
- <path
|
|
|
- strokeLinecap="round"
|
|
|
- strokeLinejoin="round"
|
|
|
- strokeWidth={2}
|
|
|
- d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
|
|
- />
|
|
|
- </svg>
|
|
|
+ <RefreshCw className={`h-4 w-4 mr-2 ${isLoading ? "animate-spin" : ""}`} />
|
|
|
Refresh
|
|
|
- </button>
|
|
|
+ </Button>
|
|
|
</div>
|
|
|
</div>
|
|
|
-
|
|
|
- <div className="overflow-x-auto">
|
|
|
- <table className="min-w-full divide-y divide-gray-200">
|
|
|
- <thead className="bg-gray-50">
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <div className="rounded-md border">
|
|
|
+ <Table>
|
|
|
+ <TableHeader>
|
|
|
{table.getHeaderGroups().map((headerGroup) => (
|
|
|
- <tr key={headerGroup.id}>
|
|
|
+ <TableRow key={headerGroup.id}>
|
|
|
{headerGroup.headers.map((header) => (
|
|
|
- <th
|
|
|
+ <TableHead
|
|
|
key={header.id}
|
|
|
- className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
|
|
|
onClick={header.column.getToggleSortingHandler()}
|
|
|
+ className="cursor-pointer"
|
|
|
>
|
|
|
- <div className="flex items-center">
|
|
|
- {flexRender(header.column.columnDef.header, header.getContext())}
|
|
|
- {header.column.getIsSorted() && (
|
|
|
- <span className="ml-1">
|
|
|
- {header.column.getIsSorted() === "asc" ? "↑" : "↓"}
|
|
|
- </span>
|
|
|
- )}
|
|
|
- </div>
|
|
|
- </th>
|
|
|
+ {flexRender(
|
|
|
+ header.column.columnDef.header,
|
|
|
+ header.getContext()
|
|
|
+ )}
|
|
|
+ {header.column.getIsSorted() && (
|
|
|
+ <span className="ml-1">
|
|
|
+ {header.column.getIsSorted() === "asc" ? "↑" : "↓"}
|
|
|
+ </span>
|
|
|
+ )}
|
|
|
+ </TableHead>
|
|
|
))}
|
|
|
- </tr>
|
|
|
+ </TableRow>
|
|
|
))}
|
|
|
- </thead>
|
|
|
- <tbody className="bg-white divide-y divide-gray-200">
|
|
|
+ </TableHeader>
|
|
|
+ <TableBody>
|
|
|
{table.getRowModel().rows.map((row) => (
|
|
|
- <tr
|
|
|
+ <TableRow
|
|
|
key={row.id}
|
|
|
- className={`hover:bg-gray-50 ${row.getIsSelected() ? 'bg-blue-50' : ''}`}
|
|
|
+ data-state={row.getIsSelected() && "selected"}
|
|
|
>
|
|
|
{row.getVisibleCells().map((cell) => (
|
|
|
- <td key={cell.id} className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
|
+ <TableCell key={cell.id}>
|
|
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
|
- </td>
|
|
|
+ </TableCell>
|
|
|
))}
|
|
|
- </tr>
|
|
|
+ </TableRow>
|
|
|
))}
|
|
|
- </tbody>
|
|
|
- </table>
|
|
|
+ </TableBody>
|
|
|
+ </Table>
|
|
|
</div>
|
|
|
|
|
|
- {/* Pagination */}
|
|
|
<div className="flex items-center justify-between mt-4">
|
|
|
- <div className="text-sm text-gray-700">
|
|
|
- Showing {table.getRowModel().rows.length} of {files.length} results
|
|
|
+ <div className="text-sm text-muted-foreground">
|
|
|
+ {table.getRowModel().rows.length} of {files.length} row(s) selected
|
|
|
</div>
|
|
|
<div className="flex gap-2">
|
|
|
- <button
|
|
|
+ <Button
|
|
|
onClick={() => table.previousPage()}
|
|
|
disabled={!table.getCanPreviousPage()}
|
|
|
- className="px-3 py-1 text-sm bg-gray-100 text-gray-700 rounded hover:bg-gray-200 disabled:opacity-50"
|
|
|
+ variant="outline"
|
|
|
+ size="sm"
|
|
|
>
|
|
|
Previous
|
|
|
- </button>
|
|
|
- <button
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
onClick={() => table.nextPage()}
|
|
|
disabled={!table.getCanNextPage()}
|
|
|
- className="px-3 py-1 text-sm bg-gray-100 text-gray-700 rounded hover:bg-gray-200 disabled:opacity-50"
|
|
|
+ variant="outline"
|
|
|
+ size="sm"
|
|
|
>
|
|
|
Next
|
|
|
- </button>
|
|
|
+ </Button>
|
|
|
</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
);
|
|
|
}
|