hook up role
This commit is contained in:
1 parent
4434189c72
commit
ece6970fa7
4 files changed
+188
-99
No files matched your search
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>;
|
||||
@@ -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))
|
||||
|
||||
Reference in new issue
Block a user