diff --git a/client/src/Hooks/useInviteForm.ts b/client/src/Hooks/useInviteForm.ts new file mode 100644 index 000000000..664cc96ab --- /dev/null +++ b/client/src/Hooks/useInviteForm.ts @@ -0,0 +1,14 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { inviteSchema, type InviteFormData } from "@/Validation/invite"; + +export const useInviteForm = () => { + const defaults: InviteFormData = { + email: "", + role: ["user"], + }; + + return { + resolver: zodResolver(inviteSchema), + defaults, + }; +}; diff --git a/client/src/Pages/Account/components/HeaderTeamControls.tsx b/client/src/Pages/Account/components/HeaderTeamControls.tsx index 405e175c0..328deb411 100644 --- a/client/src/Pages/Account/components/HeaderTeamControls.tsx +++ b/client/src/Pages/Account/components/HeaderTeamControls.tsx @@ -60,7 +60,7 @@ export const HeaderTeamControls = ({ startIcon={} onClick={onInviteClick} > - {t("pages.account.team.inviteMember")} + {t("common.buttons.inviteMember")} )} {isSuperAdmin && ( @@ -69,7 +69,7 @@ export const HeaderTeamControls = ({ color="primary" startIcon={} > - {t("pages.account.team.addMember")} + {t("common.buttons.addMember")} )} diff --git a/client/src/Pages/Account/components/InviteTeamMemberDialog.tsx b/client/src/Pages/Account/components/InviteTeamMemberDialog.tsx index 5029a5800..24f1f58f9 100644 --- a/client/src/Pages/Account/components/InviteTeamMemberDialog.tsx +++ b/client/src/Pages/Account/components/InviteTeamMemberDialog.tsx @@ -1,9 +1,20 @@ import { Stack } from "@mui/material"; import MenuItem from "@mui/material/MenuItem"; -import { useTheme } from "@mui/material"; +import { useTheme, FormHelperText, Typography } from "@mui/material"; +import { useState } from "react"; import { useTranslation } from "react-i18next"; +import { useForm, Controller } from "react-hook-form"; import { Dialog, TextField, Select, Button } from "@/Components/v2/inputs"; import type { UserRole } from "@/Types/User"; +import { useInviteForm } from "@/Hooks/useInviteForm"; +import type { InviteFormData } from "@/Validation/invite"; +import { usePost } from "@/Hooks/UseApi"; + +const CLIENT_HOST = import.meta.env.VITE_APP_CLIENT_HOST; + +interface InviteResponse { + token: string; +} interface InviteTeamMemberDialogProps { open: boolean; @@ -16,29 +27,70 @@ export const InviteTeamMemberDialog = ({ }: InviteTeamMemberDialogProps) => { const theme = useTheme(); const { t } = useTranslation(); + const { resolver, defaults } = useInviteForm(); + + const { control, handleSubmit, reset } = useForm({ + resolver, + defaultValues: defaults, + values: defaults, + }); + + const { post: generateToken, loading: generateLoading } = usePost< + InviteFormData, + InviteResponse + >(); + const { post: sendInvite, loading: sendLoading } = usePost< + InviteFormData, + InviteResponse + >(); + const [inviteLink, setInviteLink] = useState(null); const roleOptions: { value: UserRole; label: string }[] = [ { value: "admin", label: t("common.auth.roles.admin") }, { value: "user", label: t("common.auth.roles.user") }, ]; + const handleGenerateToken = async (data: InviteFormData) => { + const result = await generateToken("/invite", data); + if (result?.data?.token) { + const token = result.data.token; + const link = CLIENT_HOST ? `${CLIENT_HOST}/register/${token}` : token; + setInviteLink(link); + } + }; + + const handleSendInvite = async (data: InviteFormData) => { + const result = await sendInvite("/invite/send", data); + if (result?.success) { + handleClose(); + } + }; + + const handleClose = () => { + reset(); + setInviteLink(null); + onClose(); + }; + return ( {}} + onCancel={handleClose} + onConfirm={handleSubmit(handleSendInvite)} + confirmText={t("common.buttons.sendInvite")} + loading={sendLoading || generateLoading} maxWidth="sm" fullWidth - confirmText={t("pages.account.team.invite.sendInvite")} additionalButtons={ } > @@ -46,27 +98,66 @@ export const InviteTeamMemberDialog = ({ gap={theme.spacing(4)} mt={theme.spacing(4)} > - ( + + )} /> - + ( + <> + + {fieldState.error && ( + {fieldState.error.message} + )} + + )} + /> + {inviteLink && ( + <> + + {t("pages.account.team.invite.linkLabel")} + + + + )} ); diff --git a/client/src/Validation/invite.ts b/client/src/Validation/invite.ts new file mode 100644 index 000000000..fecbc0158 --- /dev/null +++ b/client/src/Validation/invite.ts @@ -0,0 +1,9 @@ +import { z } from "zod"; +import { UserRoles } from "@/Types/User"; + +export const inviteSchema = z.object({ + email: z.email("Please enter a valid email address"), + role: z.array(z.enum(UserRoles)).min(1, "Please select a role"), +}); + +export type InviteFormData = z.infer; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 5aba9ab6d..36b89762c 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -167,6 +167,7 @@ "uploadSuccess": "Monitors created successfully!" }, "bytesSent": "Bytes Sent", + "addMember": "Add member", "cancel": "Cancel", "checkHooks": { "failureResolveOne": "Failed to resolve incident." @@ -195,16 +196,20 @@ "home": "Home" }, "buttons": { + "addMember": "Add member", "cancel": "Cancel", "close": "Close", "configure": "Configure", "confirm": "Confirm", "create": "Create", "delete": "Delete", + "generateToken": "Generate token", "incidents": "Incidents", + "inviteMember": "Invite member", "pause": "Pause", "resume": "Resume", "save": "Save", + "sendInvite": "Send invite", "test": "Test", "testNotifications": "Test notifications", "toggleTheme": "Toggles light & dark", @@ -299,6 +304,7 @@ "createMonitor": "Create monitor", "createNew": "Create new", "delete": "Delete", + "generateToken": "Generate token", "DeleteAccountButton": "Remove account", "DeleteAccountTitle": "Remove account", "DeleteAccountWarning": "Removing your account means you won't be able to sign in again and all your data will be removed. This isn't reversible.", @@ -459,6 +465,7 @@ "discussions": "Discussions", "docs": "Docs", "incidents": "Incidents", + "inviteMember": "Invite member", "infrastructure": "Infrastructure", "logOut": "Log out", "logs": "Logs", @@ -559,8 +566,6 @@ } }, "team": { - "addMember": "Add member", - "inviteMember": "Invite member", "filter": { "placeholder": "Filter by role", "all": "All roles", @@ -584,8 +589,7 @@ "label": "Role", "placeholder": "Select a role" }, - "sendInvite": "Send invite", - "generateToken": "Generate token" + "linkLabel": "Invite link" } } }, @@ -675,8 +679,10 @@ "actions": { "configure": "Configure", "delete": "Delete", + "generateToken": "Generate token", "details": "Details", "incidents": "Incidents", + "inviteMember": "Invite member", "openSite": "Open site", "pause": "Pause", "resume": "Resume" @@ -1341,6 +1347,7 @@ "teamMember": "Team member" }, "save": "Save", + "sendInvite": "Send invite", "selectAll": "Select all", "settingsFailedToClearStats": "Failed to clear stats", "settingsFailedToDeleteMonitors": "Failed to delete all monitors", @@ -1467,6 +1474,7 @@ "description": "Create a new user and share the credentials with them. This method gives the member immediate access to all monitors.", "title": "Register new team member" }, + "addMember": "Add member", "cancel": "Cancel", "changeTeamPassword": { "changePasswordMenu": "Reset Password",