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 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,33 +45,25 @@ export default function Dashboard({ disableMediaPreview, exifEnabled, compress }
})();
}, [page]);
const files = usePaginatedFiles(page, 'none');
// sorting
const [sortStatus, setSortStatus] = useState<DataTableSortStatus>({
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;
// 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,
}
return a[sortStatus.columnAccessor] < b[sortStatus.columnAccessor] ? 1 : -1;
: {}),
});
setRecords(sortedRecords);
}, [sortStatus]);
// file modal on click
const [open, setOpen] = useState(false);
const [selectedFile, setSelectedFile] = useState(null);
@ -203,7 +195,7 @@ export default function Dashboard({ disableMediaPreview, exifEnabled, compress }
),
},
]}
records={records ?? []}
records={files.data ?? []}
fetching={files.isLoading}
loaderBackgroundBlur={5}
loaderVariant='dots'

View file

@ -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) {

View file

@ -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);

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(),
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<UserFilesResponse[]>(['files', queryString], async () => {
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 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,