|
@@ -9,8 +9,9 @@ import {
|
|
|
getSortedRowModel,
|
|
getSortedRowModel,
|
|
|
useReactTable,
|
|
useReactTable,
|
|
|
SortingState,
|
|
SortingState,
|
|
|
|
|
+ RowSelectionState,
|
|
|
} from "@tanstack/react-table";
|
|
} from "@tanstack/react-table";
|
|
|
-import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
|
|
|
|
|
+import { useQuery } from "@tanstack/react-query";
|
|
|
|
|
|
|
|
interface FileData {
|
|
interface FileData {
|
|
|
id: string;
|
|
id: string;
|
|
@@ -22,6 +23,31 @@ interface FileData {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const columns: ColumnDef<FileData>[] = [
|
|
const columns: ColumnDef<FileData>[] = [
|
|
|
|
|
+ {
|
|
|
|
|
+ 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>
|
|
|
|
|
+ ),
|
|
|
|
|
+ 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>
|
|
|
|
|
+ ),
|
|
|
|
|
+ enableSorting: false,
|
|
|
|
|
+ enableHiding: false,
|
|
|
|
|
+ },
|
|
|
{
|
|
{
|
|
|
accessorKey: "filename",
|
|
accessorKey: "filename",
|
|
|
header: "File Name",
|
|
header: "File Name",
|
|
@@ -82,7 +108,7 @@ const columns: ColumnDef<FileData>[] = [
|
|
|
|
|
|
|
|
export function FilesTable() {
|
|
export function FilesTable() {
|
|
|
const [sorting, setSorting] = useState<SortingState>([]);
|
|
const [sorting, setSorting] = useState<SortingState>([]);
|
|
|
- const queryClient = useQueryClient();
|
|
|
|
|
|
|
+ const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
|
|
|
|
|
|
|
const { data, isLoading, isError, error, refetch } = useQuery({
|
|
const { data, isLoading, isError, error, refetch } = useQuery({
|
|
|
queryKey: ["files"],
|
|
queryKey: ["files"],
|
|
@@ -101,17 +127,48 @@ export function FilesTable() {
|
|
|
columns,
|
|
columns,
|
|
|
state: {
|
|
state: {
|
|
|
sorting,
|
|
sorting,
|
|
|
|
|
+ rowSelection,
|
|
|
},
|
|
},
|
|
|
onSortingChange: setSorting,
|
|
onSortingChange: setSorting,
|
|
|
|
|
+ onRowSelectionChange: setRowSelection,
|
|
|
getCoreRowModel: getCoreRowModel(),
|
|
getCoreRowModel: getCoreRowModel(),
|
|
|
getPaginationRowModel: getPaginationRowModel(),
|
|
getPaginationRowModel: getPaginationRowModel(),
|
|
|
getSortedRowModel: getSortedRowModel(),
|
|
getSortedRowModel: getSortedRowModel(),
|
|
|
|
|
+ enableRowSelection: true,
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
const handleRefresh = () => {
|
|
const handleRefresh = () => {
|
|
|
refetch();
|
|
refetch();
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ const handleDownload = (fileId: string, filename: string) => {
|
|
|
|
|
+ const downloadUrl = `/api/files/${fileId}`;
|
|
|
|
|
+ const link = document.createElement('a');
|
|
|
|
|
+ link.href = downloadUrl;
|
|
|
|
|
+ link.download = filename;
|
|
|
|
|
+ document.body.appendChild(link);
|
|
|
|
|
+ link.click();
|
|
|
|
|
+ document.body.removeChild(link);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleDownloadSelected = () => {
|
|
|
|
|
+ const selectedRows = table.getSelectedRowModel().rows;
|
|
|
|
|
+ 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
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
if (isLoading) {
|
|
if (isLoading) {
|
|
|
return (
|
|
return (
|
|
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200">
|
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200">
|
|
@@ -168,26 +225,48 @@ export function FilesTable() {
|
|
|
<div className="p-6">
|
|
<div className="p-6">
|
|
|
<div className="flex justify-between items-center mb-4">
|
|
<div className="flex justify-between items-center mb-4">
|
|
|
<h2 className="text-xl font-semibold text-gray-900">Files in Database</h2>
|
|
<h2 className="text-xl font-semibold text-gray-900">Files in Database</h2>
|
|
|
- <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"
|
|
|
|
|
- >
|
|
|
|
|
- <svg
|
|
|
|
|
- className={`w-4 h-4 mr-2 ${isLoading ? "animate-spin" : ""}`}
|
|
|
|
|
- fill="none"
|
|
|
|
|
- stroke="currentColor"
|
|
|
|
|
- viewBox="0 0 24 24"
|
|
|
|
|
|
|
+ <div className="flex gap-2">
|
|
|
|
|
+ <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"
|
|
|
>
|
|
>
|
|
|
- <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>
|
|
|
|
|
- Refresh
|
|
|
|
|
- </button>
|
|
|
|
|
|
|
+ <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 Selected
|
|
|
|
|
+ </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"
|
|
|
|
|
+ >
|
|
|
|
|
+ <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>
|
|
|
|
|
+ Refresh
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div className="overflow-x-auto">
|
|
<div className="overflow-x-auto">
|
|
@@ -216,7 +295,10 @@ export function FilesTable() {
|
|
|
</thead>
|
|
</thead>
|
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
|
{table.getRowModel().rows.map((row) => (
|
|
{table.getRowModel().rows.map((row) => (
|
|
|
- <tr key={row.id} className="hover:bg-gray-50">
|
|
|
|
|
|
|
+ <tr
|
|
|
|
|
+ key={row.id}
|
|
|
|
|
+ className={`hover:bg-gray-50 ${row.getIsSelected() ? 'bg-blue-50' : ''}`}
|
|
|
|
|
+ >
|
|
|
{row.getVisibleCells().map((cell) => (
|
|
{row.getVisibleCells().map((cell) => (
|
|
|
<td key={cell.id} className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
<td key={cell.id} className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|