Skip to content

Commit 45946ba

Browse files
🧪 feat: add action to change the current project
Implements a new action to allow users to change their current project. - Adds a new file `apps/web/actions/changeProject.ts` for handling project change requests. - Defines a schema for validating project change requests using Zod. - Checks if the user is logged in and has access to the target project before updating their current project. - Triggers revalidation of the `/app` path after a successful project change. - Exports the `changeProject` function from `apps/web/actions/index.ts`. - Removes the `changeProject` function from the `Dashboard` component and uses the imported action instead. - Adds a utility function `parseZodError` to convert Zod errors into user-friendly messages.
1 parent c221fca commit 45946ba

5 files changed

Lines changed: 83 additions & 44 deletions

File tree

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
'use server'
2+
import { getSession, parseZodError, prisma } from "@/app/utils";
3+
import { revalidatePath } from "next/cache";
4+
import { z } from "zod";
5+
6+
// Schema for project change request validation
7+
const changeProjectSchema = z.object({
8+
projectIdToChangeTo: z.string(),
9+
});
10+
11+
// Handles project change requests
12+
export const changeProject = async (params: { projectIdToChangeTo: string }) => {
13+
// Ensure user is logged in
14+
const session = await getSession();
15+
if (!session || !session.user) {
16+
throw new Error("You must be logged in to change projects");
17+
}
18+
const userId = session.user.id;
19+
20+
// Validate input format
21+
const result = changeProjectSchema.safeParse(params);
22+
if (!result.success) {
23+
throw new Error(parseZodError(result.error));
24+
}
25+
26+
// Check user access to the target project
27+
const canUserSwitchToProject = await prisma.projectUsers.findFirst({
28+
where: {
29+
projectId: params.projectIdToChangeTo,
30+
userId,
31+
},
32+
});
33+
if (!canUserSwitchToProject) {
34+
throw new Error("You do not have access to this project");
35+
}
36+
37+
// Update user's current project
38+
await prisma.user.update({
39+
where: {
40+
id: userId,
41+
},
42+
data: {
43+
currentProjectId: params.projectIdToChangeTo,
44+
},
45+
});
46+
47+
// Trigger revalidation of the /app path
48+
revalidatePath("/app");
49+
return;
50+
};

‎apps/web/actions/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './updateUploadTitle';
22
export * from './navigate';
33

4+
export * from './changeProject';

‎apps/web/app/(dashboard)/app/page.tsx‎

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
import { authOptions } from "@/app/api/auth/[...nextauth]/AuthOptions";
22
import { Role } from "@prisma/client";
33
import { getServerSession } from "next-auth";
4-
import { formatDistanceToNow } from "date-fns";
5-
import Link from "next/link";
6-
import { AiOutlineDownload, AiOutlineVideoCameraAdd } from "react-icons/ai";
74
import TeamSwitcher from "@/components/TeamSwitcher";
85
import DashboardStatistics from "@/components/DashboardStatistics";
9-
import { IconShareUploadButton } from "@/app/(view)/view/[id]/ShareUploadButton";
106
import { redirect } from "next/navigation";
117
import Uploads from "./Uploads";
128
import { prisma } from "@/app/utils";
@@ -33,30 +29,6 @@ export default async function Dashboard() {
3329
},
3430
});
3531

36-
const changeProject = async (projectIdToChangeTo: string) => {
37-
"use server";
38-
39-
const canUserSwitchToProject = await prisma.projectUsers.findFirst({
40-
where: {
41-
projectId: projectIdToChangeTo,
42-
userId,
43-
},
44-
});
45-
46-
if (!canUserSwitchToProject) {
47-
throw new Error("You do not have access to this project");
48-
}
49-
50-
await prisma.user.update({
51-
where: {
52-
id: userId,
53-
},
54-
data: {
55-
currentProjectId: projectIdToChangeTo,
56-
},
57-
});
58-
return;
59-
};
6032

