From 550d5cc555abcd9acd81779c0cfdb94328de4b5c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 9 Feb 2026 19:26:58 +0000 Subject: [PATCH 1/6] initial commit --- client/src/Pages/Account/EditUser/index.tsx | 83 +++++++++++++++++++ .../Account/EditUser/{index.jsx => old.jsx} | 0 client/src/Routes/index.jsx | 6 +- 3 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 client/src/Pages/Account/EditUser/index.tsx rename client/src/Pages/Account/EditUser/{index.jsx => old.jsx} (100%) diff --git a/client/src/Pages/Account/EditUser/index.tsx b/client/src/Pages/Account/EditUser/index.tsx new file mode 100644 index 000000000..cd06581ed --- /dev/null +++ b/client/src/Pages/Account/EditUser/index.tsx @@ -0,0 +1,83 @@ +import { Stack, MenuItem } 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, Select } from "@/Components/v2/inputs"; +import { UserRoles } from "@/Types/User"; +import { useState } from "react"; + +const EditUserPage = () => { + const theme = useTheme(); + const { t } = useTranslation(); + + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); + const [email] = useState(""); + const [roles, setRoles] = useState([]); + + const editableRoles = UserRoles.filter((role) => role !== "superadmin"); + + return ( + + + + + setFirstName(e.target.value)} + autoComplete="given-name" + /> + setLastName(e.target.value)} + autoComplete="family-name" + /> + + + } + /> + setRoles(e.target.value as string[])} + placeholder={t("pages.account.team.invite.role.placeholder")} + sx={{ minWidth: 200 }} + > + {editableRoles.map((role) => ( + + {t(`common.auth.roles.${role}`)} + + ))} + + } + /> + + + + ); +}; + +export default EditUserPage; diff --git a/client/src/Pages/Account/EditUser/index.jsx b/client/src/Pages/Account/EditUser/old.jsx similarity index 100% rename from client/src/Pages/Account/EditUser/index.jsx rename to client/src/Pages/Account/EditUser/old.jsx diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 282a5db56..4c68c0367 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -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={ - + + + } /> From c4ab56095ec0f67b19bb5a8e81274062432e9d0d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 9 Feb 2026 19:46:11 +0000 Subject: [PATCH 2/6] strings --- client/src/Pages/Account/EditUser/index.tsx | 9 ++++-- client/src/Pages/Auth/Register/index.tsx | 12 +++---- client/src/locales/en.json | 36 +++++++++++++-------- 3 files changed, 35 insertions(+), 22 deletions(-) diff --git a/client/src/Pages/Account/EditUser/index.tsx b/client/src/Pages/Account/EditUser/index.tsx index cd06581ed..b596bcd32 100644 --- a/client/src/Pages/Account/EditUser/index.tsx +++ b/client/src/Pages/Account/EditUser/index.tsx @@ -27,19 +27,22 @@ const EditUserPage = () => { rightContent={ setFirstName(e.target.value)} autoComplete="given-name" /> setLastName(e.target.value)} autoComplete="family-name" /> diff --git a/client/src/Pages/Auth/Register/index.tsx b/client/src/Pages/Auth/Register/index.tsx index 80cee579f..75d27649f 100644 --- a/client/src/Pages/Auth/Register/index.tsx +++ b/client/src/Pages/Auth/Register/index.tsx @@ -86,8 +86,8 @@ const RegisterPage = () => { render={({ field, fieldState }) => ( @@ -99,8 +99,8 @@ const RegisterPage = () => { render={({ field, fieldState }) => ( @@ -113,8 +113,8 @@ const RegisterPage = () => { diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 69684af6a..8704450a2 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -237,6 +237,28 @@ "resolved": "Resolved", "responseTime": "Response time" }, + "form": { + "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": { @@ -634,19 +656,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?", From 197e075ba13340901238dd9540587043737a3569 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 9 Feb 2026 20:07:45 +0000 Subject: [PATCH 3/6] strings --- client/src/Pages/Account/EditUser/index.tsx | 77 ++++++++++++++----- client/src/Pages/Account/TabProfile.tsx | 8 +- .../components/AddTeamMemberDialog.tsx | 15 ++-- .../components/InviteTeamMemberDialog.tsx | 7 +- client/src/locales/en.json | 27 ++++--- 5 files changed, 88 insertions(+), 46 deletions(-) diff --git a/client/src/Pages/Account/EditUser/index.tsx b/client/src/Pages/Account/EditUser/index.tsx index b596bcd32..5a10898f6 100644 --- a/client/src/Pages/Account/EditUser/index.tsx +++ b/client/src/Pages/Account/EditUser/index.tsx @@ -1,10 +1,16 @@ -import { Stack, MenuItem } from "@mui/material"; +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, Select } from "@/Components/v2/inputs"; +import { TextField, Button, Autocomplete } from "@/Components/v2/inputs"; import { UserRoles } from "@/Types/User"; import { useState } from "react"; +import { Trash2 } from "lucide-react"; + +interface RoleOption { + id: string; + name: string; +} const EditUserPage = () => { const theme = useTheme(); @@ -17,6 +23,17 @@ const EditUserPage = () => { 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 handleRemoveRole = (roleToRemove: string) => { + setRoles(roles.filter((role) => role !== roleToRemove)); + }; + return ( @@ -50,25 +67,47 @@ const EditUserPage = () => { } /> setRoles(e.target.value as string[])} - placeholder={t("pages.account.team.invite.role.placeholder")} - sx={{ minWidth: 200 }} - > - {editableRoles.map((role) => ( - + option.name} + onChange={(_: unknown, newValue: RoleOption[]) => { + setRoles(newValue.map((r) => r.id)); + }} + isOptionEqualToValue={(option, value) => option.id === value.id} + /> + {selectedRoles.length > 0 && ( + - {t(`common.auth.roles.${role}`)} - - ))} - + {selectedRoles.map((role, index) => ( + + {role.name} + handleRemoveRole(role.id)} + aria-label="Remove role" + > + + + {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)) From 48081d85ee7aa2c05963ea4f81a18bb4aebc4149 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 9 Feb 2026 20:48:12 +0000 Subject: [PATCH 6/6] cleanup --- .../Pages/Account/EditUser/hooks/editUser.js | 102 ------------ client/src/Pages/Account/EditUser/old.jsx | 136 ---------------- .../components/ChangePasswordModal/index.jsx | 149 ------------------ .../Account/components/RoleTable/index.jsx | 44 ------ 4 files changed, 431 deletions(-) delete mode 100644 client/src/Pages/Account/EditUser/hooks/editUser.js delete mode 100644 client/src/Pages/Account/EditUser/old.jsx delete mode 100644 client/src/Pages/Account/components/ChangePasswordModal/index.jsx delete mode 100644 client/src/Pages/Account/components/RoleTable/index.jsx diff --git a/client/src/Pages/Account/EditUser/hooks/editUser.js b/client/src/Pages/Account/EditUser/hooks/editUser.js deleted file mode 100644 index aeb50ffff..000000000 --- a/client/src/Pages/Account/EditUser/hooks/editUser.js +++ /dev/null @@ -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]; -}; diff --git a/client/src/Pages/Account/EditUser/old.jsx b/client/src/Pages/Account/EditUser/old.jsx deleted file mode 100644 index 2b2ac5128..000000000 --- a/client/src/Pages/Account/EditUser/old.jsx +++ /dev/null @@ -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 ( - - - {t("editUserPage.title")} - - - - - role !== ROLES.SUPERADMIN) - .map((role) => ({ role, _id: role })) || [] - } - options={EDITABLE_ROLES} - multiple={true} - handleChange={handleRoleChange} - /> - - - - {!isSameUser && ( - - )} - - - - ); -}; - -export default EditUser; diff --git a/client/src/Pages/Account/components/ChangePasswordModal/index.jsx b/client/src/Pages/Account/components/ChangePasswordModal/index.jsx deleted file mode 100644 index 725a9b00e..000000000 --- a/client/src/Pages/Account/components/ChangePasswordModal/index.jsx +++ /dev/null @@ -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 ( - <> - - - - } - sx={{ mb: theme.spacing(5) }} - /> - - } - sx={{ mb: theme.spacing(5) }} - /> - - - - - - - - ); -}; - -ChangePasswordModal.propTypes = { - isSaving: PropTypes.bool.isRequired, - isLoading: PropTypes.bool.isRequired, - changePassword: PropTypes.func.isRequired, -}; - -export default ChangePasswordModal; diff --git a/client/src/Pages/Account/components/RoleTable/index.jsx b/client/src/Pages/Account/components/RoleTable/index.jsx deleted file mode 100644 index 90a30dcd4..000000000 --- a/client/src/Pages/Account/components/RoleTable/index.jsx +++ /dev/null @@ -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: {t("editUserPage.table.roleHeader")}, - render: (row) => { - return row; - }, - }, - { - id: "delete", - content: {t("editUserPage.table.actionHeader")}, - render: (row) => { - if (row === ROLES.SUPERADMIN) return null; - return ( - { - handleDeleteRole(row); - }} - style={{ cursor: "pointer" }} - /> - ); - }, - }, - ]; - return ( - - ); -}; - -export default RoleTable;