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:
parent
0a34b0cc21
commit
24b06c76fb
5 changed files with 65 additions and 33 deletions
|
@ -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'
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue