feat: server-side sorting (#366)

Co-authored-by: Jayvin Hernandez <gogojayvin923@gmail.com>
Co-authored-by: dicedtomato <35403473+diced@users.noreply.github.com>
This commit is contained in:
Derock 2023-05-11 01:32:13 -04:00 committed by GitHub
parent 0a34b0cc21
commit 24b06c76fb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 65 additions and 33 deletions

View file

@ -12,7 +12,7 @@ import {
import FileModal from 'components/File/FileModal'; import FileModal from 'components/File/FileModal';
import MutedText from 'components/MutedText'; import MutedText from 'components/MutedText';
import useFetch from 'lib/hooks/useFetch'; 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 { useStats } from 'lib/queries/stats';
import { userSelector } from 'lib/recoil/user'; import { userSelector } from 'lib/recoil/user';
import { bytesToHuman } from 'lib/utils/bytes'; import { bytesToHuman } from 'lib/utils/bytes';
@ -45,32 +45,24 @@ export default function Dashboard({ disableMediaPreview, exifEnabled, compress }
})(); })();
}, [page]); }, [page]);
const files = usePaginatedFiles(page, 'none');
// sorting // sorting
const [sortStatus, setSortStatus] = useState<DataTableSortStatus>({ const [sortStatus, setSortStatus] = useState<DataTableSortStatus>({
columnAccessor: 'date', columnAccessor: 'createdAt',
direction: 'asc', direction: 'asc',
}); });
const [records, setRecords] = useState(files.data);
useEffect(() => { const files = usePaginatedFiles(page, {
setRecords(files.data); filter: 'none',
}, [files.data]);
useEffect(() => { // only query for correct results if there is more than one page
if (!records || records.length === 0) return; // otherwise, querying has no effect
...(numFiles > 1
const sortedRecords = [...records].sort((a, b) => { ? {
if (sortStatus.direction === 'asc') { sortBy: sortStatus.columnAccessor as PaginatedFilesOptions['sortBy'],
return a[sortStatus.columnAccessor] > b[sortStatus.columnAccessor] ? 1 : -1; order: sortStatus.direction,
} }
: {}),
return a[sortStatus.columnAccessor] < b[sortStatus.columnAccessor] ? 1 : -1; });
});
setRecords(sortedRecords);
}, [sortStatus]);
// file modal on click // file modal on click
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@ -203,7 +195,7 @@ export default function Dashboard({ disableMediaPreview, exifEnabled, compress }
), ),
}, },
]} ]}
records={records ?? []} records={files.data ?? []}
fetching={files.isLoading} fetching={files.isLoading}
loaderBackgroundBlur={5} loaderBackgroundBlur={5}
loaderVariant='dots' loaderVariant='dots'

View file

@ -37,7 +37,9 @@ export default function FilePagation({ disableMediaPreview, exifEnabled, queryPa
})(); })();
}, [page]); }, [page]);
const pages = usePaginatedFiles(page, !checked ? 'media' : null); const pages = usePaginatedFiles(page, {
filter: !checked ? 'media' : 'none',
});
if (pages.isSuccess && pages.data.length === 0) { if (pages.isSuccess && pages.data.length === 0) {
if (page > 1 && numPages > 0) { if (page > 1 && numPages > 0) {

View file

@ -11,7 +11,10 @@ import PendingFilesModal from './PendingFilesModal';
export default function Files({ disableMediaPreview, exifEnabled, queryPage, compress }) { export default function Files({ disableMediaPreview, exifEnabled, queryPage, compress }) {
const [favoritePage, setFavoritePage] = useState(1); const [favoritePage, setFavoritePage] = useState(1);
const [favoriteNumPages, setFavoriteNumPages] = useState(0); const [favoriteNumPages, setFavoriteNumPages] = useState(0);
const favoritePages = usePaginatedFiles(favoritePage, 'media', true); const favoritePages = usePaginatedFiles(favoritePage, {
filter: 'media',
favorite: true,
});
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);

View file

@ -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<PaginatedFilesOptions>) => {
const queryString = new URLSearchParams({
page: Number(page || '1').toString(), page: Number(page || '1').toString(),
filter, filter: options?.filter ?? 'none',
...(favorite !== null && { favorite: favorite.toString() }), // ...(options?.favorite !== null && { favorite: options?.favorite?.toString() }),
}); favorite: options.favorite ? 'true' : '',
const queryString = queryBuilder.toString(); sortBy: options.sortBy ?? '',
order: options.order ?? '',
}).toString();
return useQuery<UserFilesResponse[]>(['files', queryString], async () => { return useQuery<UserFilesResponse[]>(['files', queryString], async () => {
return fetch('/api/user/paged?' + queryString) return fetch('/api/user/paged?' + queryString)

View file

@ -1,3 +1,5 @@
import { Prisma } from '@prisma/client';
import { s } from '@sapphire/shapeshift';
import config from 'lib/config'; import config from 'lib/config';
import prisma from 'lib/prisma'; import prisma from 'lib/prisma';
import { formatRootUrl } from 'lib/utils/urls'; import { formatRootUrl } from 'lib/utils/urls';
@ -5,12 +7,27 @@ import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'middleware/wi
const pageCount = 16; 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) { 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; page: string;
filter: string; filter: string;
count: string; count: string;
favorite: string; favorite: string;
sortBy: string;
order: string;
}; };
const where = { const where = {
@ -33,7 +50,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
}, },
], ],
}), }),
}; } satisfies Prisma.FileWhereInput;
if (count) { if (count) {
const count = await prisma.file.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 (!page) return res.badRequest('no page');
if (isNaN(Number(page))) return res.badRequest('page is not a number'); 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: { const files: {
favorite: boolean; favorite: boolean;
createdAt: Date; createdAt: Date;
@ -63,7 +88,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
}[] = await prisma.file.findMany({ }[] = await prisma.file.findMany({
where, where,
orderBy: { orderBy: {
createdAt: 'desc', [sortBy.value]: order.value,
}, },
select: { select: {
createdAt: true, createdAt: true,