6133
const isUserOwner = !!(await prisma.projectUsers.findFirst({
6234
where: {
@@ -99,7 +71,6 @@ export default async function Dashboard() {
9971
projects={projects}
10072
className="mb-3"
10173
currentProjectId={currentProjectId}
102-
changeProject={changeProject}
10374
/>
10475
<DashboardStatistics />
10576
<div

‎apps/web/app/utils.ts‎

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ prisma = global.prisma;
1616
export { prisma };
1717

1818
import LoopsClient from "loops";
19+
import { ZodError, ZodIssue } from "zod";
1920
export const loops = new LoopsClient(process.env.LOOPS_API_KEY!);
2021

2122
const isDev = process.env.NODE_ENV === "development";
@@ -98,10 +99,10 @@ interface WithSessionHandler {
9899

99100
export const getSession = async () => {
100101
try {
101-
const session = await getServerSession(authOptions) as Session;
102-
if (!session?.user) return { user: null } as unknown as Session;
103-
return session;
104-
} catch(error) {
102+
const session = await getServerSession(authOptions) as Session;
103+
if (!session?.user) return { user: null } as unknown as Session;
104+
return session;
105+
} catch (error) {
105106
return { user: null } as unknown as Session;
106107
}
107108
};
@@ -150,4 +151,18 @@ export const getAvatarFallbackInitials = (name: string): string => {
150151
return `${nameParts[0][0]}${nameParts[nameParts.length - 1][0]}`.toUpperCase();
151152
}
152153
return nameParts[0][0].toUpperCase();
153-
}
154+
}
155+
156+
/**
157+
* Parses Zod errors into user-friendly messages.
158+
* @param error ZodError instance containing validation errors.
159+
* @returns A string with a user-friendly error message.
160+
*/
161+
export const parseZodError = (error: ZodError): string => {
162+
// Combine all error messages into a single string
163+
const messages = error.errors.map((e: ZodIssue) => {
164+
const path = e.path.join('.');
165+
return `${path}: ${e.message}`;
166+
});
167+
return messages.join(', ');
168+
};

‎apps/web/components/TeamSwitcher.tsx‎

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@ import {
1919
CommandList,
2020
CommandSeparator,
2121
} from "@components/ui/command";
22-
import {
23-
Dialog
24-
} from "@components/ui/dialog";
22+
import { Dialog } from "@components/ui/dialog";
2523
import {
2624
Popover,
2725
PopoverContent,
@@ -31,7 +29,8 @@ import { Project } from "@prisma/client";
3129
import { toast } from "sonner";
3230
import { Settings, Users } from "lucide-react";
3331
import Link from "next/link";
34-
import { signIn } from "next-auth/react";
32+
import { signIn, useSession } from "next-auth/react";
33+
import { changeProject } from "@/actions";
3534

3635
type PopoverTriggerProps = React.ComponentPropsWithoutRef<
3736
typeof PopoverTrigger
@@ -40,29 +39,32 @@ type PopoverTriggerProps = React.ComponentPropsWithoutRef<
4039
interface TeamSwitcherProps extends PopoverTriggerProps {
4140
projects: Project[];
4241
currentProjectId: string;
43-
changeProject: (projectId: string) => Promise<void>;
4442
}
4543

4644
export default function TeamSwitcher({
4745
className,
4846
projects,
4947
currentProjectId,
50-
changeProject,
5148
}: TeamSwitcherProps) {
5249
const [open, setOpen] = React.useState(false);
5350
const [showNewProjectDialog, setShowNewProjectDialog] = React.useState(false);
5451
const [selectedProject, setSelectedProject] = React.useState<Project>(
5552
projects.find((project) => project.id === currentProjectId) ?? projects[0]
5653
);
54+
const { update: updateSession, data: sessionData } = useSession();
5755

5856
const handleProjectChange = (project: Project) => {
59-
toast.promise(changeProject(project.id), {
57+
console.log({ project });
58+
toast.promise(changeProject({ projectIdToChangeTo: project?.id }), {
6059
loading: "Switching project...",
6160
success() {
61+
// Update session with updated project ID
62+
updateSession({
63+
currentProjectId: project.id,
64+
...sessionData,
65+
});
6266
setSelectedProject(project);
63-
// @ts-ignore
64-
signIn(null);
65-
return "Project switched. Refreshing page...";
67+
return `Project switched to ${project.name}`;
6668
},
6769
error(errorMessage: string) {
6870
return `Failed to switch project ${errorMessage}`;

0 commit comments

Comments
 (0)