From d401eefd6be233aec92b48ce02956b0ecd7e2f8e Mon Sep 17 00:00:00 2001 From: karenvicent Date: Thu, 28 Aug 2025 12:58:38 -0400 Subject: [PATCH 1/9] Add direct user registration --- .../src/Components/Dialog/genericDialog.jsx | 6 +- client/src/Hooks/inviteHooks.js | 1 + .../components/AddMemberMenu/index.jsx | 68 +++++ .../components/AddTeamMember/index.jsx | 238 ++++++++++++++++++ .../Pages/Account/components/TeamPanel.jsx | 32 ++- .../Pages/Auth/components/PasswordTooltip.jsx | 13 +- .../Pages/Auth/hooks/usePasswordFeedback.jsx | 71 ++++++ client/src/Routes/index.jsx | 2 +- client/src/locales/en.json | 39 +++ 9 files changed, 456 insertions(+), 14 deletions(-) create mode 100644 client/src/Pages/Account/components/AddMemberMenu/index.jsx create mode 100644 client/src/Pages/Account/components/AddTeamMember/index.jsx create mode 100644 client/src/Pages/Auth/hooks/usePasswordFeedback.jsx diff --git a/client/src/Components/Dialog/genericDialog.jsx b/client/src/Components/Dialog/genericDialog.jsx index b2cbe1299..a06ef2732 100644 --- a/client/src/Components/Dialog/genericDialog.jsx +++ b/client/src/Components/Dialog/genericDialog.jsx @@ -2,7 +2,7 @@ import { useId } from "react"; import PropTypes from "prop-types"; import { Modal, Stack, Typography } from "@mui/material"; -const GenericDialog = ({ title, description, open, onClose, theme, children }) => { +const GenericDialog = ({ title, description, open, onClose, theme, children, width }) => { const titleId = useId(); const descriptionId = useId(); const ariaDescribedBy = description?.length > 0 ? descriptionId : ""; @@ -16,6 +16,7 @@ const GenericDialog = ({ title, description, open, onClose, theme, children }) = > {title} @@ -46,6 +48,7 @@ const GenericDialog = ({ title, description, open, onClose, theme, children }) = {description} @@ -64,6 +67,7 @@ GenericDialog.propTypes = { theme: PropTypes.object.isRequired, children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]) .isRequired, + width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), }; export { GenericDialog }; diff --git a/client/src/Hooks/inviteHooks.js b/client/src/Hooks/inviteHooks.js index 705906efa..810534454 100644 --- a/client/src/Hooks/inviteHooks.js +++ b/client/src/Hooks/inviteHooks.js @@ -33,6 +33,7 @@ const useGetInviteToken = () => { } setToken(inviteLink); + return token; } catch (error) { setError(error); } finally { diff --git a/client/src/Pages/Account/components/AddMemberMenu/index.jsx b/client/src/Pages/Account/components/AddMemberMenu/index.jsx new file mode 100644 index 000000000..5ed2ce96c --- /dev/null +++ b/client/src/Pages/Account/components/AddMemberMenu/index.jsx @@ -0,0 +1,68 @@ +import { useState } from "react"; +import Button from "@mui/material/Button"; +import Menu from "@mui/material/Menu"; +import MenuItem from "@mui/material/MenuItem"; +import { useTheme } from "@emotion/react"; +import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown"; +import { useTranslation } from "react-i18next"; +import Proptypes from "prop-types"; + +const AddMemberMenu = ({ handleInviteOpen, handleIsRegisterOpen }) => { + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + const { t } = useTranslation(); + const theme = useTheme(); + const handleClick = (event) => { + setAnchorEl(event.currentTarget); + }; + const handleClose = () => { + setAnchorEl(null); + }; + + return ( + <> + + + { + handleClose(); + handleInviteOpen(); + }} + > + {t("teamPanel.inviteTeamMember")} + + { + handleClose(); + handleIsRegisterOpen(true); + }} + > + {t("teamPanel.register")} + + + + ); +}; + +AddMemberMenu.propTypes = { + handleInviteOpen: Proptypes.func.isRequired, + handleIsRegisterOpen: Proptypes.func.isRequired, +}; + +export default AddMemberMenu; diff --git a/client/src/Pages/Account/components/AddTeamMember/index.jsx b/client/src/Pages/Account/components/AddTeamMember/index.jsx new file mode 100644 index 000000000..1d345cb3d --- /dev/null +++ b/client/src/Pages/Account/components/AddTeamMember/index.jsx @@ -0,0 +1,238 @@ +import { Button, Stack } from "@mui/material"; +import { GenericDialog } from "../../../../Components/Dialog/genericDialog"; +import TextInput from "../../../../Components/Inputs/TextInput"; +import Select from "../../../../Components/Inputs/Select"; +import { newOrChangedCredentials } from "../../../../Validation/validation"; +import { useGetInviteToken } from "../../../../Hooks/inviteHooks"; +import { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; +import { createToast } from "../../../../Utils/toastUtils"; +import { useState } from "react"; +import { networkService } from "../../../../main"; +import PasswordTooltip from "../../../Auth/components/PasswordTooltip"; +import usePasswordFeedback from "../../../Auth/hooks/usePasswordFeedback"; +import { PasswordEndAdornment } from "../../../../Components/Inputs/TextInput/Adornments"; +import PropTypes from "prop-types"; +const INITIAL_FORM_STATE = { + firstName: "", + lastName: "", + email: "", + password: "", + confirm: "", + teamId: "", +}; + +const INITIAL_ROLE_STATE = ["user"]; +const AddTeamMember = ({ handleIsRegisterOpen, isRegisterOpen, onMemberAdded }) => { + const theme = useTheme(); + const { t } = useTranslation(); + const { feedback, handlePasswordFeedback } = usePasswordFeedback(); + const [getInviteToken, clearToken] = useGetInviteToken(); + const [form, setForm] = useState(INITIAL_FORM_STATE); + const [role, setRole] = useState(INITIAL_ROLE_STATE); + const [errors, setErrors] = useState({}); + const closeAddMemberModal = () => { + handleIsRegisterOpen(false); + setForm(INITIAL_FORM_STATE); + setRole(INITIAL_ROLE_STATE); + setErrors({}); + clearToken(); + }; + + const onChange = (e) => { + let { name, value } = e.target; + if (name === "email") value = value.toLowerCase(); + const updatedForm = { ...form, [name]: value }; + + const { error } = newOrChangedCredentials.validate( + { [name]: value }, + { abortEarly: false, context: { password: form.password } } + ); + + setErrors((prev) => ({ + ...prev, + [name]: error?.details?.[0]?.message || "", + })); + + setForm(updatedForm); + + if (name === "password" || name === "confirm") { + handlePasswordFeedback(updatedForm, name, value, form, errors, setErrors); + } + }; + + const onsubmitAddMember = async (event) => { + event.preventDefault(); + + const { error } = newOrChangedCredentials.validate(form, { + abortEarly: false, + context: { password: form.password }, + }); + if (error) { + const formErrors = {}; + for (const err of error.details) { + formErrors[err.path[0]] = err.message; + } + setErrors(formErrors); + return; + } + + const token = await getInviteToken({ email: form.email, role: role }); + + const toSubmit = { + ...form, + inviteToken: token, + }; + delete toSubmit.confirm; + + try { + await networkService.registerUser(toSubmit); + + createToast({ + body: t("teamPanel.registerToast.success"), + }); + closeAddMemberModal(); + onMemberAdded(); + } catch (error) { + const errorMsg = error.response?.data?.msg || error.message || "unknownError"; + createToast({ + type: "error", + body: t(`teamPanel.registerToast.${errorMsg}`), + }); + } + }; + const tErr = (key) => (key ? t([`teamPanel.registerTeamMember.${key}`, key]) : ""); + return ( + <> + + + + + + + +