diff --git a/client/src/Hooks/useEditUserForm.ts b/client/src/Hooks/useEditUserForm.ts new file mode 100644 index 000000000..53484ee07 --- /dev/null +++ b/client/src/Hooks/useEditUserForm.ts @@ -0,0 +1,13 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { editUserSchema, type EditUserFormData } from "@/Validation/editUser"; + +export const useEditUserForm = () => { + return { + resolver: zodResolver(editUserSchema), + defaults: { + firstName: "", + lastName: "", + role: [], + } as EditUserFormData, + }; +}; diff --git a/client/src/Pages/Account/EditUser/index.tsx b/client/src/Pages/Account/EditUser/index.tsx index c9732549c..483181c35 100644 --- a/client/src/Pages/Account/EditUser/index.tsx +++ b/client/src/Pages/Account/EditUser/index.tsx @@ -1,122 +1,181 @@ -import { Stack, Typography, IconButton, Divider } from "@mui/material"; -import { useTheme } from "@mui/material"; -import { useTranslation } from "react-i18next"; -import { ConfigBox, BasePage, Breadcrumb } from "@/Components/v2/design-elements"; -import { TextField, Button, Autocomplete } from "@/Components/v2/inputs"; -import { UserRoles } from "@/Types/User"; -import { useState } from "react"; import { Trash2 } from "lucide-react"; +import { TextField, Button, Autocomplete } from "@/Components/v2/inputs"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import IconButton from "@mui/material/IconButton"; +import Divider from "@mui/material/Divider"; +import FormHelperText from "@mui/material/FormHelperText"; +import { useForm, Controller } from "react-hook-form"; +import { ConfigBox, BasePage } from "@/Components/v2/design-elements"; + +import { UserRoles } from "@/Types/User"; +import { useTranslation } from "react-i18next"; +import type { UserRole, User } from "@/Types/User"; +import { useParams } from "react-router-dom"; +import { useTheme } from "@mui/material"; +import { useEditUserForm } from "@/Hooks/useEditUserForm"; +import { useGet, usePatch } from "@/Hooks/UseApi"; +import type { EditUserFormData } from "@/Validation/editUser"; +import { useEffect } from "react"; interface RoleOption { - id: string; + id: UserRole; name: string; } const EditUserPage = () => { const theme = useTheme(); const { t } = useTranslation(); + const { userId } = useParams<{ userId: string }>(); + const { resolver, defaults } = useEditUserForm(); + const { patch, loading: isSaving } = usePatch(); - const [firstName, setFirstName] = useState(""); - const [lastName, setLastName] = useState(""); - const [email] = useState(""); - const [roles, setRoles] = useState([]); + const { data: user, isLoading } = useGet(`/auth/users/${userId}`); + + const { control, handleSubmit, reset, watch, setValue } = useForm({ + resolver, + defaultValues: defaults, + }); + + useEffect(() => { + if (user) { + reset({ + firstName: user.firstName || "", + lastName: user.lastName || "", + role: user.role || [], + }); + } + }, [user, reset]); const editableRoles = UserRoles.filter((role) => role !== "superadmin"); - const roleOptions: RoleOption[] = editableRoles.map((role) => ({ id: role, name: t(`common.auth.roles.${role}`), })); - const selectedRoles = roleOptions.filter((r) => roles.includes(r.id)); + const watchedRoles = watch("role"); + const selectedRoles = roleOptions.filter((r) => watchedRoles?.includes(r.id)); + const handleRemoveRole = (roleToRemove: UserRole) => { + const newRoles = watchedRoles.filter((role) => role !== roleToRemove); + setValue("role", newRoles, { shouldValidate: true }); + }; - const handleRemoveRole = (roleToRemove: string) => { - setRoles(roles.filter((role) => role !== roleToRemove)); + const onSubmit = async (data: EditUserFormData) => { + await patch(`/auth/users/${userId}`, data); }; return ( - - - - setFirstName(e.target.value)} - autoComplete="given-name" - /> - setLastName(e.target.value)} - autoComplete="family-name" - /> - - - } - /> - - option.name} - onChange={(_: unknown, newValue: RoleOption[]) => { - setRoles(newValue.map((r) => r.id)); - }} - isOptionEqualToValue={(option, value) => option.id === value.id} - /> - {selectedRoles.length > 0 && ( - - {selectedRoles.map((role, index) => ( - - {role.name} - handleRemoveRole(role.id)} - aria-label="Remove role" + +
+ + + ( + + )} + /> + ( + + )} + /> + + + } + /> + + ( + <> + option.name} + onChange={(_: unknown, newValue: RoleOption[]) => { + field.onChange(newValue.map((r) => r.id)); + }} + isOptionEqualToValue={(option, value) => option.id === value.id} + /> + {fieldState.error && ( + {fieldState.error.message} + )} + + )} + /> + {selectedRoles.length > 0 && ( + + {selectedRoles.map((role, index) => ( + - - - {index < selectedRoles.length - 1 && } - - ))} - - )} - - } - /> - - + {role.name} + handleRemoveRole(role.id)} + aria-label="Remove role" + > + + + {index < selectedRoles.length - 1 && } + + ))} + + )} + + } + /> + + +
); }; diff --git a/client/src/Validation/editUser.ts b/client/src/Validation/editUser.ts new file mode 100644 index 000000000..6fb42a6be --- /dev/null +++ b/client/src/Validation/editUser.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; +import { namePattern } from "@/Validation/patterns"; +import { UserRoles } from "@/Types/User"; + +export const editUserSchema = z.object({ + firstName: z + .string() + .min(1, "First name is required") + .max(50, "First name must be 50 characters or less") + .regex(namePattern, "First name contains invalid characters"), + lastName: z + .string() + .min(1, "Last name is required") + .max(50, "Last name must be 50 characters or less") + .regex(namePattern, "Last name contains invalid characters"), + role: z.array(z.enum(UserRoles)).min(1, "At least one role is required"), +}); + +export type EditUserFormData = z.infer; diff --git a/server/src/validation/joi.ts b/server/src/validation/joi.ts index f01a8da40..63be09932 100755 --- a/server/src/validation/joi.ts +++ b/server/src/validation/joi.ts @@ -689,7 +689,6 @@ const editUserByIdParamValidation = joi.object({ const editUserByIdBodyValidation = joi.object({ firstName: nameValidation.required(), lastName: nameValidation.required(), - email: joi.string().email().required(), role: joi .array() .items(joi.string().valid(...UserRoles)) @@ -700,7 +699,6 @@ const editUserByIdBodyValidation = joi.object({ const editSuperadminUserByIdBodyValidation = joi.object({ firstName: nameValidation.required(), lastName: nameValidation.required(), - email: joi.string().email().required(), role: joi .array() .items(joi.string().valid(...UserRoles))