Task: When an issue is assigned to a user, automatically set the status to IN_PROGRESS.
I am able to send a PATCH request using axios api and it is working fine (updating the badge if the issue detail page which is a server component). But my <AssigneeSelect … /> component which is the drop down is not updating its value. I’m unable to communicate between two client components which are inside a server component using router.refresh(). Can anyone please guide me here?
page.tsx
import authOptions from "@/app/auth/authOptions";
import prisma from "@/prisma/client";
import { Box, Flex, Grid } from "@radix-ui/themes";
import { getServerSession } from "next-auth";
import { notFound } from "next/navigation";
import { cache } from "react";
import AssigneeSelect from "./AssigneeSelect";
import DeleteIssueButton from "./DeleteIssueButton";
import EditIssueButton from "./EditIssueButton";
import IssueDetails from "./IssueDetails";
import StatusSelect from "./StatusSelect";
interface Props {
params: { id: string };
}
const fetchIssue = cache((issueId: number) =>
prisma.issue.findUnique({
where: { id: issueId },
})
);
const IssueDetailPage = async ({ params }: Props) => {
const session = await getServerSession(authOptions);
const issue = await fetchIssue(parseInt(params.id));
if (!issue) notFound();
console.log(issue.status);
return (
<Grid columns={{ initial: "1", sm: "5" }} gap="5">
<Box className="md:col-span-4">
<IssueDetails issue={issue} />
</Box>
{session && (
<Box>
<Flex direction="column" gap="4">
<AssigneeSelect issue={issue} />
<StatusSelect issue={issue} />
<EditIssueButton issueId={issue.id} />
<DeleteIssueButton issueId={issue.id} />
</Flex>
</Box>
)}
</Grid>
);
};
export default IssueDetailPage;
export async function generateMetadata({ params }: Props) {
const issue = await fetchIssue(parseInt(params.id));
return {
title: issue?.title,
description: "Details of issue " + issue?.title,
};
}
AssigneeSelect.tsx
"use client";
import { Skeleton } from "@/app/components";
import { Issue, Status, User } from "@prisma/client";
import { Select } from "@radix-ui/themes";
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
import { useRouter } from "next/navigation";
import toast, { Toaster } from "react-hot-toast";
const AssigneeSelect = ({ issue }: { issue: Issue }) => {
const { data: users, error, isLoading } = useUsers();
if (isLoading) return <Skeleton />;
if (error) return null;
const router = useRouter();
const assignIssue = async (userId: string) => {
try {
await axios.patch(`/api/issues/${issue.id}`, {
assignedToUserId: userId || null,
status: userId ? Status.IN_PROGRESS : Status.OPEN,
});
router.refresh();
} catch (error) {
toast.error("Changes could not be saved");
}
};
return (
<>
<Select.Root
defaultValue={issue.assignedToUserId || ""}
onValueChange={assignIssue}
>
<Select.Trigger placeholder="Assign..."></Select.Trigger>
<Select.Content>
<Select.Group>
<Select.Label>Suggestions</Select.Label>
<Select.Item value="">Unassigned</Select.Item>
{users?.map((user) => (
<Select.Item key={user.id} value={user.id}>
{user.name}
</Select.Item>
))}
</Select.Group>
</Select.Content>
</Select.Root>
<Toaster />
</>
);
};
const useUsers = () =>
useQuery<User[]>({
queryKey: ["users"],
queryFn: () => axios.get("/api/users").then((res) => res.data),
staleTime: 60 * 1000, // 60 s refresh time
retry: 3, // retry 3 times after first request
});
export default AssigneeSelect;
StatusSelect.tsx
"use client";
import { Issue, Status } from "@prisma/client";
import { Select } from "@radix-ui/themes";
import axios from "axios";
import { useRouter } from "next/navigation";
import { useState } from "react";
import toast, { Toaster } from "react-hot-toast";
const statuses: { label: string; value: Status }[] = [
{ label: "Open", value: "OPEN" },
{ label: "In Progress", value: "IN_PROGRESS" },
{ label: "Closed", value: "CLOSED" },
];
const StatusSelect = ({ issue }: { issue: Issue }) => {
const router = useRouter();
const assignIssueStatus = async (status: Status) => {
try {
await axios.patch(`/api/issues/${issue.id}`, {
status: status,
});
router.refresh();
} catch (error) {
toast.error("Changes could not be saved");
}
};
return (
<>
<Select.Root
defaultValue={issue.status}
onValueChange={assignIssueStatus}
>
<Select.Trigger placeholder="Status Change"></Select.Trigger>
<Select.Content>
<Select.Group>
<Select.Label>Status</Select.Label>
{statuses.map((status) => (
<Select.Item key={status.label} value={status.value}>
{status.label}
</Select.Item>
))}
</Select.Group>
</Select.Content>
</Select.Root>
<Toaster />
</>
);
};
export default StatusSelect;
validationSchemas.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.")
.max(65535)
.optional(),
assignedToUserId: z
.string()
.min(1, "AssignedToUserId is required.")
.max(255)
.optional()
.nullable(),
status: z.enum(["OPEN", "IN_PROGRESS", "CLOSED"]).optional(),
});
issue-tracker\app\api\issues[id]\route.ts
import authOptions from "@/app/auth/authOptions";
import { patchIssueSchema } from "@/app/validationSchemas";
import prisma from "@/prisma/client";
import { getServerSession } from "next-auth";
import { NextRequest, NextResponse } from "next/server";
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();
const validation = patchIssueSchema.safeParse(body);
if (!validation.success)
return NextResponse.json(validation.error.format(), { status: 400 });
console.log(body);
const { assignedToUserId, title, description, status } = body;
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,
status,
},
});
return NextResponse.json(updatedIssue);
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const session = await getServerSession(authOptions);
if (!session) return NextResponse.json({}, { status: 401 });
const issue = await prisma.issue.findUnique({
where: { id: parseInt(params.id) },
});
if (!issue)
return NextResponse.json({ error: "Invalid issue" }, { status: 404 });
await prisma.issue.delete({
where: { id: issue.id },
});
return NextResponse.json({});
}