import type { Doc } from "@fileone/convex/_generated/dataModel" import { type DirectoryHandle, type FileHandle, type FileSystemHandle, type FileSystemItem, FileType, isSameHandle, newDirectoryHandle, newFileHandle, newFileSystemHandle, } from "@fileone/convex/model/filesystem" import { Link, useNavigate } from "@tanstack/react-router" import { type ColumnDef, flexRender, getCoreRowModel, type Row, type Table as TableType, useReactTable, } from "@tanstack/react-table" import { type PrimitiveAtom, useSetAtom, useStore } from "jotai" import { useContext, useEffect, useMemo, useRef } from "react" import { DirectoryIcon } from "@/components/icons/directory-icon" import { Checkbox } from "@/components/ui/checkbox" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" import { isControlOrCommandKeyActive, keyboardModifierAtom, } from "@/lib/keyboard" import { TextFileIcon } from "../../components/icons/text-file-icon" import { type FileDragInfo, useFileDrop } from "../../files/use-file-drop" import { cn } from "../../lib/utils" import { DirectoryPageContext } from "./context" type DirectoryContentTableProps = { filterFn: (item: FileSystemItem) => boolean directoryUrlFn: (directory: Doc<"directories">) => string fileDragInfoAtom: PrimitiveAtom onContextMenu: ( row: Row, table: TableType, ) => void onOpenFile: (file: Doc<"files">) => void } function formatFileSize(bytes: number): string { if (bytes === 0) return "0 B" const k = 1024 const sizes = ["B", "KB", "MB", "GB", "TB", "PB"] const i = Math.floor(Math.log(bytes) / Math.log(k)) return `${parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}` } function useTableColumns( onOpenFile: (file: Doc<"files">) => void, directoryUrlFn: (directory: Doc<"directories">) => string, ): ColumnDef[] { return useMemo( () => [ { id: "select", header: ({ table }) => ( { table.toggleAllPageRowsSelected(!!value) }} aria-label="Select all" /> ), cell: ({ row }) => ( { e.stopPropagation() }} onCheckedChange={row.getToggleSelectedHandler()} aria-label="Select row" /> ), enableSorting: false, enableHiding: false, size: 24, }, { header: "Name", accessorKey: "doc.name", cell: ({ row }) => { switch (row.original.kind) { case FileType.File: return ( ) case FileType.Directory: return ( ) } }, size: 1000, }, { header: "Size", accessorKey: "size", cell: ({ row }) => { switch (row.original.kind) { case FileType.File: return (
{formatFileSize(row.original.doc.size)}
) case FileType.Directory: return
-
} }, }, { header: "Created At", accessorKey: "createdAt", cell: ({ row }) => { return (
{new Date( row.original.doc.createdAt, ).toLocaleString()}
) }, }, ], [onOpenFile, directoryUrlFn], ) } export function DirectoryContentTable({ filterFn, directoryUrlFn, onContextMenu, fileDragInfoAtom, onOpenFile, }: DirectoryContentTableProps) { const { directoryContent } = useContext(DirectoryPageContext) const store = useStore() const navigate = useNavigate() const table = useReactTable({ data: directoryContent || [], columns: useTableColumns(onOpenFile, directoryUrlFn), getCoreRowModel: getCoreRowModel(), enableRowSelection: true, enableGlobalFilter: true, globalFilterFn: (row, _columnId, _filterValue, _addMeta) => filterFn(row.original), getRowId: (row) => row.doc._id, }) useEffect( function escapeToClearSelections() { const handleEscape = (e: KeyboardEvent) => { if (e.key === "Escape") { table.setRowSelection({}) } } window.addEventListener("keydown", handleEscape) return () => window.removeEventListener("keydown", handleEscape) }, [table.setRowSelection], ) const handleRowContextMenu = ( row: Row, _event: React.MouseEvent, ) => { if (!row.getIsSelected()) { selectRow(row) } onContextMenu(row, table) } const selectRow = (row: Row) => { const keyboardModifiers = store.get(keyboardModifierAtom) const isMultiSelectMode = isControlOrCommandKeyActive(keyboardModifiers) const isRowSelected = row.getIsSelected() if (isRowSelected && isMultiSelectMode) { row.toggleSelected(false) } else if (isRowSelected && !isMultiSelectMode) { table.setRowSelection({ [row.id]: true, }) row.toggleSelected(true) } else if (!isRowSelected) { if (isMultiSelectMode) { row.toggleSelected(true) } else { table.setRowSelection({ [row.id]: true, }) } } } const handleRowDoubleClick = (row: Row) => { if (row.original.kind === FileType.Directory) { navigate({ to: `/directories/${row.original.doc._id}`, }) } } return (
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( {header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext(), )} ))} ))} {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( selectRow(row)} fileDragInfoAtom={fileDragInfoAtom} onContextMenu={(e) => handleRowContextMenu(row, e) } onDoubleClick={() => { handleRowDoubleClick(row) }} /> )) ) : ( )}
) } function NoResultsRow() { return ( No results. ) } function FileItemRow({ table, row, onClick, onContextMenu, onDoubleClick, fileDragInfoAtom, }: { table: TableType row: Row onClick: () => void onContextMenu: (e: React.MouseEvent) => void onDoubleClick: () => void fileDragInfoAtom: PrimitiveAtom }) { const ref = useRef(null) const setFileDragInfo = useSetAtom(fileDragInfoAtom) const { isDraggedOver, dropHandlers } = useFileDrop({ destItem: row.original.kind === FileType.Directory ? newDirectoryHandle(row.original.doc._id) : null, dragInfoAtom: fileDragInfoAtom, }) const handleDragStart = (_e: React.DragEvent) => { let source: DirectoryHandle | FileHandle switch (row.original.kind) { case FileType.File: source = newFileHandle(row.original.doc._id) break case FileType.Directory: source = newDirectoryHandle(row.original.doc._id) break } let draggedItems: FileSystemHandle[] // drag all selections, but only if the currently dragged row is also selected if (row.getIsSelected()) { draggedItems = table .getSelectedRowModel() .rows.map((row) => newFileSystemHandle(row.original)) if (!draggedItems.some((item) => isSameHandle(item, source))) { draggedItems.push(source) } } else { draggedItems = [source] } setFileDragInfo({ source, items: draggedItems, }) } const handleDragEnd = () => { setFileDragInfo(null) } return ( {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} ) } function DirectoryNameCell({ directory, directoryUrlFn, }: { directory: Doc<"directories"> directoryUrlFn: (directory: Doc<"directories">) => string }) { return (
{directory.name}
) } function FileNameCell({ file, onOpenFile, }: { file: Doc<"files"> onOpenFile: (file: Doc<"files">) => void }) { return (
) }