From 24b06c76fb4cb9e8bb53e462095dd95aabb8708f Mon Sep 17 00:00:00 2001 From: Derock Date: Thu, 11 May 2023 01:32:13 -0400 Subject: [PATCH] feat: server-side sorting (#366) Co-authored-by: Jayvin Hernandez Co-authored-by: dicedtomato <35403473+diced@users.noreply.github.com> --- src/components/pages/Dashboard/index.tsx | 36 ++++++++------------- src/components/pages/Files/FilePagation.tsx | 4 ++- src/components/pages/Files/index.tsx | 5 ++- src/lib/queries/files.ts | 22 +++++++++---- src/pages/api/user/paged.ts | 31 ++++++++++++++++-- 5 files changed, 65 insertions(+), 33 deletions(-) diff --git a/src/components/pages/Dashboard/index.tsx b/src/components/pages/Dashboard/index.tsx index 4e0a6f1..8de2918 100644 --- a/src/components/pages/Dashboard/index.tsx +++ b/src/components/pages/Dashboard/index.tsx @@ -12,7 +12,7 @@ import { import FileModal from 'components/File/FileModal'; import MutedText from 'components/MutedText'; import useFetch from 'lib/hooks/useFetch'; -import { usePaginatedFiles, useRecent } from 'lib/queries/files'; +import { PaginatedFilesOptions, usePaginatedFiles, useRecent } from 'lib/queries/files'; import { useStats } from 'lib/queries/stats'; import { userSelector } from 'lib/recoil/user'; import { bytesToHuman } from 'lib/utils/bytes'; @@ -45,32 +45,24 @@ export default function Dashboard({ disableMediaPreview, exifEnabled, compress } })(); }, [page]); - const files = usePaginatedFiles(page, 'none'); - // sorting const [sortStatus, setSortStatus] = useState({ - columnAccessor: 'date', + columnAccessor: 'createdAt', direction: 'asc', }); - const [records, setRecords] = useState(files.data); - useEffect(() => { - setRecords(files.data); - }, [files.data]); + const files = usePaginatedFiles(page, { + filter: 'none', - useEffect(() => { - if (!records || records.length === 0) return; - - const sortedRecords = [...records].sort((a, b) => { - if (sortStatus.direction === 'asc') { - return a[sortStatus.columnAccessor] > b[sortStatus.columnAccessor] ? 1 : -1; - } - - return a[sortStatus.columnAccessor] < b[sortStatus.columnAccessor] ? 1 : -1; - }); - - setRecords(sortedRecords); - }, [sortStatus]); + // only query for correct results if there is more than one page + // otherwise, querying has no effect + ...(numFiles > 1 + ? { + sortBy: sortStatus.columnAccessor as PaginatedFilesOptions['sortBy'], + order: sortStatus.direction, + } + : {}), + }); // file modal on click const [open, setOpen] = useState(false); @@ -203,7 +195,7 @@ export default function Dashboard({ disableMediaPreview, exifEnabled, compress } ), }, ]} - records={records ?? []} + records={files.data ?? []} fetching={files.isLoading} loaderBackgroundBlur={5} loaderVariant='dots' diff --git a/src/components/pages/Files/FilePagation.tsx b/src/components/pages/Files/FilePagation.tsx index 57aba25..7303980 100644 --- a/src/components/pages/Files/FilePagation.tsx +++ b/src/components/pages/Files/FilePagation.tsx @@ -37,7 +37,9 @@ export default function FilePagation({ disableMediaPreview, exifEnabled, queryPa })(); }, [page]); - const pages = usePaginatedFiles(page, !checked ? 'media' : null); + const pages = usePaginatedFiles(page, { + filter: !checked ? 'media' : 'none', + }); if (pages.isSuccess && pages.data.length === 0) { if (page > 1 && numPages > 0) { diff --git a/src/components/pages/Files/index.tsx b/src/components/pages/Files/index.tsx index 9a03bbe..abde034 100644 --- a/src/components/pages/Files/index.tsx +++ b/src/components/pages/Files/index.tsx @@ -11,7 +11,10 @@ import PendingFilesModal from './PendingFilesModal'; export default function Files({ disableMediaPreview, exifEnabled, queryPage, compress }) { const [favoritePage, setFavoritePage] = useState(1); const [favoriteNumPages, setFavoriteNumPages] = useState(0); - const favoritePages = usePaginatedFiles(favoritePage, 'media', true); + const favoritePages = usePaginatedFiles(favoritePage, { + filter: 'media', + favorite: true, + }); const [open, setOpen] = useState(false); diff --git a/src/lib/queries/files.ts b/src/lib/queries/files.ts index 47cbfdb..6740c91 100644 --- a/src/lib/queries/files.ts +++ b/src/lib/queries/files.ts @@ -33,13 +33,23 @@ export const useFiles = (query: { [key: string]: string } = {}) => { ); }); }; -export const usePaginatedFiles = (page?: number, filter = 'media', favorite = null) => { - const queryBuilder = new URLSearchParams({ + +export type PaginatedFilesOptions = { + filter: 'media' | 'none'; + favorite: boolean; + sortBy: 'createdAt' | 'views' | 'expiresAt' | 'size' | 'name' | 'mimetype'; + order: 'asc' | 'desc'; +}; + +export const usePaginatedFiles = (page?: number, options?: Partial) => { + const queryString = new URLSearchParams({ page: Number(page || '1').toString(), - filter, - ...(favorite !== null && { favorite: favorite.toString() }), - }); - const queryString = queryBuilder.toString(); + filter: options?.filter ?? 'none', + // ...(options?.favorite !== null && { favorite: options?.favorite?.toString() }), + favorite: options.favorite ? 'true' : '', + sortBy: options.sortBy ?? '', + order: options.order ?? '', + }).toString(); return useQuery(['files', queryString], async () => { return fetch('/api/user/paged?' + queryString) diff --git a/src/pages/api/user/paged.ts b/src/pages/api/user/paged.ts index 0210257..534c6cb 100644 --- a/src/pages/api/user/paged.ts +++ b/src/pages/api/user/paged.ts @@ -1,3 +1,5 @@ +import { Prisma } from '@prisma/client'; +import { s } from '@sapphire/shapeshift'; import config from 'lib/config'; import prisma from 'lib/prisma'; import { formatRootUrl } from 'lib/utils/urls'; @@ -5,12 +7,27 @@ import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'middleware/wi const pageCount = 16; +const sortByValidator = s.enum( + ...([ + 'createdAt', + 'views', + 'expiresAt', + 'size', + 'name', + 'mimetype', + ] satisfies (keyof Prisma.FileOrderByWithRelationInput)[]) +); + +const orderValidator = s.enum('asc', 'desc'); + async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { - const { page, filter, count, favorite } = req.query as { + const { page, filter, count, favorite, ...rest } = req.query as { page: string; filter: string; count: string; favorite: string; + sortBy: string; + order: string; }; const where = { @@ -33,7 +50,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { }, ], }), - }; + } satisfies Prisma.FileWhereInput; if (count) { const count = await prisma.file.count({ @@ -48,6 +65,14 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { if (!page) return res.badRequest('no page'); if (isNaN(Number(page))) return res.badRequest('page is not a number'); + // validate sortBy + const sortBy = sortByValidator.run(rest.sortBy || 'createdAt'); + if (!sortBy.isOk()) return res.badRequest('invalid sortBy option'); + + // validate order + const order = orderValidator.run(rest.order || 'desc'); + if (!sortBy.isOk()) return res.badRequest('invalid order option'); + const files: { favorite: boolean; createdAt: Date; @@ -63,7 +88,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { }[] = await prisma.file.findMany({ where, orderBy: { - createdAt: 'desc', + [sortBy.value]: order.value, }, select: { createdAt: true,