diff --git a/src/app/(dashboard)/projects/[projectId]/users/page.tsx b/src/app/(dashboard)/projects/[projectId]/users/page.tsx index 1611cea..776ba31 100644 --- a/src/app/(dashboard)/projects/[projectId]/users/page.tsx +++ b/src/app/(dashboard)/projects/[projectId]/users/page.tsx @@ -21,6 +21,10 @@ export default function ProjectUsersPage() { const { projectId } = useParams(); const [users, setUsers] = useState([]); const [projectData, setProjectData] = useState([]); + const [projectName, setProjectName] = useState(""); + const [originatingProjectId, setOriginatingProjectId] = useState< + number | null + >(null); const [loading, setLoading] = useState(true); useEffect(() => { @@ -31,6 +35,9 @@ export default function ProjectUsersPage() { getProjects(), ]); + const projectIdNum = parseInt(projectId as string, 10); + setOriginatingProjectId(projectIdNum); + if (usersResult.success) { setUsers(usersResult.users); } else { @@ -39,6 +46,11 @@ export default function ProjectUsersPage() { if (projectsResult.success) { setProjectData(projectsResult.projects); + + const projectName = projectsResult.projects.find( + (project) => project.id === projectIdNum, + )?.name; + setProjectName(projectName); } else { toast.error("Error", { description: `Failed to fetch projects: ${projectsResult.error}`, @@ -85,11 +97,7 @@ export default function ProjectUsersPage() { - { - projectData.find( - (project) => project.id == (projectId as unknown), - )?.name - } + {projectName ? projectName : "Project"} @@ -108,6 +116,9 @@ export default function ProjectUsersPage() { }))} isLoading={loading} onUserAction={handleUserAction} + originatingProjectId={ + originatingProjectId != null ? originatingProjectId : undefined + } /> ); diff --git a/src/components/forms/AddUserForm.tsx b/src/components/forms/AddUserForm.tsx index 775b04d..10b405c 100644 --- a/src/components/forms/AddUserForm.tsx +++ b/src/components/forms/AddUserForm.tsx @@ -12,11 +12,12 @@ import { import { Input } from "../ui/input"; import * as z from "zod"; import { useForm } from "react-hook-form"; -import { createUserAction } from "@/lib/actions"; +import { createUserAction, linkUserToProjectId } from "@/lib/actions"; import { UserColumn } from "../usertable/columns"; import { CircleX, Loader2 } from "lucide-react"; import { useState } from "react"; import { Alert } from "../ui/alert"; +import { toast } from "sonner"; const createUserSchema = z.object({ name: z.string().min(2, "Name must be at least 2 characters"), @@ -27,9 +28,14 @@ const createUserSchema = z.object({ type CreateUserFormProps = { onUserAdd: (newUser: UserColumn) => void; onClose?: () => void; + projectIds?: number[]; }; -const CreateUserForm = ({ onUserAdd, onClose }: CreateUserFormProps) => { +const CreateUserForm = ({ + onUserAdd, + onClose, + projectIds, +}: CreateUserFormProps) => { /** Form to create a user */ const createUserForm = useForm({ resolver: zodResolver(createUserSchema), @@ -57,6 +63,43 @@ const CreateUserForm = ({ onUserAdd, onClose }: CreateUserFormProps) => { role: result.user.type, projects: result.user.projectIds, }; + + // Link user to project if originatingProjectId is provided + if (projectIds && projectIds.length > 0) { + const projectPromises: Promise<{ success: boolean }>[] = []; + + for (const projectId of projectIds) { + projectPromises.push( + linkUserToProjectId({ + projectId: projectId, + userId: String(result.user.id), + }), + ); + } + + const linkResults = await Promise.all(projectPromises); + const failedLinks = linkResults.filter((r) => !r.success); + + if (!newUser.projects) { + newUser.projects = []; + } + + for (let i = 0; i < linkResults.length; i++) { + if (linkResults[i].success) { + newUser.projects.push(projectIds[i]); + } + } + + newUser.projects = Array.from(new Set(newUser.projects)); + + // Show warning if any links failed + if (failedLinks.length > 0) { + toast.warning("User created with incomplete project links", { + description: `User was created successfully, but failed to link to ${failedLinks.length} project${failedLinks.length > 1 ? "s" : ""}. Please manually add them to the project.`, + }); + } + } + if (onUserAdd) { onUserAdd(newUser); } @@ -68,7 +111,14 @@ const CreateUserForm = ({ onUserAdd, onClose }: CreateUserFormProps) => { setLoading(false); } } catch (error) { - console.error("Error creating user:", error); + setError("An unexpected error occurred. Please try again."); + toast.error("Failed to create user", { + description: + error instanceof Error + ? error.message + : "An unexpected error occurred", + }); + setLoading(false); } }; diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index f795980..c660d91 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -24,7 +24,8 @@ const badgeVariants = cva( ); export interface BadgeProps - extends React.HTMLAttributes, + extends + React.HTMLAttributes, VariantProps {} function Badge({ className, variant, ...props }: BadgeProps) { diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 961fe42..bf1d595 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -35,7 +35,8 @@ const buttonVariants = cva( ); export interface ButtonProps - extends React.ButtonHTMLAttributes, + extends + React.ButtonHTMLAttributes, VariantProps { asChild?: boolean; } diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx index ad1a8b8..adcf7e0 100644 --- a/src/components/ui/sheet.tsx +++ b/src/components/ui/sheet.tsx @@ -50,7 +50,8 @@ const sheetVariants = cva( ); interface SheetContentProps - extends React.ComponentPropsWithoutRef, + extends + React.ComponentPropsWithoutRef, VariantProps {} const SheetContent = React.forwardRef< diff --git a/src/components/usertable/data-table.tsx b/src/components/usertable/data-table.tsx index 4822141..7bc426b 100644 --- a/src/components/usertable/data-table.tsx +++ b/src/components/usertable/data-table.tsx @@ -41,6 +41,7 @@ interface DataTableProps { projectData: ProjectColumn[]; isLoading: boolean; onUserAction: (user: UserColumn, action: "add" | "update" | "delete") => void; + originatingProjectId?: number; } export function UserDataTable({ @@ -48,6 +49,7 @@ export function UserDataTable({ projectData, isLoading, onUserAction, + originatingProjectId, }: DataTableProps) { const [columnFilters, setColumnFilters] = useState([]); const [isAddUserDialogOpen, setIsAddUserDialogOpen] = useState(false); @@ -153,6 +155,11 @@ export function UserDataTable({ onUserAction?.(user, "add")} onClose={() => setIsAddUserDialogOpen(false)} + projectIds={ + originatingProjectId != undefined + ? [originatingProjectId] + : [] + } /> diff --git a/src/lib/actions.ts b/src/lib/actions.ts index 13b3d64..dbf59c5 100644 --- a/src/lib/actions.ts +++ b/src/lib/actions.ts @@ -157,6 +157,36 @@ export async function linkUserToProject(data: { } } +export async function linkUserToProjectId(data: { + projectId: number; + userId: string; +}) { + const session = await getSession(); + if (!session) { + return { success: false, error: "Unauthorized" }; + } + + if (!requireAdmin(session.user)) { + return { + success: false, + error: "Only admins and superadmins can link users to projects", + }; + } + + const junoClient = getJunoInstance(); + try { + await junoClient.user.linkToProject({ + credentials: session.jwt, + project: { id: data.projectId }, + userId: data.userId, + }); + return { success: true }; + } catch (error) { + console.error("Error linking user:", error); + return { success: false, error: "Failed to link user type to project" }; + } +} + export async function unlinkUserFromProject(data: { projectName: string; userId: string;