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 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'
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue