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 (
+ <>
+ }
+ onClick={handleClick}
+ >
+ Add Team Member
+
+
+ >
+ );
+};
+
+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 (
+ <>
+
+
+
+
+
+
+
+
+ >
+ );
+};
+AddTeamMember.propTypes = {
+ handleIsRegisterOpen: PropTypes.func.isRequired,
+ isRegisterOpen: PropTypes.bool.isRequired,
+ onMemberAdded: PropTypes.func.isRequired,
+};
+
+export default AddTeamMember;
diff --git a/client/src/Pages/Account/components/TeamPanel.jsx b/client/src/Pages/Account/components/TeamPanel.jsx
index 95ece219f..c3307cecf 100644
--- a/client/src/Pages/Account/components/TeamPanel.jsx
+++ b/client/src/Pages/Account/components/TeamPanel.jsx
@@ -9,10 +9,12 @@ import { networkService } from "../../../main";
import { createToast } from "../../../Utils/toastUtils";
import Select from "../../../Components/Inputs/Select";
import { GenericDialog } from "../../../Components/Dialog/genericDialog";
+import AddTeamMember from "../components/AddTeamMember";
import DataTable from "../../../Components/Table";
import { useGetInviteToken } from "../../../Hooks/inviteHooks";
import { useNavigate } from "react-router-dom";
import { useIsSuperAdmin } from "../../../Hooks/useIsAdmin";
+import AddMemberMenu from "./AddMemberMenu";
/**
* TeamPanel component manages the organization and team members,
* providing functionalities like renaming the organization, managing team members,
@@ -65,7 +67,10 @@ const TeamPanel = () => {
render: (row) => row.role,
},
];
-
+ const [refreshTrigger, setRefreshTrigger] = useState(0);
+ const refreshTeamList = () => {
+ setRefreshTrigger((prev) => prev + 1);
+ };
useEffect(() => {
const fetchTeam = async () => {
try {
@@ -79,7 +84,7 @@ const TeamPanel = () => {
};
fetchTeam();
- }, []);
+ }, [refreshTrigger]);
useEffect(() => {
const ROLE_MAP = {
@@ -109,6 +114,10 @@ const TeamPanel = () => {
setIsDisabled(Object.keys(errors).length !== 0 || toInvite.email === "");
}, [errors, toInvite.email]);
const [isOpen, setIsOpen] = useState(false);
+ const [isRegisterOpen, setIsRegisterOpen] = useState(false);
+ const handleIsRegisterOpen = (open) => {
+ setIsRegisterOpen(open);
+ };
const handleChange = (event) => {
const { value } = event.target;
@@ -138,7 +147,6 @@ const TeamPanel = () => {
const handleGetToken = async () => {
await getInviteToken({ email: toInvite.email, role: toInvite.role });
};
-
const handleInviteMember = async () => {
if (!toInvite.email) {
setErrors((prev) => ({ ...prev, email: "Email is required." }));
@@ -239,13 +247,16 @@ const TeamPanel = () => {
-
+
+
+ setIsOpen(true)}
+ handleIsRegisterOpen={handleIsRegisterOpen}
+ />
{
}}
/>
-
{
+const PasswordTooltip = ({ feedback, form, children, offset = [0, 0] }) => {
const theme = useTheme();
const { t } = useTranslation();
const hasPassword = form.password.length > 0;
@@ -73,6 +73,16 @@ const PasswordTooltip = ({ feedback, form, children }) => {
}
slotProps={{
+ popper: {
+ modifiers: [
+ {
+ name: "offset",
+ options: {
+ offset: offset,
+ },
+ },
+ ],
+ },
tooltip: {
sx: {
backgroundColor: theme.palette.tertiary.background,
@@ -111,6 +121,7 @@ PasswordTooltip.propTypes = {
password: PropTypes.string.isRequired,
}),
children: PropTypes.node,
+ offset: PropTypes.arrayOf(PropTypes.number),
};
export default PasswordTooltip;
diff --git a/client/src/Pages/Auth/hooks/usePasswordFeedback.jsx b/client/src/Pages/Auth/hooks/usePasswordFeedback.jsx
new file mode 100644
index 000000000..4159e6166
--- /dev/null
+++ b/client/src/Pages/Auth/hooks/usePasswordFeedback.jsx
@@ -0,0 +1,71 @@
+import { useState } from "react";
+import { newOrChangedCredentials } from "../../../Validation/validation";
+
+const usePasswordFeedback = () => {
+ const [feedback, setFeedback] = useState({});
+ const getFeedbackStatus = (form, errors, field, criteria) => {
+ const fieldErrors = errors?.[field];
+ const isFieldEmpty = form?.[field]?.length === 0;
+ const hasError = fieldErrors?.includes(criteria) || fieldErrors?.includes("empty");
+ const isCorrect = !isFieldEmpty && !hasError;
+
+ if (isCorrect) {
+ return "success";
+ } else if (hasError) {
+ return "error";
+ } else {
+ return "info";
+ }
+ };
+
+ const handlePasswordFeedback = (updatedForm, name, value, form, errors, setErrors) => {
+ const validateValue = { [name]: value };
+ const validateOptions = { abortEarly: false, context: { password: form.password } };
+ if (name === "password" && form.confirm.length > 0) {
+ validateValue.confirm = form.confirm;
+ validateOptions.context = { password: value };
+ } else if (name === "confirm") {
+ validateValue.password = form.password;
+ }
+ const { error } = newOrChangedCredentials.validate(validateValue, validateOptions);
+
+ const pwdErrors = error?.details.map((error) => ({
+ path: error.path[0],
+ type: error.type,
+ }));
+
+ const errorsByPath =
+ pwdErrors &&
+ pwdErrors.reduce((acc, { path, type }) => {
+ if (!acc[path]) {
+ acc[path] = [];
+ }
+ acc[path].push(type);
+ return acc;
+ }, {});
+
+ const oldErrors = { ...errors };
+ if (name === "password") {
+ oldErrors.password = undefined;
+ } else if (name === "confirm") {
+ oldErrors.confirm = undefined;
+ }
+ const newErrors = { ...oldErrors, ...errorsByPath };
+
+ setErrors(newErrors);
+
+ const newFeedback = {
+ length: getFeedbackStatus(updatedForm, errorsByPath, "password", "string.min"),
+ special: getFeedbackStatus(updatedForm, errorsByPath, "password", "special"),
+ number: getFeedbackStatus(updatedForm, errorsByPath, "password", "number"),
+ uppercase: getFeedbackStatus(updatedForm, errorsByPath, "password", "uppercase"),
+ lowercase: getFeedbackStatus(updatedForm, errorsByPath, "password", "lowercase"),
+ confirm: getFeedbackStatus(updatedForm, errorsByPath, "confirm", "different"),
+ };
+
+ setFeedback(newFeedback);
+ };
+ return { feedback, handlePasswordFeedback, getFeedbackStatus };
+};
+
+export default usePasswordFeedback;
diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx
index 350d2f82d..7ddcd7e3c 100644
--- a/client/src/Routes/index.jsx
+++ b/client/src/Routes/index.jsx
@@ -223,7 +223,7 @@ const Routes = () => {
}
+ element={}
/>