|
|
@@ -122,6 +122,118 @@ export function FilesTable() {
|
|
|
},
|
|
|
});
|
|
|
|
|
|
+ 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",
|
|
|
+ header: "File Name",
|
|
|
+ cell: ({ row }) => (
|
|
|
+ <div className="font-medium text-gray-900">{row.getValue("filename")}</div>
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ accessorKey: "mimetype",
|
|
|
+ header: "Type",
|
|
|
+ cell: ({ row }) => (
|
|
|
+ <div className="text-sm text-gray-600">{row.getValue("mimetype")}</div>
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ accessorKey: "size",
|
|
|
+ header: "Size",
|
|
|
+ cell: ({ row }) => {
|
|
|
+ const bytes = row.getValue("size") as number;
|
|
|
+ if (bytes === 0) return "0 Bytes";
|
|
|
+
|
|
|
+ const k = 1024;
|
|
|
+ const sizes = ["Bytes", "KB", "MB", "GB"];
|
|
|
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
|
+
|
|
|
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ accessorKey: "createdAt",
|
|
|
+ header: "Created",
|
|
|
+ cell: ({ row }) => {
|
|
|
+ const date = new Date(row.getValue("createdAt"));
|
|
|
+ return date.toLocaleDateString("en-US", {
|
|
|
+ year: "numeric",
|
|
|
+ month: "short",
|
|
|
+ day: "numeric",
|
|
|
+ hour: "2-digit",
|
|
|
+ minute: "2-digit",
|
|
|
+ });
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ accessorKey: "updatedAt",
|
|
|
+ header: "Updated",
|
|
|
+ cell: ({ row }) => {
|
|
|
+ const date = new Date(row.getValue("updatedAt"));
|
|
|
+ return date.toLocaleDateString("en-US", {
|
|
|
+ year: "numeric",
|
|
|
+ month: "short",
|
|
|
+ day: "numeric",
|
|
|
+ hour: "2-digit",
|
|
|
+ minute: "2-digit",
|
|
|
+ });
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: "actions",
|
|
|
+ header: "Actions",
|
|
|
+ cell: ({ row }) => (
|
|
|
+ <div className="flex gap-2">
|
|
|
+ <button
|
|
|
+ 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>
|
|
|
+ </div>
|
|
|
+ ),
|
|
|
+ enableSorting: false,
|
|
|
+ },
|
|
|
+ ];
|
|
|
+
|
|
|
const table = useReactTable({
|
|
|
data: data || [],
|
|
|
columns,
|
|
|
@@ -169,6 +281,65 @@ export function FilesTable() {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+ const handleDeleteSelected = async () => {
|
|
|
+ const selectedRows = table.getSelectedRowModel().rows;
|
|
|
+ if (selectedRows.length === 0) return;
|
|
|
+
|
|
|
+ const confirmDelete = window.confirm(
|
|
|
+ `Are you sure you want to delete ${selectedRows.length} file${selectedRows.length > 1 ? 's' : ''}? This action cannot be undone.`
|
|
|
+ );
|
|
|
+
|
|
|
+ if (!confirmDelete) return;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const deletePromises = selectedRows.map(async (row) => {
|
|
|
+ const response = await fetch(`/api/files/${row.original.id}`, {
|
|
|
+ method: 'DELETE',
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error(`Failed to delete file: ${row.original.filename}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ return row.original.id;
|
|
|
+ });
|
|
|
+
|
|
|
+ await Promise.all(deletePromises);
|
|
|
+
|
|
|
+ // Clear selection and refresh the table
|
|
|
+ setRowSelection({});
|
|
|
+ refetch();
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error deleting files:', error);
|
|
|
+ alert('Failed to delete some files. Please try again.');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleDeleteFile = async (fileId: string, filename: string) => {
|
|
|
+ const confirmDelete = window.confirm(
|
|
|
+ `Are you sure you want to delete "${filename}"? This action cannot be undone.`
|
|
|
+ );
|
|
|
+
|
|
|
+ if (!confirmDelete) return;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await fetch(`/api/files/${fileId}`, {
|
|
|
+ method: 'DELETE',
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error('Failed to delete file');
|
|
|
+ }
|
|
|
+
|
|
|
+ refetch();
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error deleting file:', error);
|
|
|
+ alert('Failed to delete file. Please try again.');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
if (isLoading) {
|
|
|
return (
|
|
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200">
|
|
|
@@ -246,6 +417,26 @@ export function FilesTable() {
|
|
|
</svg>
|
|
|
Download Selected
|
|
|
</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"
|
|
|
+ >
|
|
|
+ <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>
|
|
|
+ Delete Selected
|
|
|
+ </button>
|
|
|
<button
|
|
|
onClick={handleRefresh}
|
|
|
disabled={isLoading}
|