/** * JellyseerRequestsTable — sortable/filterable table of Jellyseerr requests. * * Uses TanStack Table directly (the shared DataTable is deliberately * visibility-only). Defaults: status filter = "open" (pending/approved/ * processing), sorted by date added (newest first). Client-side sort + filter * + pagination over the backend's fetched set. */ import { useMemo, useState } from "react"; import { type ColumnDef, type SortingState, flexRender, getCoreRowModel, getPaginationRowModel, getSortedRowModel, useReactTable, } from "@tanstack/react-table"; import { ArrowDown, ArrowUp, ChevronsUpDown, Search } from "lucide-react"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Skeleton } from "@/components/ui/skeleton"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { TablePagination } from "@/components/ui/table-pagination"; import { useJellyseerRequests } from "../hooks/useJellyseer"; import type { JellyseerRequest } from "../api/jellyseerr"; const OPEN_STATUSES = new Set(["pending", "approved", "processing"]); type StatusFilter = "open" | "pending" | "approved"; function formatDate(v?: number | string): string { if (!v) return "—"; const n = Number(v); const ms = n > 1e12 ? n : n * 1000; // seconds -> ms const d = new Date(ms); return Number.isNaN(d.getTime()) ? String(v) : d.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric", }); } const columns: ColumnDef[] = [ { accessorKey: "name", header: "Name", cell: ({ row }) => ( {row.original.name ?? "—"} ), }, { accessorKey: "type", header: "Type", cell: ({ row }) => ( {row.original.type ?? "—"} ), }, { accessorKey: "status", header: "Status", cell: ({ row }) => ( {row.original.status ?? "—"} ), }, { accessorKey: "media_status", header: "Media", cell: ({ row }) => row.original.media_status ? ( {row.original.media_status} ) : ( "—" ), }, { accessorKey: "created_at", header: "Requested", cell: ({ row }) => formatDate(row.original.created_at), sortDescFirst: true, }, ]; export function JellyseerRequestsTable({ serviceId }: { serviceId: string }) { const { data: requests = [], isLoading, error, } = useJellyseerRequests(serviceId); const [sorting, setSorting] = useState([ { id: "created_at", desc: true }, ]); const [search, setSearch] = useState(""); const [statusFilter, setStatusFilter] = useState("open"); const filtered = useMemo(() => { const q = search.trim().toLowerCase(); return requests.filter((r) => { const status = String(r.status ?? ""); if (statusFilter === "open") { if (!OPEN_STATUSES.has(status)) return false; } else if (status !== statusFilter) { return false; } if ( q && !String(r.name ?? "") .toLowerCase() .includes(q) ) return false; return true; }); }, [requests, statusFilter, search]); /* eslint-disable react-hooks/incompatible-library -- TanStack's useReactTable intentionally returns non-memoizable updater fns (controlled state). */ const table = useReactTable({ data: filtered, columns, state: { sorting }, onSortingChange: setSorting, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getPaginationRowModel: getPaginationRowModel(), initialState: { pagination: { pageSize: 10 } }, }); if (isLoading) { return ; } if (error) { return ( {error.message} ); } return (
setSearch(e.target.value)} className="pl-8" />
{table.getHeaderGroups().map((hg) => ( {hg.headers.map((header) => ( {header.isPlaceholder ? null : ( )} ))} ))} {table.getRowModel().rows.length ? ( table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => ( {flexRender( cell.column.columnDef.cell, cell.getContext(), )} ))} )) ) : ( No requests. )}
); }