hook up role

This commit is contained in:
Alex Holliday committed 2026-02-09 20:38:04 +00:00
1 parent 4434189c72
commit ece6970fa7
4 files changed
+188 -99

No files matched your search

+13
View File
@@ -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,
};
};
+156 -97
View File
@@ -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<string[]>([]);
const { data: user, isLoading } = useGet<User>(`/auth/users/${userId}`);
const { control, handleSubmit, reset, watch, setValue } = useForm<EditUserFormData>({
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 (
<BasePage>
<Stack gap={theme.spacing(8)}>
<ConfigBox
title={t("pages.account.form.name.title")}
subtitle={t("pages.account.form.name.description")}
rightContent={
<Stack gap={theme.spacing(8)}>
<TextField
fieldLabel={t("common.form.name.option.firstName.label")}
placeholder={t("common.form.name.option.firstName.placeholder")}
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
autoComplete="given-name"
/>
<TextField
fieldLabel={t("common.form.name.option.lastName.label")}
placeholder={t("common.form.name.option.lastName.placeholder")}
value={lastName}
onChange={(e) => setLastName(e.target.value)}
autoComplete="family-name"
/>
<TextField
fieldLabel={t("common.form.email.option.email.label")}
placeholder={t("common.form.email.option.email.placeholder")}
value={email}
disabled
/>
</Stack>
}
/>
<ConfigBox
title={t("pages.editUser.form.roles.title")}
subtitle={t("pages.editUser.form.roles.description")}
rightContent={
<Stack spacing={theme.spacing(4)}>
<Autocomplete
fieldLabel={t("common.form.role.option.role.label")}
multiple
options={roleOptions}
value={selectedRoles}
getOptionLabel={(option) => option.name}
onChange={(_: unknown, newValue: RoleOption[]) => {
setRoles(newValue.map((r) => r.id));
}}
isOptionEqualToValue={(option, value) => option.id === value.id}
/>
{selectedRoles.length > 0 && (
<Stack
flex={1}
width="100%"
>
{selectedRoles.map((role, index) => (
<Stack
direction="row"
alignItems="center"
key={role.id}
width="100%"
>
<Typography flexGrow={1}>{role.name}</Typography>
<IconButton
size="small"
onClick={() => handleRemoveRole(role.id)}
aria-label="Remove role"
<BasePage loading={isLoading}>
<form onSubmit={handleSubmit(onSubmit)}>
<Stack gap={theme.spacing(8)}>
<ConfigBox
title={t("pages.account.form.name.title")}
subtitle={t("pages.account.form.name.description")}
rightContent={
<Stack gap={theme.spacing(8)}>
<Controller
name="firstName"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
fieldLabel={t("common.form.name.option.firstName.label")}
placeholder={t("common.form.name.option.firstName.placeholder")}
error={!!fieldState.error}
helperText={fieldState.error?.message}
autoComplete="given-name"
/>
)}
/>
<Controller
name="lastName"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
fieldLabel={t("common.form.name.option.lastName.label")}
placeholder={t("common.form.name.option.lastName.placeholder")}
error={!!fieldState.error}
helperText={fieldState.error?.message}
autoComplete="family-name"
/>
)}
/>
<TextField
fieldLabel={t("common.form.email.option.email.label")}
placeholder={t("common.form.email.option.email.placeholder")}
value={user?.email || ""}
disabled
/>
</Stack>
}
/>
<ConfigBox
title={t("pages.editUser.form.roles.title")}
subtitle={t("pages.editUser.form.roles.description")}
rightContent={
<Stack spacing={theme.spacing(4)}>
<Controller
name="role"
control={control}
render={({ field, fieldState }) => (
<>
<Autocomplete
fieldLabel={t("common.form.role.option.role.label")}
multiple
options={roleOptions}
value={selectedRoles}
getOptionLabel={(option) => option.name}
onChange={(_: unknown, newValue: RoleOption[]) => {
field.onChange(newValue.map((r) => r.id));
}}
isOptionEqualToValue={(option, value) => option.id === value.id}
/>
{fieldState.error && (
<FormHelperText error>{fieldState.error.message}</FormHelperText>
)}
</>
)}
/>
{selectedRoles.length > 0 && (
<Stack
flex={1}
width="100%"
>
{selectedRoles.map((role, index) => (
<Stack
direction="row"
alignItems="center"
key={role.id}
width="100%"
>
<Trash2 size={16} />
</IconButton>
{index < selectedRoles.length - 1 && <Divider />}
</Stack>
))}
</Stack>
)}
</Stack>
}
/>
<Button
variant="contained"
color="primary"
sx={{ alignSelf: "flex-end", minWidth: 100 }}
>
{t("common.buttons.save")}
</Button>
</Stack>
<Typography flexGrow={1}>{role.name}</Typography>
<IconButton
size="small"
onClick={() => handleRemoveRole(role.id)}
aria-label="Remove role"
>
<Trash2 size={16} />
</IconButton>
{index < selectedRoles.length - 1 && <Divider />}
</Stack>
))}
</Stack>
)}
</Stack>
}
/>
<Button
type="submit"
variant="contained"
color="primary"
loading={isSaving}
sx={{ alignSelf: "flex-end", minWidth: 100 }}
>
{t("common.buttons.save")}
</Button>
</Stack>
</form>
</BasePage>
);
};
+19
View File
@@ -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<typeof editUserSchema>;
-2
View File
@@ -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))