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
+46
View File
@@ -115,3 +115,49 @@ def test_stat_widget_unknown_stat_returns_error():
with patch("media_library_viewer_api.widgets.sources.get_stats_provider", return_value=provider):
data = asyncio.run(src.fetch(_service(), "stat", {"stat": "nope"}))
assert "error" in data
def test_jellyseer_client_requests_maps_fields():
"""requests() paginates /request and maps type/status/media_status enums."""
from media_library_viewer_api.clients.jellyseerr import JellyseerrClient
c = JellyseerrClient("https://js.example.com", "key")
c.session = MagicMock()
resp = MagicMock()
resp.raise_for_status.return_value = None
resp.status_code = 200
resp.text = ""
resp.json.return_value = {
"results": [
{
"id": 7,
"type": 1,
"title": "Inception",
"status": 1,
"media": {"status": 5},
"createdAt": 1_700_000_000,
}
]
}
c.session.get.return_value = resp
out = c.requests(500)
assert len(out) == 1
r = out[0]
assert r["id"] == 7
assert r["type"] == "movie" # 1 -> movie
assert r["name"] == "Inception"
assert r["status"] == "pending" # 1 -> pending
assert r["media_status"] == "available" # 5 -> available
assert r["created_at"] == 1_700_000_000
# Single short page -> no second fetch.
assert c.session.get.call_count == 1
def test_fetch_jellyseer_requests_not_configured_returns_empty():
"""No jellyseerr config -> empty list (tab shows 'No requests')."""
from media_library_viewer_api.widgets.jellyseerr_stats import fetch_jellyseer_requests
service = ServiceRecord(id="s", service_type="jellyfin", name="JF", config={}, secrets={})
assert fetch_jellyseer_requests(service) == []