跳到主要内容

分配 Issue 给用户

本节代码链接

Select Button

/app/issues/[id]/AssigneeSelect.tsx
"use client";
import { Select } from "@radix-ui/themes";

const AssigneeSelect = () => {
return (
<Select.Root>
<Select.Trigger placeholder="Assign..." />
<Select.Content>
<Select.Group>
<Select.Label>Suggestions</Select.Label>
<Select.Item value="1">Castamere</Select.Item>
</Select.Group>
</Select.Content>
</Select.Root>
);
};
export default AssigneeSelect;

效果如下

Select Button

获取所有用户

本节代码链接

构建 API

/app/api/users
import { NextRequest, NextResponse } from "next/server";
import prisma from "@/prisma/client";

export async function GET(reques: NextRequest) {
const users = await prisma.user.findMany({ orderBy: { name: "asc" } });
return NextResponse.json(users);
}

客户端获取数据

/app/issues/[id]/AssigneeSelect.tsx
"use client";
import { User } from "@prisma/client";
import { Select } from "@radix-ui/themes";
import axios from "axios";
import { useEffect, useState } from "react";

const AssigneeSelect = () => {
const [users, setUsers] = useState<User[]>([]);

useEffect(() => {
const getUsers = async () => {
const { data } = await axios.get<User[]>("/api/users");
setUsers(data);
};
getUsers();
}, []);
return (
<Select.Root>
<Select.Trigger placeholder="Assign..." />
<Select.Content>
<Select.Group>
<Select.Label>Suggestions</Select.Label>
{users.map((user) => (
<Select.Item value={user.id} key={user.id}>
{user.name}
</Select.Item>
))}
</Select.Group>
</Select.Content>
</Select.Root>
);
};
export default AssigneeSelect;

React-Query

配置 React-Query

本节代码链接

使用如下命令安装 React-Query

npm i @tanstack/react-query

安装好后,在 /app 目录下创建 QueryClientProvider.tsx

/app/QueryClientProvider.tsx
"use client";
import {
QueryClient,
QueryClientProvider as ReactQueryClientProvider,
} from "@tanstack/react-query";
import { PropsWithChildren } from "react";

const queryClient = new QueryClient();

const QueryClientProvider = ({ children }: PropsWithChildren) => {
return (
<ReactQueryClientProvider client={queryClient}>
{children}
</ReactQueryClientProvider>
);
};
export default QueryClientProvider;

然后在 layout 中将 body 内所有内容用 QueryClientProvider 包起来

/app/layout.tsx
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<QueryClientProvider>
<AuthProvider>
<Theme appearance="light" accentColor="violet">
<NavBar />
<main className="p-5">
<Container>{children}</Container>
</main>
</Theme>
</AuthProvider>
</QueryClientProvider>
</body>
</html>
);
}

使用 React-Query

本节代码链接

首先,在 "/app/issues/[id]/Assign" 中去掉之前的 useEffect 和 useState,之后参照下面修改

/app/issues/[id]/AssigneeSelect.tsx
  ...
+ import { useQuery } from "@tanstack/react-query";
+ import { Skeleton } from "@/app/components";

const AssigneeSelect = () => {
+ const {
+ data: users,
+ error,
+ isLoading,
+ } = useQuery<User[]>({
// 用于缓存的 key,在不同地方调用 useQuery 若 key 一样则不会重复获取
+ queryKey: ["users"],
// 用于获取数据的函数
+ queryFn: () => axios.get<User[]>("/api/users").then((res) => res.data),
// 数据缓存多久
+ staleTime: 60 * 1000,
// 最多重复获取几次
+ retry: 3,
+ });
+ if (error) return null;
+ if (isLoading) return <Skeleton />;
...
};
export default AssigneeSelect;
完整代码(非 git diff 版)
/app/issues/[id]/AssigneeSelect.tsx
"use client";
import { User } from "@prisma/client";
import { Select } from "@radix-ui/themes";
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
import { Skeleton } from "@/app/components";

const AssigneeSelect = () => {
const {
data: users,
error,
isLoading,
} = useQuery<User[]>({
queryKey: ["users"], // 用于缓存的 key,在不同地方调用 useQuery 若 key 一样则不会重复获取
queryFn: () => axios.get<User[]>("/api/users").then((res) => res.data), // 用于获取数据的函数
staleTime: 60 * 1000, // 数据缓存多久
retry: 3, // 最多重复获取几次
});
if (error) return null;
if (isLoading) return <Skeleton />;

return (
<Select.Root>
<Select.Trigger placeholder="Assign..." />
<Select.Content>
<Select.Group>
<Select.Label>Suggestions</Select.Label>
{users?.map((user) => (
<Select.Item value={user.id} key={user.id}>
{user.name}
</Select.Item>
))}
</Select.Group>
</Select.Content>
</Select.Root>
);
};
export default AssigneeSelect;

