本文实现问题列表的过滤、排序以及分页。
构建过滤器组件
app/issues添加IssueStatusFilter.tsx,创建IssueStatusFilter组件,这是一个用状态来筛选列表的组件。
import { Status } from "@prisma/client";
import { Select } from "@radix-ui/themes";
const statuses: { label: string; value: Status | "ALL" }[] = [
{ label: "All", value: "ALL" },
{ label: "Open", value: "OPEN" },
{ label: "In Progress", value: "IN_PROGRESS" },
{ label: "Closed", value: "CLOSED" },
];
const IssueStatusFilter = () => {
return (
<Select.Root>
<Select.Trigger placeholder="Filter by status..."></Select.Trigger>
<Select.Content>
{statuses.map((status) => (
<Select.Item key={status.value} value={status.value!}>
{status.label}
</Select.Item>
))}
</Select.Content>
</Select.Root>
);
};
export default IssueStatusFilter;
在IssueActions里调用IssueStatusFilter。并将之前的Box改成Flex, justify=’between’,使得IssueStatusFilter在左侧,原”New Issue”按钮在右侧。
查看页面。
提交Git,命名:Build the filter component.
实现过滤器
修改IssueStatusFilter组件,添加Select组件的onValueChange事件处理,当先中某个status时,就跳转到 /issues?status=<选中的状态>。
在IssuesPage,获取status查询字符串,并进行相应的过滤。如果不在Status枚举范围内,则为undefined,在Prisma中,where某个属性赋于undefined不作过滤。
查看过滤效果。
提交Git,命名:Filter issues.
使列表可排序
当点击列表某一列标题时,列表就会进行排序。跟过滤一样,可以将点击列作为searchParams传递。当前列表有3列,我们不希望每一列进行设置,可以统一设置列表数据,再进行map操作。在定义value类型时,用了keyof Issue ,表示只有Issue模型的属性才是合法的。
给每列的标题加上Link, 其中href不用类似{`/issues?orderBy=${volumn.value}`}这样的写法,因为这样在点击标题排序时会将原来的status查询参数覆盖掉,我们使用了提供对象及属性query的写法。
最后,为了使排序可视化,在排序列使用<ArrowUpIcon>组件标示排序箭头,这里我们只实现升序,未实现降序,如果要实现的也是很简单的。
查看页面。点击标题时不会覆盖status查询参数。
提交Git,命名:Make columns sortable.
列表排序
Prisma的排序语法类似 orderBy:{title:’asc’},但我们不能硬编码title等属性,需要动态获取,通过将searchParams参数加上方括号达到目的。另外还需要判断查询参数orderBy的值是合规的,比如必须是status、title或createAt。
修改IssuesPage如下。
提交Git,命名:Sort issues.
修复Filter错误
目前应用有一个Bug,就是选择状态进行过滤时,会将orderBy的查询参数删除。我们需要在过滤时保存原来的orderBy参数。
回到IssueStatusFilter组件修改,为了获取查询参数,需要用到useSearchParams的Hook,再通过URLSearchParams简化获取查询参数的键值对,也可方便返回查询字符串。
刷新网页后,url的status与组件筛选显示不一致,需要设置Select的defaultValue。
提交Git,命名:Fix filtering bugs.
生成虚拟数据
生成一些虚拟数据用于测试分页功能。这里使用chatGPT来生成,输入以下要求:
Given the following Prisma model, generater SQL statement to insert 20 records in the issues table. Use real-world titles and descriptions for issues. Status can be OPEN, IN_PROGRESS or CLOSED. description should be a paragraph long. Provide defferent values for createdAt and updatedAt columns.
model Issue {
id Int @id @default(autoincrement())
title String @db.VarChar(255)
description String @db.Text
status Status @default(OPEN)
createAt DateTime @default(now())
updateAt DateTime @updatedAt
}
输入的要求越详细,输出的结果越理想,ChatGPT就生成了插入20条记录的SQL语句。复制下来到DataGrip执行就生成了20条记录。
如果您无法访问chatGTP也是没关系,只需随意插入20条或更多记录即可。
形成的页面记录。
构建分页组件
分页组件是通用的,我们在app/components下新建Pagination.tsx文件。Pagination组件接受3个参数:条目总数itemCount, 当前页currentPage, 每页显示条数pageSize。
计算出总页数,在总页数小于等于1时,直接返回不显示Pagination组件。接着显示组件,用Flex组件包裹使其水平放置,当当前页面为1时,前一页及首页按钮禁用,当当前页为最后一页时,后一页及末页按钮禁用。
根据实际显示效果,做一些样式化。
import {
ChevronLeftIcon,
ChevronRightIcon,
DoubleArrowLeftIcon,
DoubleArrowRightIcon,
} from "@radix-ui/react-icons";
import { Button, Flex, Text } from "@radix-ui/themes";
interface Props {
itemCount: number;
currentPage: number;
pageSize: number;
}
const Pagination = ({ itemCount, currentPage, pageSize }: Props) => {
const pageCount = Math.ceil(itemCount / pageSize);
if (pageCount <= 1) return;
return (
<Flex align="center" gap="2">
<Text size="2">
Page {currentPage} of {pageCount}
</Text>
<Button color="gray" variant="soft" disabled={currentPage === 1}>
<DoubleArrowLeftIcon />
</Button>
<Button color="gray" variant="soft" disabled={currentPage === 1}>
<ChevronLeftIcon />
</Button>
<Button color="gray" variant="soft" disabled={currentPage === pageCount}>
<ChevronRightIcon />
</Button>
<Button color="gray" variant="soft" disabled={currentPage === pageCount}>
<DoubleArrowRightIcon />
</Button>
</Flex>
);
};
export default Pagination;
提交Git,命名:Build the layout of the pagination component.
实施分页
实施分页,我们需要将要显示的页码作为查询参数传递,类似前面的filter和orderBy。修改Pagination组件,首先使用useSearchParams获取原有的查询参数,因为不能清除之前的查询参数;接着根据不同的按钮,设置要显示的页面,并作为查询参数传递。
提交Git,命名:Implement pagination.
将问题列表分页
修改IssuesPage,Props添加page,然后定义pageSize,并获取当前筛选的问题总数目issueCount, 由于获取问题列表清单及总数目的where对象是一样,我们提取出来免得重复。prisma获取某一页的数据使用skip和take属性。
最后,在表格底部添加 Pagination组件,并提供必需的属性。
查看页面,并测试翻页正常。
提交Git,命名:Paginate issues.
提取 IssueTable 组件
目前,IssuesPage违反了单一职责的原则,该页面的职责是关注布局,但已包含了Issue列表表格的细节,我们将表格细节提取出来,形成IssueTable。IssueTable接受searchParams和issues参数,由于searchParams在IssuesPage中也会用到,我们提取出IssueQuery结构并export供IssuesPage调用。同样的,我们export出 columnNames供IssuesPage调用。
import { Issue, Status } from "@prisma/client";
import { ArrowUpIcon } from "@radix-ui/react-icons";
import { Table } from "@radix-ui/themes";
import Link from "next/link";
import { IssueStatusBadge } from "../components";
export interface IssueQuery {
status: Status;
orderBy: keyof Issue;
page: string;
}
interface Props {
searchParams: IssueQuery;
issues: Issue[];
}
const IssueTable = ({ searchParams, issues }: Props) => {
return (
<Table.Root variant="surface">
<Table.Header>
<Table.Row>
{columns.map((column) => (
<Table.ColumnHeaderCell
key={column.value}
className={column.className}
>
<Link
href={{ query: { ...searchParams, orderBy: column.value } }}
>
{column.label}
</Link>
{column.value === searchParams.orderBy && (
<ArrowUpIcon className="inline" />
)}
</Table.ColumnHeaderCell>
))}
</Table.Row>
</Table.Header>
<Table.Body>
{issues.map((issue) => (
<Table.Row key={issue.id}>
<Table.Cell>
<Link href={`/issues/${issue.id}`}>{issue.title}</Link>
<div className="block md:hidden">
{<IssueStatusBadge status={issue.status} />}
</div>
</Table.Cell>
<Table.Cell className="hidden md:table-cell">
{<IssueStatusBadge status={issue.status} />}
</Table.Cell>
<Table.Cell className="hidden md:table-cell">
{issue.createAt.toLocaleString()}
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Root>
);
};
const columns: { label: string; value: keyof Issue; className?: string }[] = [
{ label: "Issue", value: "title" },
{ label: "Status", value: "status", className: "hidden md:table-cell" },
{ label: "Create", value: "createAt", className: "hidden md:table-cell" },
];
export const columnNames = columns.map((column) => column.value);
export default IssueTable;
然后在IssuesPage调用IssueTable,IssuePage就变得清晰简单了。
提交Git,命名:Refactor:Extract IssueTable.
小结
本文实现了问题列表的过滤、排序以及分页。
下一篇实现仪表板功能。