Merge pull request #3262 from bluewave-labs/feat/v2-edit-user
feat: v2 edit user
This commit is contained in:
14 files changed
+278
-481
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,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];
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 ?? ""}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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
@@ -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": {
|
||||
|
||||
@@ -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