【Next.js 项目实战系列】07-分配 Issue 给用户

原文链接

CSDN 的排版/样式可能有问题,去我的博客查看原文系列吧,觉得有用的话,给我的库点个star,关注一下吧 

上一篇【Next.js 项目实战系列】06-身份验证

分配 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.tsx

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

CSDN 的排版/样式可能有问题,去我的博客查看原文系列吧,觉得有用的话,给我的库点个star,关注一下吧 

下一篇讲数据处理

下一篇【Next.js 项目实战系列】08-数据处理

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/899061.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MySQL 查找连续相同名称的记录组,并保留每组内时间最大的一条记录

要求&#xff1a;查找连续相同名称的记录组&#xff0c;并保留每组内时间最大的一条记录&#xff0c;同时计算每组记录的 num 总和。 今天有人问了我一个问题&#xff0c;大致就是下面这样的数据结构&#xff08;原谅我实在不知道怎么描述这个问题&#xff09; 然后需要得到下面…

【C++】C++多态世界:从基础到前沿

【C】C多态世界&#xff1a;从基础到前沿 多态的定义及实现1. 虚函数2. 虚函数的重写3. C11 override 和 final重载、覆盖(重写)、隐藏(重定义)的对比&#xff08;关键&#xff09; 抽象类1. 概念2. 接口继承和实现继承 多态的原理1虚函数表2. 虚函数存在哪的&#xff1f;虚表存…

与ai一起作诗(《校园清廉韵》)

与ai对话犹如拷问自己的灵魂&#xff0c;与其说ai助力还不如说在和自己对话。 (笔记模板由python脚本于2024年10月19日 19:18:33创建&#xff0c;本篇笔记适合喜欢python和诗歌的coder翻阅) 【学习的细节是欢悦的历程】 Python 官网&#xff1a;https://www.python.org/ Free&…

基于Django的推荐系统、人脸识别登录、微信支付Demo、打卡门禁系统

基于Django的推荐系统、人脸识别登录、微信支付Demo、打卡门禁系统 1、推荐系统 图书管理、电影推荐、音乐推荐、在线课程选修、旅游推荐系统 图书管理点我跳转 电影管理点我跳转 课程管理点我跳转 2、算法 基于用户协同过滤推荐、物品协同过滤推荐、神经网络推荐、随机森…

Linux中如何理解一切皆文件

根据之前的学习我们会有一些少许的疑惑&#xff0c;我们的stdin &#xff0c;stdout&#xff0c;stderr访问的是键盘显示器&#xff0c;然而键盘显示器等他们都有一个共同的特点就是他们都是外设&#xff0c;那么这些外设是怎么被看成是文件的呢&#xff1f; 看图可以知道硬件的…

Jmeter 实战 JDBC配置

​ JDBC JDBC&#xff08;Java Database Connectivity&#xff09;是一种用于执行SQL语句的Java API。通过这个API&#xff0c;可以直接连接并执行SQL脚本&#xff0c;与数据库进行交互。 使用JMeter压力测试时&#xff0c;操作数据库的场景 在使用JMeter进行接口压力测试时…

【去哪儿-注册安全分析报告-缺少轨迹的滑动条】

前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 1. 暴力破解密码&#xff0c;造成用户信息泄露 2. 短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉 3. 带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大&#xff0c;造…

什么是分库分表?为什么要分库分表?什么时候需要分库分表?怎么样拆分?(数据库分库分表详解)

文章目录 1、什么是分库分表&#xff1f;1.1、分库分表的概念1.2、分库分表的方式1.2.1、垂直分库1.2.2、垂直分表1.2.3、水平分库1.2.4、水平分表 2、为什么要分库分表&#xff1f;3、什么时候需要分库分表&#xff1f;4、分库分表的数据路由4.1、数据路由的目的4.2、数据路由…

【Linux】磁盘文件系统(inode)、软硬链接

文章目录 1. 认识磁盘1.1 磁盘的物理结构1.2 磁盘的逻辑结构 2. 引入文件系统2.1 EXT系列文件系统的分区结构2.2 inode 3. 软硬链接3.1 软链接3.2 硬链接 在讲过了内存文件系统后&#xff0c;我们可以知道文件分为两种&#xff1a; 打开的文件&#xff08;内存中&#xff09;未…

VMamba:视觉SSM

