From 7376266913ca89fc18432506f4400d8f9a48dad0 Mon Sep 17 00:00:00 2001
From: 0xsysr3ll <0xsysr3ll@pm.me>
Date: Sat, 20 Jun 2026 23:19:34 +0200
Subject: [PATCH] feat(requests): add more sorting options
---
next-env.d.ts | 2 +-
seerr-api.yml | 3 +-
server/lib/requestSort.test.ts | 381 +++++++++++++++++++++++++++
server/lib/requestSort.ts | 187 +++++++++++++
server/routes/request.test.ts | 110 ++++++++
server/routes/request.ts | 55 ++--
src/components/RequestList/index.tsx | 30 ++-
src/i18n/locale/en.json | 4 +
8 files changed, 743 insertions(+), 29 deletions(-)
create mode 100644 server/lib/requestSort.test.ts
create mode 100644 server/lib/requestSort.ts
diff --git a/next-env.d.ts b/next-env.d.ts
index 19709046af..7996d352f4 100644
--- a/next-env.d.ts
+++ b/next-env.d.ts
@@ -1,6 +1,6 @@
///
///
-import "./.next/types/routes.d.ts";
+import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
diff --git a/seerr-api.yml b/seerr-api.yml
index 6d58dd3cbf..47defc6a62 100644
--- a/seerr-api.yml
+++ b/seerr-api.yml
@@ -6325,10 +6325,11 @@ paths:
name: sort
schema:
type: string
- enum: [added, modified]
+ enum: [added, modified, popularity, releaseDate, voteAverage, title]
default: added
- in: query
name: sortDirection
+ description: Sort direction for the selected sort field.
schema:
type: string
enum: [asc, desc]
diff --git a/server/lib/requestSort.test.ts b/server/lib/requestSort.test.ts
new file mode 100644
index 0000000000..05d6023b2d
--- /dev/null
+++ b/server/lib/requestSort.test.ts
@@ -0,0 +1,381 @@
+import assert from 'node:assert/strict';
+import { describe, it } from 'node:test';
+
+import type TheMovieDb from '@server/api/themoviedb';
+import { MediaType } from '@server/constants/media';
+import type { MediaRequest } from '@server/entity/MediaRequest';
+import {
+ getRequestSortColumn,
+ isTmdbRequestSort,
+ parseRequestSort,
+ sortRequestsByTmdbField,
+} from '@server/lib/requestSort';
+
+function createRequest(
+ id: number,
+ tmdbId: number,
+ type: MediaType = MediaType.MOVIE
+): MediaRequest {
+ return {
+ id,
+ type,
+ media: { tmdbId },
+ } as MediaRequest;
+}
+
+function createTmdbMock(
+ movies: Record<
+ number,
+ {
+ original_title: string;
+ popularity: number;
+ vote_average: number;
+ release_date: string;
+ }
+ > = {},
+ tv: Record<
+ number,
+ {
+ original_name: string;
+ popularity: number;
+ vote_average: number;
+ first_air_date: string;
+ }
+ > = {},
+ failingIds: Set = new Set()
+) {
+ let movieFetchCount = 0;
+ let tvFetchCount = 0;
+
+ const tmdb = {
+ getMovie: async ({ movieId }: { movieId: number }) => {
+ movieFetchCount++;
+
+ if (failingIds.has(movieId) || !movies[movieId]) {
+ throw new Error('Movie not found');
+ }
+
+ return movies[movieId];
+ },
+ getTvShow: async ({ tvId }: { tvId: number }) => {
+ tvFetchCount++;
+
+ if (failingIds.has(tvId) || !tv[tvId]) {
+ throw new Error('TV show not found');
+ }
+
+ return tv[tvId];
+ },
+ };
+
+ return {
+ tmdb: tmdb as Pick,
+ getFetchCounts: () => ({ movieFetchCount, tvFetchCount }),
+ };
+}
+
+describe('parseRequestSort', () => {
+ it('parses all documented sort fields with sortDirection', () => {
+ const cases = [
+ {
+ sort: 'added',
+ sortDirection: 'desc',
+ field: 'added',
+ direction: 'desc',
+ },
+ { sort: 'added', sortDirection: 'asc', field: 'added', direction: 'asc' },
+ {
+ sort: 'modified',
+ sortDirection: 'desc',
+ field: 'modified',
+ direction: 'desc',
+ },
+ {
+ sort: 'modified',
+ sortDirection: 'asc',
+ field: 'modified',
+ direction: 'asc',
+ },
+ {
+ sort: 'popularity',
+ sortDirection: 'desc',
+ field: 'popularity',
+ direction: 'desc',
+ },
+ {
+ sort: 'releaseDate',
+ sortDirection: 'asc',
+ field: 'releaseDate',
+ direction: 'asc',
+ },
+ {
+ sort: 'voteAverage',
+ sortDirection: 'desc',
+ field: 'voteAverage',
+ direction: 'desc',
+ },
+ { sort: 'title', sortDirection: 'asc', field: 'title', direction: 'asc' },
+ ] as const;
+
+ for (const { sort, sortDirection, field, direction } of cases) {
+ assert.deepEqual(parseRequestSort(sort, sortDirection), {
+ field,
+ direction,
+ });
+ }
+ });
+
+ it('defaults to added desc when sort or sortDirection are omitted', () => {
+ assert.deepEqual(parseRequestSort('added'), {
+ field: 'added',
+ direction: 'desc',
+ });
+ assert.deepEqual(parseRequestSort(undefined, undefined), {
+ field: 'added',
+ direction: 'desc',
+ });
+ });
+
+ it('defaults unknown sort values to added', () => {
+ assert.deepEqual(parseRequestSort('popularity.desc', 'asc'), {
+ field: 'added',
+ direction: 'asc',
+ });
+ });
+});
+
+describe('isTmdbRequestSort', () => {
+ it('identifies TMDB-backed sort fields', () => {
+ assert.equal(isTmdbRequestSort('popularity'), true);
+ assert.equal(isTmdbRequestSort('releaseDate'), true);
+ assert.equal(isTmdbRequestSort('voteAverage'), true);
+ assert.equal(isTmdbRequestSort('title'), true);
+ assert.equal(isTmdbRequestSort('added'), false);
+ assert.equal(isTmdbRequestSort('modified'), false);
+ });
+});
+
+describe('getRequestSortColumn', () => {
+ it('maps request date and modified date to database columns', () => {
+ assert.equal(getRequestSortColumn('added'), 'request.createdAt');
+ assert.equal(getRequestSortColumn('modified'), 'request.updatedAt');
+ });
+});
+
+describe('sortRequestsByTmdbField', () => {
+ it('fetches TMDB data only once per unique type-tmdbId key', async () => {
+ const { tmdb, getFetchCounts } = createTmdbMock({
+ 100: {
+ original_title: 'Shared Movie',
+ popularity: 10,
+ vote_average: 7,
+ release_date: '2020-01-01',
+ },
+ });
+
+ const requests = [
+ createRequest(1, 100),
+ createRequest(2, 100),
+ createRequest(3, 101, MediaType.TV),
+ ];
+
+ await sortRequestsByTmdbField(requests, 'title', 'asc', tmdb);
+
+ assert.equal(getFetchCounts().movieFetchCount, 1);
+ assert.equal(getFetchCounts().tvFetchCount, 1);
+ });
+
+ it('places requests without TMDB data at the end', async () => {
+ const { tmdb } = createTmdbMock({
+ 100: {
+ original_title: 'Alpha',
+ popularity: 10,
+ vote_average: 7,
+ release_date: '2020-01-01',
+ },
+ });
+
+ const requests = [
+ createRequest(1, 999),
+ createRequest(2, 100),
+ createRequest(3, 998),
+ ];
+
+ const sorted = await sortRequestsByTmdbField(
+ requests,
+ 'title',
+ 'asc',
+ tmdb
+ );
+
+ assert.deepEqual(
+ sorted.map((request) => request.id),
+ [2, 1, 3]
+ );
+ });
+
+ it('sorts by each TMDB field in ascending and descending order', async () => {
+ const { tmdb } = createTmdbMock({
+ 1: {
+ original_title: 'Alpha',
+ popularity: 10,
+ vote_average: 5,
+ release_date: '2020-01-01',
+ },
+ 2: {
+ original_title: 'Zulu',
+ popularity: 20,
+ vote_average: 9,
+ release_date: '2022-01-01',
+ },
+ });
+
+ const requests = [createRequest(2, 2), createRequest(1, 1)];
+
+ const titleAsc = await sortRequestsByTmdbField(
+ requests,
+ 'title',
+ 'asc',
+ tmdb
+ );
+ const titleDesc = await sortRequestsByTmdbField(
+ requests,
+ 'title',
+ 'desc',
+ tmdb
+ );
+ const popularityAsc = await sortRequestsByTmdbField(
+ requests,
+ 'popularity',
+ 'asc',
+ tmdb
+ );
+ const popularityDesc = await sortRequestsByTmdbField(
+ requests,
+ 'popularity',
+ 'desc',
+ tmdb
+ );
+ const voteAverageAsc = await sortRequestsByTmdbField(
+ requests,
+ 'voteAverage',
+ 'asc',
+ tmdb
+ );
+ const voteAverageDesc = await sortRequestsByTmdbField(
+ requests,
+ 'voteAverage',
+ 'desc',
+ tmdb
+ );
+ const releaseDateAsc = await sortRequestsByTmdbField(
+ requests,
+ 'releaseDate',
+ 'asc',
+ tmdb
+ );
+ const releaseDateDesc = await sortRequestsByTmdbField(
+ requests,
+ 'releaseDate',
+ 'desc',
+ tmdb
+ );
+
+ assert.deepEqual(
+ titleAsc.map((request) => request.id),
+ [1, 2]
+ );
+ assert.deepEqual(
+ titleDesc.map((request) => request.id),
+ [2, 1]
+ );
+ assert.deepEqual(
+ popularityAsc.map((request) => request.id),
+ [1, 2]
+ );
+ assert.deepEqual(
+ popularityDesc.map((request) => request.id),
+ [2, 1]
+ );
+ assert.deepEqual(
+ voteAverageAsc.map((request) => request.id),
+ [1, 2]
+ );
+ assert.deepEqual(
+ voteAverageDesc.map((request) => request.id),
+ [2, 1]
+ );
+ assert.deepEqual(
+ releaseDateAsc.map((request) => request.id),
+ [1, 2]
+ );
+ assert.deepEqual(
+ releaseDateDesc.map((request) => request.id),
+ [2, 1]
+ );
+ });
+
+ it('uses request id as a directional tie-breaker', async () => {
+ const { tmdb } = createTmdbMock({
+ 100: {
+ original_title: 'Same Title',
+ popularity: 10,
+ vote_average: 7,
+ release_date: '2020-01-01',
+ },
+ 101: {
+ original_title: 'Same Title',
+ popularity: 10,
+ vote_average: 7,
+ release_date: '2020-01-01',
+ },
+ });
+
+ const requests = [createRequest(5, 101), createRequest(2, 100)];
+
+ const asc = await sortRequestsByTmdbField(requests, 'title', 'asc', tmdb);
+ const desc = await sortRequestsByTmdbField(requests, 'title', 'desc', tmdb);
+
+ assert.deepEqual(
+ asc.map((request) => request.id),
+ [2, 5]
+ );
+ assert.deepEqual(
+ desc.map((request) => request.id),
+ [5, 2]
+ );
+ });
+
+ it('handles TMDB fetch failures gracefully', async () => {
+ const { tmdb } = createTmdbMock(
+ {
+ 100: {
+ original_title: 'Available',
+ popularity: 10,
+ vote_average: 7,
+ release_date: '2020-01-01',
+ },
+ },
+ {},
+ new Set([200])
+ );
+
+ const requests = [
+ createRequest(1, 200),
+ createRequest(2, 100),
+ createRequest(3, 200),
+ ];
+
+ const sorted = await sortRequestsByTmdbField(
+ requests,
+ 'popularity',
+ 'desc',
+ tmdb
+ );
+
+ assert.deepEqual(
+ sorted.map((request) => request.id),
+ [2, 3, 1]
+ );
+ });
+});
diff --git a/server/lib/requestSort.ts b/server/lib/requestSort.ts
new file mode 100644
index 0000000000..928024e87b
--- /dev/null
+++ b/server/lib/requestSort.ts
@@ -0,0 +1,187 @@
+import TheMovieDb from '@server/api/themoviedb';
+import { MediaType } from '@server/constants/media';
+import type { MediaRequest } from '@server/entity/MediaRequest';
+
+export type RequestSortField =
+ | 'added'
+ | 'modified'
+ | 'popularity'
+ | 'releaseDate'
+ | 'voteAverage'
+ | 'title';
+
+export type RequestSortDirection = 'asc' | 'desc';
+
+export interface ParsedRequestSort {
+ field: RequestSortField;
+ direction: RequestSortDirection;
+}
+
+const TMDB_SORT_FIELDS: RequestSortField[] = [
+ 'popularity',
+ 'releaseDate',
+ 'voteAverage',
+ 'title',
+];
+
+const SORT_FIELDS: RequestSortField[] = [
+ 'added',
+ 'modified',
+ 'popularity',
+ 'releaseDate',
+ 'voteAverage',
+ 'title',
+];
+
+export function isTmdbRequestSort(
+ field: RequestSortField
+): field is Exclude {
+ return TMDB_SORT_FIELDS.includes(field);
+}
+
+function isRequestSortField(value?: string): value is RequestSortField {
+ return SORT_FIELDS.includes(value as RequestSortField);
+}
+
+export function parseRequestSort(
+ sort?: string,
+ sortDirection?: string
+): ParsedRequestSort {
+ const field: RequestSortField = isRequestSortField(sort) ? sort : 'added';
+ const direction: RequestSortDirection =
+ sortDirection === 'asc' ? 'asc' : 'desc';
+
+ return { field, direction };
+}
+
+export type DbRequestSortField = 'added' | 'modified';
+
+export function getRequestSortColumn(field: DbRequestSortField): string {
+ switch (field) {
+ case 'modified':
+ return 'request.updatedAt';
+ default:
+ return 'request.createdAt';
+ }
+}
+
+interface MediaSortCache {
+ title: string;
+ popularity: number;
+ voteAverage: number;
+ releaseDate: string;
+}
+
+interface TmdbSortProvider {
+ getMovie: TheMovieDb['getMovie'];
+ getTvShow: TheMovieDb['getTvShow'];
+}
+
+async function fetchMediaSortCache(
+ tmdb: TmdbSortProvider,
+ mediaType: MediaType,
+ tmdbId: number
+): Promise {
+ try {
+ if (mediaType === MediaType.MOVIE) {
+ const movie = await tmdb.getMovie({ movieId: tmdbId });
+
+ return {
+ title: movie.original_title?.toLowerCase() ?? '',
+ popularity: movie.popularity ?? 0,
+ voteAverage: movie.vote_average ?? 0,
+ releaseDate: movie.release_date ?? '',
+ };
+ }
+
+ const tv = await tmdb.getTvShow({ tvId: tmdbId });
+
+ return {
+ title: tv.original_name?.toLowerCase() ?? '',
+ popularity: tv.popularity ?? 0,
+ voteAverage: tv.vote_average ?? 0,
+ releaseDate: tv.first_air_date ?? '',
+ };
+ } catch {
+ return null;
+ }
+}
+
+export async function sortRequestsByTmdbField(
+ requests: MediaRequest[],
+ field: Exclude,
+ direction: RequestSortDirection,
+ tmdb: TmdbSortProvider = new TheMovieDb()
+): Promise {
+ const uniqueMedia = new Map<
+ string,
+ { mediaType: MediaType; tmdbId: number }
+ >();
+
+ for (const request of requests) {
+ const key = `${request.type}-${request.media.tmdbId}`;
+
+ if (!uniqueMedia.has(key)) {
+ uniqueMedia.set(key, {
+ mediaType: request.type,
+ tmdbId: request.media.tmdbId,
+ });
+ }
+ }
+
+ const cacheMap = new Map();
+
+ await Promise.all(
+ [...uniqueMedia.entries()].map(async ([key, { mediaType, tmdbId }]) => {
+ const data = await fetchMediaSortCache(tmdb, mediaType, tmdbId);
+
+ if (data) {
+ cacheMap.set(key, data);
+ }
+ })
+ );
+
+ const multiplier = direction === 'asc' ? 1 : -1;
+
+ return [...requests].sort((a, b) => {
+ const keyA = `${a.type}-${a.media.tmdbId}`;
+ const keyB = `${b.type}-${b.media.tmdbId}`;
+ const cacheA = cacheMap.get(keyA);
+ const cacheB = cacheMap.get(keyB);
+
+ if (!cacheA && !cacheB) {
+ return (a.id - b.id) * multiplier;
+ }
+
+ if (!cacheA) {
+ return 1;
+ }
+
+ if (!cacheB) {
+ return -1;
+ }
+
+ let comparison = 0;
+
+ switch (field) {
+ case 'title':
+ comparison = cacheA.title.localeCompare(cacheB.title);
+ break;
+ case 'popularity':
+ comparison = cacheA.popularity - cacheB.popularity;
+ break;
+ case 'voteAverage':
+ comparison = cacheA.voteAverage - cacheB.voteAverage;
+ break;
+ case 'releaseDate':
+ comparison = cacheA.releaseDate.localeCompare(cacheB.releaseDate);
+ break;
+ }
+
+ if (comparison === 0) {
+ return (a.id - b.id) * multiplier;
+ }
+
+ return comparison * multiplier;
+ });
+}
diff --git a/server/routes/request.test.ts b/server/routes/request.test.ts
index d90f6d3b0c..e286fa2a66 100644
--- a/server/routes/request.test.ts
+++ b/server/routes/request.test.ts
@@ -117,6 +117,116 @@ async function seedRequest(status = MediaRequestStatus.PENDING) {
});
}
+describe('GET /request', () => {
+ it('sorts by request date ascending', async () => {
+ const userRepo = getRepository(User);
+ const mediaRepo = getRepository(Media);
+ const requestRepo = getRepository(MediaRequest);
+
+ const requestedBy = await userRepo.findOneOrFail({
+ where: { email: 'admin@seerr.dev' },
+ });
+
+ const media = await mediaRepo.save(
+ new Media({
+ mediaType: MediaType.MOVIE,
+ tmdbId: 77701,
+ status: MediaStatus.UNKNOWN,
+ status4k: MediaStatus.UNKNOWN,
+ })
+ );
+
+ const olderRequest = await requestRepo.save(
+ new MediaRequest({
+ type: MediaType.MOVIE,
+ status: MediaRequestStatus.PENDING,
+ media,
+ requestedBy,
+ is4k: false,
+ createdAt: new Date('2024-01-01T00:00:00.000Z'),
+ updatedAt: new Date('2024-01-01T00:00:00.000Z'),
+ })
+ );
+
+ const newerRequest = await requestRepo.save(
+ new MediaRequest({
+ type: MediaType.MOVIE,
+ status: MediaRequestStatus.PENDING,
+ media,
+ requestedBy,
+ is4k: false,
+ createdAt: new Date('2025-01-01T00:00:00.000Z'),
+ updatedAt: new Date('2025-01-01T00:00:00.000Z'),
+ })
+ );
+
+ const agent = await loginAs('admin@seerr.dev', 'test1234');
+ const res = await agent.get(
+ '/request?filter=all&sort=added&sortDirection=asc&take=10'
+ );
+
+ assert.strictEqual(res.status, 200);
+ assert.deepEqual(
+ res.body.results.map((request: { id: number }) => request.id),
+ [olderRequest.id, newerRequest.id]
+ );
+ });
+
+ it('sorts by modified date with sortDirection', async () => {
+ const userRepo = getRepository(User);
+ const mediaRepo = getRepository(Media);
+ const requestRepo = getRepository(MediaRequest);
+
+ const requestedBy = await userRepo.findOneOrFail({
+ where: { email: 'admin@seerr.dev' },
+ });
+
+ const media = await mediaRepo.save(
+ new Media({
+ mediaType: MediaType.MOVIE,
+ tmdbId: 77702,
+ status: MediaStatus.UNKNOWN,
+ status4k: MediaStatus.UNKNOWN,
+ })
+ );
+
+ const olderRequest = await requestRepo.save(
+ new MediaRequest({
+ type: MediaType.MOVIE,
+ status: MediaRequestStatus.PENDING,
+ media,
+ requestedBy,
+ is4k: false,
+ createdAt: new Date('2024-01-01T00:00:00.000Z'),
+ updatedAt: new Date('2024-01-01T00:00:00.000Z'),
+ })
+ );
+
+ const newerRequest = await requestRepo.save(
+ new MediaRequest({
+ type: MediaType.MOVIE,
+ status: MediaRequestStatus.PENDING,
+ media,
+ requestedBy,
+ is4k: false,
+ createdAt: new Date('2024-01-01T00:00:00.000Z'),
+ updatedAt: new Date('2025-01-01T00:00:00.000Z'),
+ })
+ );
+
+ const agent = await loginAs('admin@seerr.dev', 'test1234');
+ const res = await agent.get(
+ '/request?filter=all&sort=modified&sortDirection=desc&take=10'
+ );
+
+ assert.strictEqual(res.status, 200);
+ assert.deepEqual(
+ res.body.results.map((request: { id: number }) => request.id),
+ [newerRequest.id, olderRequest.id]
+ );
+ });
+});
+
describe('DELETE /request/:requestId', () => {
it('allows the owner to delete their own pending request', async () => {
const mediaRequest = await seedRequest();
diff --git a/server/routes/request.ts b/server/routes/request.ts
index fafa90692e..bf57b9d157 100644
--- a/server/routes/request.ts
+++ b/server/routes/request.ts
@@ -22,6 +22,12 @@ import type {
RequestResultsResponse,
} from '@server/interfaces/api/requestInterfaces';
import { Permission } from '@server/lib/permissions';
+import {
+ getRequestSortColumn,
+ isTmdbRequestSort,
+ parseRequestSort,
+ sortRequestsByTmdbField,
+} from '@server/lib/requestSort';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import { isAuthenticated } from '@server/middleware/auth';
@@ -103,24 +109,11 @@ requestRoutes.get, RequestResultsResponse>(
];
}
- let sortFilter: string;
- let sortDirection: 'ASC' | 'DESC';
-
- switch (req.query.sort) {
- case 'modified':
- sortFilter = 'request.updatedAt';
- break;
- default:
- sortFilter = 'request.id';
- }
-
- switch (req.query.sortDirection) {
- case 'asc':
- sortDirection = 'ASC';
- break;
- default:
- sortDirection = 'DESC';
- }
+ const { field: sortField, direction: sortDirection } = parseRequestSort(
+ req.query.sort as string | undefined,
+ req.query.sortDirection as string | undefined
+ );
+ const sortDirectionSql = sortDirection.toUpperCase() as 'ASC' | 'DESC';
let query = getRepository(MediaRequest)
.createQueryBuilder('request')
@@ -175,11 +168,27 @@ requestRoutes.get, RequestResultsResponse>(
break;
}
- const [requests, requestCount] = await query
- .orderBy(sortFilter, sortDirection)
- .take(pageSize)
- .skip(skip)
- .getManyAndCount();
+ let requests: MediaRequest[];
+ let requestCount: number;
+
+ if (isTmdbRequestSort(sortField)) {
+ const allRequests = await query.getMany();
+ const sortedRequests = await sortRequestsByTmdbField(
+ allRequests,
+ sortField,
+ sortDirection
+ );
+
+ requestCount = sortedRequests.length;
+ requests = sortedRequests.slice(skip, skip + pageSize);
+ } else {
+ [requests, requestCount] = await query
+ .orderBy(getRequestSortColumn(sortField), sortDirectionSql)
+ .addOrderBy('request.id', sortDirectionSql)
+ .take(pageSize)
+ .skip(skip)
+ .getManyAndCount();
+ }
const settings = getSettings();
diff --git a/src/components/RequestList/index.tsx b/src/components/RequestList/index.tsx
index a5e21f2320..82f123d1f0 100644
--- a/src/components/RequestList/index.tsx
+++ b/src/components/RequestList/index.tsx
@@ -30,11 +30,25 @@ const messages = defineMessages('components.RequestList', {
showallrequests: 'Show All Requests',
sortAdded: 'Most Recent',
sortModified: 'Last Modified',
+ sortPopularity: 'Popularity',
+ sortReleaseDate: 'Release / First Air Date',
+ sortTmdbRating: 'TMDB Rating',
+ sortTitle: 'Title',
sortDirection: 'Toggle Sort Direction',
unableToConnect:
'Unable to connect to {services}. Some information may be unavailable.',
});
+type Sort =
+ | 'added'
+ | 'modified'
+ | 'popularity'
+ | 'releaseDate'
+ | 'voteAverage'
+ | 'title';
+
+type SortDirection = 'asc' | 'desc';
+
enum Filter {
ALL = 'all',
PENDING = 'pending',
@@ -47,10 +61,6 @@ enum Filter {
COMPLETED = 'completed',
}
-type Sort = 'added' | 'modified';
-
-type SortDirection = 'asc' | 'desc';
-
type MediaType = 'all' | 'movie' | 'tv';
const RequestList = () => {
@@ -269,6 +279,18 @@ const RequestList = () => {
+
+
+
+