Merge pull request #3262 from bluewave-labs/feat/v2-edit-user

feat: v2 edit user
This commit is contained in:
Alexander Holliday authored and GitHub committed 2026-02-09 12:50:17 -08:00
commit 20d4f2c092
14 files changed
+278 -481

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,
};
};
@@ -1,102 +0,0 @@
import { useState, useEffect } from "react";
import { ROLES } from "../../../../Utils/roleUtils.js";
import { editUserValidation } from "../../../../Validation/validation.js";
import Joi from "joi";
import { createToast } from "../../../../Utils/toastUtils.jsx";
import { useTranslation } from "react-i18next";
export const useEditUserForm = (user) => {
const [searchInput, setSearchInput] = useState("");
const [form, setForm] = useState({
firstName: "",
lastName: "",
email: "",
role: [],
});
// Effect to set user
useEffect(() => {
if (user) {
setForm({
firstName: user?.firstName,
lastName: user?.lastName,
email: user?.email,
role: user?.role,
});
}
}, [user]);
const handleRoleChange = (value) => {
const hasSuperAdmin = form.role.includes(ROLES.SUPERADMIN);
const newRoles = value.map((item) => item.role);
if (hasSuperAdmin && !newRoles.includes(ROLES.SUPERADMIN)) {
newRoles.push(ROLES.SUPERADMIN);
}
setForm({
...form,
role: newRoles,
});
};
const handleDeleteRole = (role) => {
if (role === ROLES.SUPERADMIN) return;
setForm({ ...form, role: form?.role?.filter((r) => r !== role) });
};
const handleSearchInput = (value) => {
setSearchInput(value);
};
return [
form,
setForm,
handleRoleChange,
handleDeleteRole,
searchInput,
handleSearchInput,
];
};
export const useValidateEditUserForm = () => {
const [errors, setErrors] = useState({});
const { t } = useTranslation();
const validateForm = (form) => {
const { error } = editUserValidation.validate(form, {
abortEarly: false,
});
const errs = error?.details;
const errsObj = errs?.reduce((acc, curr) => {
acc[curr.path[0]] = curr.message;
return acc;
}, {});
setErrors(errsObj);
if (errs?.length > 0) {
createToast({
variant: "warning",
hasIcon: true,
title: t("editUserPage.toast.validationErrors"),
body: errs.map((err) => t(err.message)),
});
return false;
}
return true;
};
const validateField = (name, value) => {
const fieldSchema = Joi.object({
[name]: editUserValidation.extract(name),
});
const { error } = fieldSchema.validate({ [name]: value });
setErrors((prev) => {
const prevErrors = { ...prev };
if (error) prevErrors[name] = error.details[0].message;
else delete prevErrors[name];
return prevErrors;
});
};
return [errors, validateForm, validateField];
};
-136
View File
@@ -1,136 +0,0 @@
// Components
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
import Search from "@/Components/v1/Inputs/Search/index.jsx";
import Button from "@mui/material/Button";
import RoleTable from "../components/RoleTable/index.jsx";
import ChangePasswordModal from "@/Pages/Account/components/ChangePasswordModal/index.jsx";
// Utils
import { useParams } from "react-router-dom";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import { useGetUser, useEditUser } from "../../../Hooks/userHooks.js";
import { EDITABLE_ROLES, ROLES } from "../../../Utils/roleUtils.js";
import { useEditUserForm, useValidateEditUserForm } from "./hooks/editUser.js";
import { useSelector } from "react-redux";
const EditUser = () => {
const { user } = useSelector((state) => state.auth);
const { userId } = useParams();
const isSameUser = user?._id === userId;
const theme = useTheme();
const { t } = useTranslation();
const BREADCRUMBS = [
{ name: t("menu.team"), path: "/account/team" },
{ name: t("editUserPage.title"), path: "" },
];
const [userToEdit, isLoading, error] = useGetUser(userId);
const [editUser, isSaving, saveError, changePassword] = useEditUser(userId);
const [
form,
setForm,
handleRoleChange,
handleDeleteRole,
searchInput,
handleSearchInput,
] = useEditUserForm(userToEdit);
const [errors, validateForm, validateField] = useValidateEditUserForm();
const onChange = (e) => {
const name = e.target.name;
const value = e.target.value;
validateField(name, value);
setForm({ ...form, [name]: value });
};
const handleSubmit = (e) => {
e.preventDefault();
const valid = validateForm(form);
if (valid) {
editUser(form);
}
};
return (
<Stack gap={theme.spacing(20)}>
<Breadcrumbs list={BREADCRUMBS} />
<Typography variant="h2">{t("editUserPage.title")}</Typography>
<Stack
component="form"
onSubmit={handleSubmit}
gap={theme.spacing(12)}
maxWidth="50%"
>
<TextInput
name="firstName"
label={t("editUserPage.form.firstName")}
value={form?.firstName}
onChange={onChange}
error={errors?.firstName ? true : false}
helperText={t(errors?.firstName)}
/>
<TextInput
name="lastName"
label={t("editUserPage.form.lastName")}
value={form?.lastName}
onChange={onChange}
error={errors?.lastName ? true : false}
helperText={t(errors?.lastName)}
/>
<TextInput
name="email"
label={t("editUserPage.form.email")}
value={form?.email}
disabled={true}
error={errors?.email ? true : false}
helperText={t(errors?.email)}
/>
<Search
label={t("editUserPage.form.role")}
filteredBy="role"
inputValue={searchInput}
handleInputChange={handleSearchInput}
value={
form?.role
?.filter((role) => role !== ROLES.SUPERADMIN)
.map((role) => ({ role, _id: role })) || []
}
options={EDITABLE_ROLES}
multiple={true}
handleChange={handleRoleChange}
/>
<RoleTable
roles={form?.role}
handleDeleteRole={handleDeleteRole}
/>
<Stack
direction="row"
spacing={theme.spacing(10)}
mt={theme.spacing(8)}
//justifyContent="flex-end"
>
<Button
type="submit"
variant="contained"
color="accent"
loading={isLoading || isSaving}
>
{t("editUserPage.form.save")}
</Button>
{!isSameUser && (
<ChangePasswordModal
isSaving={isSaving}
isLoading={isLoading}
changePassword={changePassword}
/>
)}
</Stack>
</Stack>
</Stack>
);
};
export default EditUser;
+183
View File
@@ -0,0 +1,183 @@
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: 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 { 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 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 onSubmit = async (data: EditUserFormData) => {
await patch(`/auth/users/${userId}`, data);
};
return (
<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%"
>
<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>
);
};
export default EditUserPage;
+4 -4
View File
@@ -90,8 +90,8 @@ export const TabProfile = () => {
render={({ field, fieldState }) => (
<TextField
{...field}
fieldLabel={t("pages.account.form.name.option.firstName.label")}
placeholder={t("pages.account.form.name.option.firstName.placeholder")}
fieldLabel={t("common.form.name.option.firstName.label")}
placeholder={t("common.form.name.option.firstName.placeholder")}
autoComplete="given-name"
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
@@ -104,8 +104,8 @@ export const TabProfile = () => {
render={({ field, fieldState }) => (
<TextField
{...field}
fieldLabel={t("pages.account.form.name.option.lastName.label")}
placeholder={t("pages.account.form.name.option.lastName.placeholder")}
fieldLabel={t("common.form.name.option.lastName.label")}
placeholder={t("common.form.name.option.lastName.placeholder")}
autoComplete="family-name"
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
@@ -91,8 +91,8 @@ export const AddTeamMemberDialog = ({
render={({ field, fieldState }) => (
<TextField
{...field}
fieldLabel={t("pages.auth.register.form.option.name.label")}
placeholder={t("pages.auth.register.form.option.name.placeholder")}
fieldLabel={t("common.form.name.option.firstName.label")}
placeholder={t("common.form.name.option.firstName.placeholder")}
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
fullWidth
@@ -105,8 +105,8 @@ export const AddTeamMemberDialog = ({
render={({ field, fieldState }) => (
<TextField
{...field}
fieldLabel={t("pages.auth.register.form.option.surname.label")}
placeholder={t("pages.auth.register.form.option.surname.placeholder")}
fieldLabel={t("common.form.name.option.lastName.label")}
placeholder={t("common.form.name.option.lastName.placeholder")}
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
fullWidth
@@ -119,8 +119,8 @@ export const AddTeamMemberDialog = ({
render={({ field, fieldState }) => (
<TextField
{...field}
fieldLabel={t("pages.auth.common.form.option.email.label")}
placeholder={t("pages.auth.common.form.option.email.placeholder")}
fieldLabel={t("common.form.email.option.email.label")}
placeholder={t("common.form.email.option.email.placeholder")}
type="email"
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
@@ -136,8 +136,7 @@ export const AddTeamMemberDialog = ({
{...field}
value={field.value[0] ?? "user"}
onChange={(e) => field.onChange([e.target.value])}
fieldLabel={t("pages.account.team.invite.role.label")}
placeholder={t("pages.account.team.invite.role.placeholder")}
fieldLabel={t("common.form.role.option.role.label")}
fullWidth
>
{roleOptions.map((option) => (
@@ -1,149 +0,0 @@
import { useState } from "react";
import { Button, Stack } from "@mui/material";
import { GenericDialog } from "@/Components/v1/Dialog/genericDialog";
import TextInput from "@/Components/v1/Inputs/TextInput";
import PasswordTooltip from "@/Pages/Auth/components/PasswordTooltip";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import { createToast } from "../../../../Utils/toastUtils";
import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments";
import usePasswordFeedback from "@/Pages/Auth/hooks/usePasswordFeedback";
import PropTypes from "prop-types";
const ChangePasswordModal = ({ isSaving, isLoading, changePassword }) => {
const INITIAL_FORM_STATE = {
password: "",
confirm: "",
};
const theme = useTheme();
const { t } = useTranslation();
const { feedback, handlePasswordFeedback } = usePasswordFeedback();
const [form, setForm] = useState(INITIAL_FORM_STATE);
const [isChangePasswordOpen, setIsChangePasswordOpen] = useState(false);
const [errors, setErrors] = useState({});
const [isLoadingSubmit, setIsLoadingSubmit] = useState(false);
const closeChangePasswordModal = () => {
setIsChangePasswordOpen(false);
setForm(INITIAL_FORM_STATE);
};
const onChange = (e) => {
let { name, value } = e.target;
const updatedForm = { ...form, [name]: value };
setForm(updatedForm);
handlePasswordFeedback(updatedForm, name, value, form, errors, setErrors);
};
const isFormValid =
form.password.length > 1 &&
form.confirm.length > 1 &&
!errors.password &&
!errors.confirm;
const onsubmitChangePassword = async (event) => {
event.preventDefault();
if (!isFormValid) return;
const newPasswordForm = {
password: form.password,
};
try {
setIsLoadingSubmit(true);
await changePassword(newPasswordForm);
closeChangePasswordModal();
} catch (error) {
const errorMsg = error.response?.data?.msg || error.message || "unknownError";
createToast({
type: "error",
body: t(errorMsg),
});
} finally {
setIsLoadingSubmit(false);
}
};
return (
<>
<Button
variant="contained"
color="error"
onClick={() => setIsChangePasswordOpen(true)}
disabled={isLoading || isSaving}
>
{t("teamPanel.changeTeamPassword.changePasswordMenu")}
</Button>
<GenericDialog
title={t("teamPanel.changeTeamPassword.title")}
description={t("teamPanel.changeTeamPassword.description")}
open={isChangePasswordOpen}
onClose={closeChangePasswordModal}
theme={theme}
width={{ sm: "55%", md: "50%", lg: "40%", xl: "30%" }}
>
<PasswordTooltip
feedback={feedback}
form={form}
>
<TextInput
type="password"
id="register-password-input"
name="password"
label={t("auth.common.inputs.password.label")}
isRequired={true}
placeholder="••••••••••"
value={form.password}
onChange={onChange}
error={errors.password && errors.password[0] ? true : false}
endAdornment={<PasswordEndAdornment />}
sx={{ mb: theme.spacing(5) }}
/>
</PasswordTooltip>
<TextInput
type="password"
id="register-confirm-input"
name="confirm"
label={t("auth.common.inputs.passwordConfirm.label")}
gap={theme.spacing(4)}
isRequired={true}
placeholder={t("auth.common.inputs.passwordConfirm.placeholder")}
autoComplete="current-password"
value={form.confirm}
onChange={onChange}
error={errors.confirm && errors.confirm[0] ? true : false}
endAdornment={<PasswordEndAdornment />}
sx={{ mb: theme.spacing(5) }}
/>
<Stack
direction="row"
spacing={theme.spacing(10)}
mt={theme.spacing(8)}
justifyContent="flex-end"
>
<Button
variant="contained"
color="error"
onClick={closeChangePasswordModal}
disabled={isLoadingSubmit}
>
{t("teamPanel.cancel")}
</Button>
<Button
variant="contained"
color="accent"
onClick={onsubmitChangePassword}
disabled={isLoadingSubmit || !isFormValid}
>
{t("save")}
</Button>
</Stack>
</GenericDialog>
</>
);
};
ChangePasswordModal.propTypes = {
isSaving: PropTypes.bool.isRequired,
isLoading: PropTypes.bool.isRequired,
changePassword: PropTypes.func.isRequired,
};
export default ChangePasswordModal;
@@ -104,8 +104,8 @@ export const InviteTeamMemberDialog = ({
render={({ field, fieldState }) => (
<TextField
{...field}
fieldLabel={t("pages.account.team.invite.email.label")}
placeholder={t("pages.account.team.invite.email.placeholder")}
fieldLabel={t("common.form.email.option.email.label")}
placeholder={t("common.form.email.option.email.placeholder")}
type="email"
fullWidth
error={!!fieldState.error}
@@ -122,8 +122,7 @@ export const InviteTeamMemberDialog = ({
{...field}
value={Array.isArray(value) ? (value[0] ?? "") : ""}
onChange={(e) => onChange([e.target.value as UserRole])}
fieldLabel={t("pages.account.team.invite.role.label")}
placeholder={t("pages.account.team.invite.role.placeholder")}
fieldLabel={t("common.form.role.option.role.label")}
fullWidth
error={!!fieldState.error}
>
@@ -1,44 +0,0 @@
import Typography from "@mui/material/Typography";
import DataTable from "@/Components/v1/Table/index.jsx";
import Icon from "@/Components/v1/Icon";
import { useTranslation } from "react-i18next";
import { ROLES } from "../../../../Utils/roleUtils.js";
const RoleTable = ({ roles, handleDeleteRole }) => {
const { t } = useTranslation();
const HEADERS = [
{
id: "name",
content: <Typography>{t("editUserPage.table.roleHeader")}</Typography>,
render: (row) => {
return row;
},
},
{
id: "delete",
content: <Typography>{t("editUserPage.table.actionHeader")}</Typography>,
render: (row) => {
if (row === ROLES.SUPERADMIN) return null;
return (
<Icon
name="Trash2"
size={20}
onClick={() => {
handleDeleteRole(row);
}}
style={{ cursor: "pointer" }}
/>
);
},
},
];
return (
<DataTable
headers={HEADERS}
data={roles}
/>
);
};
export default RoleTable;
+6 -6
View File
@@ -86,8 +86,8 @@ const RegisterPage = () => {
render={({ field, fieldState }) => (
<TextField
{...field}
fieldLabel={t("pages.auth.register.form.option.name.label")}
placeholder={t("pages.auth.register.form.option.name.placeholder")}
fieldLabel={t("common.form.name.option.firstName.label")}
placeholder={t("common.form.name.option.firstName.placeholder")}
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
@@ -99,8 +99,8 @@ const RegisterPage = () => {
render={({ field, fieldState }) => (
<TextField
{...field}
fieldLabel={t("pages.auth.register.form.option.surname.label")}
placeholder={t("pages.auth.register.form.option.surname.placeholder")}
fieldLabel={t("common.form.name.option.lastName.label")}
placeholder={t("common.form.name.option.lastName.placeholder")}
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
@@ -113,8 +113,8 @@ const RegisterPage = () => {
<TextField
{...field}
disabled={!!token}
fieldLabel={t("pages.auth.common.form.option.email.label")}
placeholder={t("pages.auth.common.form.option.email.placeholder")}
fieldLabel={t("common.form.email.option.email.label")}
placeholder={t("common.form.email.option.email.placeholder")}
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
+4 -2
View File
@@ -46,7 +46,7 @@ import CreateNotifications from "../Pages/Notifications/create";
// Settings
import Account from "@/Pages/Account";
import EditUser from "../Pages/Account/EditUser/index.jsx";
import EditUser from "../Pages/Account/EditUser";
import Settings from "../Pages/Settings";
import Maintenance from "../Pages/Maintenance";
@@ -354,7 +354,9 @@ const Routes = () => {
path="account/team/:userId"
element={
<RoleProtectedRoute roles={["superadmin"]}>
<EditUser />
<ThemeProvider theme={v2theme}>
<EditUser />
</ThemeProvider>
</RoleProtectedRoute>
}
/>
+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>;
+39 -24
View File
@@ -237,6 +237,35 @@
"resolved": "Resolved",
"responseTime": "Response time"
},
"form": {
"role": {
"option": {
"role": {
"label": "Roles"
}
}
},
"name": {
"option": {
"firstName": {
"label": "First name",
"placeholder": "Enter your first name"
},
"lastName": {
"label": "Last name",
"placeholder": "Enter your last name"
}
}
},
"email": {
"option": {
"email": {
"label": "Email",
"placeholder": "you@example.com"
}
}
}
},
"table": {
"empty": "Nothing here",
"headers": {
@@ -522,17 +551,7 @@
"form": {
"name": {
"title": "Name",
"description": "Update your personal information",
"option": {
"firstName": {
"label": "First name",
"placeholder": "Enter your first name"
},
"lastName": {
"label": "Last name",
"placeholder": "Enter your last name"
}
}
"description": "Update your personal information"
},
"photo": {
"title": "Profile photo",
@@ -634,19 +653,7 @@
"register": {
"title": "Welcome back to Checkmate!",
"subtitle": "Sign up to get started",
"submit": "Register",
"form": {
"option": {
"name": {
"label": "Name",
"placeholder": "Your name"
},
"surname": {
"label": "Surname",
"placeholder": "Your surname"
}
}
}
"submit": "Register"
},
"forgotPassword": {
"title": "Forgot your password?",
@@ -812,6 +819,14 @@
}
}
},
"editUser": {
"form": {
"roles": {
"title": "Roles",
"description": "Assign roles to the user. Multiple roles can be selected."
}
}
},
"incidents": {
"dialog": {
"details": {
-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))