论文标题&#xff1a;VMamba: Visual State Space Model 论文地址&#xff1a;https://arxiv.org/pdf/2401.10166 摘要 VMamba 是一个视觉骨干网络&#xff0c;基于状态空间模型&#xff08;SSM&#xff09;&#xff0c;其复杂度是线性的。该架构的核心是视觉状态空间&#xff…

uniapp学习(007-2 壁纸项目:详细设计css代码较多)

零基础入门uniapp Vue3组合式API版本到咸虾米壁纸项目实战&#xff0c;开发打包微信小程序、抖音小程序、H5、安卓APP客户端等 总时长 23:40:00 共116P 此文章包含第70p-第p78的内容 文章目录 客服消息按钮的open-type属性添加客服设置按钮 ifdef和ifndef 实现多端匹配语法实…

【数据结构笔记】优先级队列PriorityQueue

堆序性质&#xff1a;除了根节点&#xff0c;其他节点都不大&#xff08;小&#xff09;于父节点 进而根节点是最大&#xff08;小&#xff09;堆的最大&#xff08;小&#xff09;元 完全二叉堆 物理上是Vector 逻辑上是完全二叉树 层次遍历序列与物理存储顺序相同Rank为…

阅读笔记 Marketing Management Chapter 12

来源: Marketing Management, Kotler and Keller (2016), 15th edition Chapter 12 Addressing Competition and Driving Growth 本章围绕以下问题展开&#xff1a; 为什么公司发展核心业务很重要&#xff1f; 市场领导者如何扩大整个市场并捍卫市场份额&#xff1f; 市场挑…

Go_Parser部署、使用与原理分析

文章目录 前言1、概述2、安装与使用2.1、源码安装2.1.1、部署系统依赖组件2.1.1.1、部署IDA Pro 7.5 SP32.1.1.2、部署Python 3.9.132.1.1.3、部署Go 1.13.1 2.1.2、使用源码安装系统 2.2、使用方法2.2.1、准备测试程序2.2.2、创建IDA Pro项目2.2.3、使用Go_Parser解析二进制程…

【毕业设计】基于SpringBoot的网上商城系统

前言 &#x1f525;本系统可以选作为毕业设计&#xff0c;运用了现在主流的SSM框架&#xff0c;采用Maven来帮助我们管理依赖&#xff0c;所选结构非常合适大学生所学的技术&#xff0c;非常合适作为大学的毕业设计&#xff0c;难以适中。 &#x1f525;采用技术&#xff1a;Sp…

『Mysql集群』Mysql高可用集群之读写分离(二)

前言 主从复制: 解决了Mysql的单点故障问题以及提高MySQL的整体服务性能. 读写分离: 解决的是数据库的读性能问题,分担主库的压力&#xff0c;提高系统的可用性和稳定性。 分库分表: 数据库分表可以解决单表海量数据的查询性能问题&#xff0c;分库可以解决单台数据库的并发…

libaom 编解码项目编码接口文件介绍

对外头文件&#xff1a; 编码端&#xff1a;aom/aom_encoder.h、aom/aomcx.h解码端&#xff1a;aom/aom_decoder.h、aom/aomdx.h aom/aom_encoder.h 该头文件包了aom/aom_codec.h、aom/aom_external_partition.h头文件介绍&#xff1a;当前文件描述了应用程序与视频编码器算法之…

基于tfjs实现线性回归等基本模型

目录 1.回归模型基础概念与应用综述 1.1 线性回归&#xff08;Linear Regression&#xff09; 1.2 多元线性回归&#xff08;Multiple Linear Regression&#xff09; 1.3 广义线性回归&#xff08;Generalized Linear Model, GLM&#xff09; 1.4 逻辑回归&#xff08;Lo…

关于武汉芯景科技有限公司的限流开关芯片XJ6241开发指南(兼容LTC4411)

一、芯片引脚介绍 1.芯片引脚 二、系统结构图 三、功能描述 1.CTL引脚控制VIN和VOUT的通断 2.CTL引脚控制STAT引脚的状态 3.输出电压高于输入电压加上–VRTO的值&#xff0c;芯片处于关断状态

免费开源Odoo软件如何实现电商仓库高效发货

世界排名第一的免费开源ERP软件Odoo&#xff0c;拥有非常强大的仓库管理WMS功能。本文以电商仓库发货管理为例&#xff0c;介绍电商订单的仓库发货作业的各种方法。电商订单仓库发货流程&#xff0c;通常分为三个步骤&#xff0c;即拣货、打包、发货。根据仓库日处理订单数量的…