Prisma Relation

本节代码链接

我们需要在 Prisma 中的 Issue model 和 User model 创建一个 Relation

schema.prisma
  model Issue {
id Int @id @default(autoincrement())
title String @db.VarChar(255)
description String @db.Text
status Status @default(OPEN)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt()
+ assignedToUserId String? @db.VarChar(255)
+ assignedToUser User? @relation(fields: [assignedToUserId], references: [id])
}

model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
+ assignedIssues Issue[]
}

更新修改 Issue API

本节代码链接

首先,添加一个新的 zod schema,其中 title, description, assignedToUserId 都设置为了 optional

validationSchema.ts
import { z } from "zod";

export const issueSchema = z.object({
title: z.string().min(1, "Title is required!").max(255),
description: z.string().min(1, "Description is required!").max(65535),
});

export const patchIssueSchema = z.object({
title: z.string().min(1, "Title is required!").max(255).optional(),
description: z.string().min(1, "Description is required!").optional(),
assignedToUserId: z
.string()
.min(1, "AssignedToUserId is required.")
.max(255)
.optional()
.nullable(),
});

然后修改 "/app/api/issues/[id]/route.tsx"

/app/api/issues/[id]/route.tsx
+ import { patchIssueSchema } from "@/app/validationSchema";
...

export async function PATCH(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const session = await getServerSession(authOptions);
if (!session) return NextResponse.json({}, { status: 401 });

const body = await request.json();
// 换成 patchIssueSchema
+ const validation = patchIssueSchema.safeParse(body);
if (!validation.success)
return NextResponse.json(validation.error.format(), { status: 400 });

// 直接将 title, description, assignedToUserId 结构出来
+ const { title, description, assignedToUserId } = body;

// 若 body 中有 assignedToUserId,则判断该用户是否存在
+ if (assignedToUserId) {
+ const user = await prisma.user.findUnique({
+ where: { id: assignedToUserId },
+ });
+ if (!user)
+ return NextResponse.json({ error: "Invalid user" }, { status: 400 });
+ }

const issue = await prisma.issue.findUnique({
where: { id: parseInt(params.id) },
});
if (!issue)
return NextResponse.json({ error: "Invalid Issue" }, { status: 404 });

const updatedIssue = await prisma.issue.update({
where: { id: issue.id },
+ data: {
+ title,
+ description,
+ assignedToUserId,
+ },
});

return NextResponse.json(updatedIssue, { status: 200 });
}

分配 Issue

本节代码链接

/app/issues/[id]/AssigneeSelect.tsx
  ...
const AssigneeSelect = ({ issue }: { issue: Issue }) => {
...

return (
<Select.Root
// 设置初始显示值
+ defaultValue={issue.assignedToUserId || ""}
// 当选择时,使用patch (不需要await)
+ onValueChange={(userId) => {
+ axios.patch("/api/issues/" + issue.id, {
+ assignedToUserId: userId === "Unassign" ? null : userId,
+ });
+ }}
>
<Select.Trigger placeholder="Assign..." />
<Select.Content>
<Select.Group>
<Select.Label>Suggestions</Select.Label>
{/* 添加一个 unassign */}
+ <Select.Item value="Unassign">Unassign</Select.Item>
{users?.map((user) => (
<Select.Item value={user.id} key={user.id}>
{user.name}
</Select.Item>
))}
</Select.Group>
</Select.Content>
</Select.Root>
);
};
export default AssigneeSelect;

显示 Toast

本节代码链接

使用如下命令安装

npm i react-hot-toast

我们只需要在该组件任意地方添加 <Toaster /> 组件,然后在需要报错的地方调用 toast() 函数即可

/app/issues/[id]/AssigneeSelect.tsx
+ import toast, { Toaster } from "react-hot-toast";

const AssigneeSelect = ({ issue }: { issue: Issue }) => {

return (
<>
<Select.Root
defaultValue={issue.assignedToUserId || ""}
onValueChange={ (userId) => {
+ axios
+ .patch("/api/issues/" + issue.id, {
+ assignedToUserId: userId === "Unassign" ? null : userId,
+ })
// 调用 toast.error()即可
+ .catch(() => toast.error("Changes could not be saved!"));
}
}
>
...
</Select.Root>
+ <Toaster />
</>
);
};
export default AssigneeSelect;

效果如下

Toast

请作者喝可乐🥤: