From 59b16f647eec128e1f029450c8078694644d45ce Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 22 Aug 2024 08:12:59 +0400 Subject: [PATCH] Update AddOrgMemberModal.tsx --- .../OrgMembersSection/AddOrgMemberModal.tsx | 252 ++++++++++++++---- 1 file changed, 198 insertions(+), 54 deletions(-) diff --git a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx index aedeb369a..f0dfe5b77 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx @@ -1,65 +1,112 @@ import { Controller, useForm } from "react-hook-form"; -import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; +import { faCheckCircle, faChevronDown } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { yupResolver } from "@hookform/resolvers/yup"; -import * as yup from "yup"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; import { createNotification } from "@app/components/notifications"; -import { Button, FormControl, IconButton, Input, Modal, ModalContent } from "@app/components/v2"; +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + FormControl, + Modal, + ModalContent, + Select, + SelectItem, + TextArea +} from "@app/components/v2"; import { useOrganization } from "@app/context"; -import { useToggle } from "@app/hooks"; -import { useAddUserToOrg, useFetchServerStatus } from "@app/hooks/api"; +import { + useAddUsersToOrg, + useFetchServerStatus, + useGetOrgRoles, + useGetUserWorkspaces +} from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; -const addMemberFormSchema = yup.object({ - email: yup.string().email().required().label("Email").trim().lowercase() +import { OrgInviteLink } from "./OrgInviteLink"; + +const DEFAULT_ORG_MEMBER_ROLE_SLUG = "member"; + +const EmailSchema = z.string().email().min(1).trim().toLowerCase(); + +const addMemberFormSchema = z.object({ + emails: z.string().min(1).trim().toLowerCase(), + projectIds: z.array(z.string().min(1).trim().toLowerCase()).default([]), + organizationRoleSlug: z.string().min(1).default(DEFAULT_ORG_MEMBER_ROLE_SLUG) }); -type TAddMemberForm = yup.InferType; +type TAddMemberForm = z.infer; type Props = { popUp: UsePopUpState<["addMember"]>; handlePopUpToggle: (popUpName: keyof UsePopUpState<["addMember"]>, state?: boolean) => void; - completeInviteLink: string; - setCompleteInviteLink: (link: string) => void; + completeInviteLinks: Array<{ + email: string; + link: string; + }> | null; + setCompleteInviteLinks: (links: Array<{ email: string; link: string }> | null) => void; }; export const AddOrgMemberModal = ({ popUp, handlePopUpToggle, - completeInviteLink, - setCompleteInviteLink + completeInviteLinks, + setCompleteInviteLinks }: Props) => { - const { currentOrg } = useOrganization(); + const { data: organizationRoles } = useGetOrgRoles(currentOrg?.id ?? ""); const { data: serverDetails } = useFetchServerStatus(); - const { mutateAsync: addUserMutateAsync } = useAddUserToOrg(); - - const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false); + const { mutateAsync: addUsersMutateAsync } = useAddUsersToOrg(); const { control, handleSubmit, + watch, reset, formState: { isSubmitting } - } = useForm({ resolver: yupResolver(addMemberFormSchema) }); + } = useForm({ resolver: zodResolver(addMemberFormSchema) }); - const onAddMember = async ({ email }: TAddMemberForm) => { + const onAddMembers = async ({ emails, organizationRoleSlug, projectIds }: TAddMemberForm) => { if (!currentOrg?.id) return; try { - const { data } = await addUserMutateAsync({ + const parsedEmails = emails + .replace(/\s/g, "") + .split(",") + .map((email) => { + if (EmailSchema.safeParse(email).success) { + return email.trim(); + } + + return null; + }); + + if (parsedEmails.includes(null)) { + createNotification({ + text: "Invalid email addresses provided.", + type: "error" + }); + return; + } + + const { data } = await addUsersMutateAsync({ organizationId: currentOrg?.id, - inviteeEmail: email + inviteeEmails: emails.split(",").map((email) => email.trim()), + organizationRoleSlug, + projectIds }); - setCompleteInviteLink(data?.completeInviteLink ?? ""); + setCompleteInviteLinks(data?.completeInviteLinks ?? null); // only show this notification when email is configured. // A [completeInviteLink] will not be sent if smtp is configured - if (!data.completeInviteLink) { + if (!data.completeInviteLinks) { createNotification({ text: "Successfully invited user to the organization.", type: "success" @@ -80,47 +127,153 @@ export const AddOrgMemberModal = ({ reset(); }; - const copyTokenToClipboard = () => { - navigator.clipboard.writeText(completeInviteLink as string); - setInviteLinkCopied.on(); - }; + const projectIds = watch("projectIds", []); + const { data: projects } = useGetUserWorkspaces(); return ( { handlePopUpToggle("addMember", isOpen); - setCompleteInviteLink(""); + setCompleteInviteLinks(null); }} > - {!completeInviteLink && ( -
- An invite is specific to an email address and expires after 1 day. -
- For security reasons, you will need to separately add members to projects. -
+ {!completeInviteLinks && ( +
An invite is specific to an email address and expires after 1 day.
)} - {completeInviteLink && + {completeInviteLinks && "This Infisical instance does not have a email provider setup. Please share this invite link with the invitee manually"} } > - {!completeInviteLink && ( -
+ {!completeInviteLinks && ( + ( - +