mirror of
https://github.com/diced/zipline.git
synced 2026-04-28 10:43:06 -07:00
fix: impl serverside pagination folders (#1052)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { useApiPagination } from '@/components/pages/files/useApiPagination';
|
||||
import { type Response } from '@/lib/api/response';
|
||||
import { useQueryState } from '@/lib/client/hooks/useQueryState';
|
||||
import { useTitle } from '@/lib/client/hooks/useTitle';
|
||||
@@ -20,20 +21,26 @@ import {
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { IconFolder, IconUpload } from '@tabler/icons-react';
|
||||
import { lazy, Suspense, useEffect, useMemo, useState } from 'react';
|
||||
import { Link, Params, useLoaderData, useNavigate } from 'react-router-dom';
|
||||
import { lazy, Suspense, useEffect, useMemo } from 'react';
|
||||
import { Link, Params, useLoaderData, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
|
||||
const DashboardFile = lazy(() => import('@/components/file/DashboardFile'));
|
||||
const DashboardFileModal = lazy(() => import('@/components/file/DashboardFile/DashboardFileModal'));
|
||||
|
||||
export async function loader({ params }: { params: Params<string> }) {
|
||||
const res = await fetch(`/api/server/folder/${params.id}`);
|
||||
export async function loader({ params, request }: { params: Params<string>; request: Request }) {
|
||||
const url = new URL(request.url);
|
||||
const page = url.searchParams.get('page') ?? '1';
|
||||
const perpage = url.searchParams.get('perpage') ?? '15';
|
||||
|
||||
const res = await fetch(
|
||||
`/api/server/folder/${params.id}?page=${encodeURIComponent(page)}&perpage=${encodeURIComponent(perpage)}`,
|
||||
);
|
||||
if (!res.ok) {
|
||||
throw new Response('Folder not found', { status: 404 });
|
||||
}
|
||||
return {
|
||||
folder: (await res.json()) as Response['/api/server/folder/[id]'],
|
||||
initial: (await res.json()) as Response['/api/server/folder/[id]'],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -67,10 +74,30 @@ function PublicFolderCard({ folder }: { folder: Partial<Folder> }) {
|
||||
const PER_PAGE_OPTIONS = [9, 12, 15, 30, 45];
|
||||
|
||||
export function Component() {
|
||||
const { folder } = useLoaderData<typeof loader>();
|
||||
const { initial } = useLoaderData<typeof loader>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useTitle(folder.name);
|
||||
const [, setSearchParams] = useSearchParams();
|
||||
const [page, setPage] = useQueryState('page', 1);
|
||||
const [perpage] = useQueryState('perpage', 15);
|
||||
|
||||
const { data, isLoading } = useApiPagination<Response['/api/server/folder/[id]']>(
|
||||
{
|
||||
route: `/api/server/folder/${initial.folder.id}`,
|
||||
page,
|
||||
perpage,
|
||||
sort: 'createdAt',
|
||||
order: 'desc',
|
||||
},
|
||||
{ fallbackData: initial, keepPreviousData: true, revalidateOnFocus: false },
|
||||
);
|
||||
|
||||
const folder = data?.folder ?? initial.folder;
|
||||
const files = data?.page ?? [];
|
||||
const totalRecords = data?.total ?? 0;
|
||||
const cachedPages = data?.pages ?? 0;
|
||||
|
||||
useTitle(folder.name ?? 'Folder');
|
||||
|
||||
const buildBreadcrumbs = () => {
|
||||
const items: FolderBreadcrumb[] = [];
|
||||
@@ -88,27 +115,14 @@ export function Component() {
|
||||
|
||||
const breadcrumbs = buildBreadcrumbs();
|
||||
const children = (folder.children ?? []) as Partial<Folder>[];
|
||||
|
||||
const [perpage, setPerpage] = useState(15);
|
||||
const [page, setPage] = useQueryState('page', 1);
|
||||
|
||||
const from = (page - 1) * perpage + 1;
|
||||
const to = Math.min(page * perpage, folder.files?.length ?? 0);
|
||||
const totalRecords = folder.files?.length ?? 0;
|
||||
const cachedPages = Math.ceil(totalRecords / perpage);
|
||||
|
||||
const visible = useMemo(() => {
|
||||
if (!folder.files) return [];
|
||||
|
||||
const start = (page - 1) * perpage;
|
||||
return folder.files.slice(start, start + perpage);
|
||||
}, [folder.files, page, perpage]);
|
||||
const from = totalRecords === 0 ? 0 : (page - 1) * perpage + 1;
|
||||
const to = Math.min(page * perpage, totalRecords);
|
||||
|
||||
const [current, setCurrent, setFiles] = useFileNavStore(
|
||||
useShallow((state) => [state.current, state.setCurrent, state.setFiles]),
|
||||
);
|
||||
const currentFile = current ? (visible.find((file) => file.id === current) ?? null) : null;
|
||||
const ids = useMemo(() => visible.map((file) => file.id), [visible]);
|
||||
const currentFile = current ? (files.find((file) => file.id === current) ?? null) : null;
|
||||
const ids = useMemo(() => files.map((file) => file.id), [files]);
|
||||
|
||||
useEffect(() => {
|
||||
setFiles(ids);
|
||||
@@ -173,7 +187,7 @@ export function Component() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{(visible.length ?? 0) > 0 && (
|
||||
{(files.length ?? 0) > 0 && (
|
||||
<>
|
||||
<Title order={3} mt='md' mb='sm'>
|
||||
Files
|
||||
@@ -186,7 +200,7 @@ export function Component() {
|
||||
}}
|
||||
spacing='md'
|
||||
>
|
||||
{visible.map((file: any) => (
|
||||
{files.map((file: any) => (
|
||||
<Suspense fallback={<Skeleton height={350} animate />} key={file.id}>
|
||||
<DashboardFile file={file} reduce onOpen={(fileId) => setCurrent(fileId)} />
|
||||
</Suspense>
|
||||
@@ -195,7 +209,7 @@ export function Component() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{children.length === 0 && (folder.files?.length ?? 0) === 0 && (
|
||||
{children.length === 0 && totalRecords === 0 && (
|
||||
<Text c='dimmed' mt='md'>
|
||||
This folder is empty.
|
||||
</Text>
|
||||
@@ -209,12 +223,16 @@ export function Component() {
|
||||
value={perpage.toString()}
|
||||
data={PER_PAGE_OPTIONS.map((val) => ({ value: val.toString(), label: `${val}` }))}
|
||||
onChange={(value) => {
|
||||
setPerpage(Number(value));
|
||||
setPage(1);
|
||||
setSearchParams((prev) => {
|
||||
prev.set('perpage', value ?? '15');
|
||||
prev.set('page', '1');
|
||||
return prev;
|
||||
});
|
||||
}}
|
||||
w={80}
|
||||
size='xs'
|
||||
variant='filled'
|
||||
disabled={isLoading}
|
||||
/>
|
||||
|
||||
<Pagination
|
||||
@@ -224,6 +242,7 @@ export function Component() {
|
||||
size='sm'
|
||||
withControls
|
||||
withEdges
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Response } from '@/lib/api/response';
|
||||
import type { Response } from '@/lib/api/response';
|
||||
import useSWR from 'swr';
|
||||
|
||||
type ApiPaginationOptions = {
|
||||
route?: string;
|
||||
page?: number;
|
||||
filter?: string;
|
||||
perpage?: number;
|
||||
@@ -26,14 +27,15 @@ type ApiPaginationOptions = {
|
||||
};
|
||||
};
|
||||
|
||||
const fetcher = async (
|
||||
const fetcher = async <T,>(
|
||||
{ options }: { options: ApiPaginationOptions; key: string } = {
|
||||
options: {
|
||||
page: 1,
|
||||
},
|
||||
key: '/api/user/files',
|
||||
},
|
||||
): Promise<Response['/api/user/files']> => {
|
||||
): Promise<T> => {
|
||||
const route = options.route ?? '/api/user/files';
|
||||
const searchParams = new URLSearchParams();
|
||||
if (options.page) searchParams.append('page', options.page.toString());
|
||||
if (options.filter) searchParams.append('filter', options.filter);
|
||||
@@ -48,7 +50,7 @@ const fetcher = async (
|
||||
}
|
||||
if (options.folderId) searchParams.append('folder', options.folderId);
|
||||
|
||||
const res = await fetch(`/api/user/files${searchParams.toString() ? `?${searchParams.toString()}` : ''}`);
|
||||
const res = await fetch(`${route}${searchParams.toString() ? `?${searchParams.toString()}` : ''}`);
|
||||
|
||||
if (!res.ok) {
|
||||
const json = await res.json();
|
||||
@@ -59,14 +61,18 @@ const fetcher = async (
|
||||
return res.json();
|
||||
};
|
||||
|
||||
export function useApiPagination(
|
||||
export function useApiPagination<T = Response['/api/user/files']>(
|
||||
options: ApiPaginationOptions = {
|
||||
page: 1,
|
||||
},
|
||||
swrConfig?: Parameters<typeof useSWR<T>>[2],
|
||||
) {
|
||||
const { data, error, isLoading, mutate } = useSWR<Response['/api/user/files']>(
|
||||
{ key: '/api/user/files', options },
|
||||
{ fetcher },
|
||||
const { data, error, isLoading, mutate } = useSWR<T>(
|
||||
{ key: options.route ?? '/api/user/files', options },
|
||||
{
|
||||
fetcher: (k) => fetcher<T>(k),
|
||||
...swrConfig,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -76,3 +82,4 @@ export function useApiPagination(
|
||||
mutate,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { ApiError } from '@/lib/api/errors';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { fileSelect } from '@/lib/db/models/file';
|
||||
import { File, cleanFiles, fileSchema, fileSelect } from '@/lib/db/models/file';
|
||||
import { buildPublicParentChain, cleanFolder, Folder, folderSchema } from '@/lib/db/models/folder';
|
||||
import { paginationQs } from '@/lib/validation';
|
||||
import typedPlugin from '@/server/typedPlugin';
|
||||
import z from 'zod';
|
||||
|
||||
export type ApiServerFolderResponse = Partial<Folder>;
|
||||
export type ApiServerFolderResponse = {
|
||||
folder: Partial<Folder>;
|
||||
page: File[];
|
||||
total: number;
|
||||
pages: number;
|
||||
};
|
||||
|
||||
export const PATH = '/api/server/folder/:id';
|
||||
export default typedPlugin(
|
||||
@@ -18,21 +24,29 @@ export default typedPlugin(
|
||||
params: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
querystring: paginationQs.pick({
|
||||
page: true,
|
||||
perpage: true,
|
||||
sortBy: true,
|
||||
order: true,
|
||||
}),
|
||||
response: {
|
||||
200: folderSchema.partial(),
|
||||
200: z.object({
|
||||
folder: folderSchema.partial(),
|
||||
page: z.array(fileSchema),
|
||||
total: z.number(),
|
||||
pages: z.number(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { page, perpage, sortBy, order } = req.query;
|
||||
|
||||
const folder = await prisma.folder.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
files: {
|
||||
select: { ...fileSelect, password: true, tags: false },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
},
|
||||
children: {
|
||||
where: { public: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
@@ -54,12 +68,34 @@ export default typedPlugin(
|
||||
if (!folder) throw new ApiError(9002);
|
||||
if (!folder.public && !folder.allowUploads) throw new ApiError(9002);
|
||||
|
||||
const where = { folderId: folder.id };
|
||||
const total = await prisma.file.count({ where });
|
||||
const pages = total === 0 ? 0 : Math.ceil(total / perpage);
|
||||
|
||||
const files = cleanFiles(
|
||||
await prisma.file.findMany({
|
||||
where,
|
||||
select: { ...fileSelect, password: true, tags: false },
|
||||
orderBy: {
|
||||
[sortBy]: order,
|
||||
},
|
||||
skip: (Number(page) - 1) * perpage,
|
||||
take: perpage,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
|
||||
if (!folder.public && folder.allowUploads) {
|
||||
return res.send({
|
||||
id: folder.id,
|
||||
name: folder.name,
|
||||
allowUploads: folder.allowUploads,
|
||||
public: folder.public,
|
||||
folder: {
|
||||
id: folder.id,
|
||||
name: folder.name,
|
||||
allowUploads: folder.allowUploads,
|
||||
public: folder.public,
|
||||
},
|
||||
page: [],
|
||||
total,
|
||||
pages,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -67,7 +103,14 @@ export default typedPlugin(
|
||||
folder.parent = await buildPublicParentChain(folder.parentId);
|
||||
}
|
||||
|
||||
return res.send(cleanFolder(folder, true));
|
||||
const cleanedFolder = cleanFolder(folder, true);
|
||||
|
||||
return res.send({
|
||||
folder: cleanedFolder,
|
||||
page: files,
|
||||
total,
|
||||
pages,
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user