feat(jellyseer): sortable/filterable requests table on the Requests tab

Replace the static "recent requests" list with a proper table of all Jellyseerr
requests, sorted by date added (newest first by default) with standard sorting
and filtering.

Backend:
- JellyseerrClient.requests(max_count=500): paginated GET /api/v1/request
  (sort=added), mapped with type (movie/tv), status, media_status, and
  created_at labels. Returns up to 500 so the table can sort/filter client-side.
- fetch_jellyseer_requests(service) reuses the per-service cached client
  (shared with the stats widgets).
- new GET /api/jellyseerr/requests endpoint.

Frontend:
- JellyseerRequestsTable: TanStack Table (sorting via getSortedRowModel,
  pagination via getPaginationRowModel) reusing the Table primitives +
  TablePagination. Columns: Name / Type / Status / Media / Requested, all
  sortable; default sort Requested desc. A search box filters by name and a
  status dropdown defaults to "Open" (pending+approved+processing) with
  All/Pending/Approved/Declined options. (The shared DataTable is deliberately
  visibility-only, so this is a dedicated sortable table.)
- RequestsTab renders the stats grid + the new table (the compact recent list
  stays on the Requests overview widget).
- useJellyseerRequests hook + fetchJellyseerRequests API client.

Tests: client requests() mapping + single-page stop; fetch helper not-configured;
RequestsTab test mocks both hooks. 404/404 backend + 184/184 frontend pass;
build (tsc -b && vite build) + ESLint clean.
This commit is contained in:
Developer
2026-07-12 17:18:21 +00:00
parent 54851779fb
commit 7665ef4d10
21 changed files with 447 additions and 77 deletions
@@ -24,6 +24,7 @@ _MEDIA_STATUS: dict[int, str] = {
4: "partially_available",
5: "available",
}
_REQUEST_TYPE: dict[int, str] = {1: "movie", 2: "tv"}
def _label(value: Any, table: dict[int, str]) -> str:
@@ -195,3 +196,41 @@ class JellyseerrClient:
}
)
return mapped
def requests(self, max_count: int = 500) -> list[dict[str, Any]]:
"""Return requests (paginated), mapped for the requests table.
Fetches up to ``max_count`` requests (no status filter, so the table
can filter/sort client-side). The table defaults to showing "open"
(pending/approved/processing) sorted by date added (newest first).
"""
max_count = max(1, min(int(max_count), 1000))
results: list[dict[str, Any]] = []
take = 100
skip = 0
while skip < max_count:
payload = self.get("/request", sort="added", skip=skip, take=take)
if not isinstance(payload, dict):
break
page = payload.get("results") or []
items = [r for r in page if isinstance(r, dict)] if isinstance(page, list) else []
for r in items:
media = r.get("media") or {}
results.append(
{
"id": r.get("id"),
"type": _label(r.get("type"), _REQUEST_TYPE),
"name": r.get("title") or (media or {}).get("title") or (media or {}).get("name") or "",
"status": _label(r.get("status"), _REQUEST_STATUS),
"media_status": _label((media or {}).get("status"), _MEDIA_STATUS),
"created_at": r.get("createdAt"),
}
)
if len(items) < take:
break
skip += len(items)
if len(results) >= max_count:
results = results[:max_count]
break
logger.info("Jellyseerr returned %s requests", len(results))
return results