Update AddOrgMemberModal.tsx

This commit is contained in:
Daniel Hougaard
2024-08-23 02:25:15 +04:00
parent 2ab5932693
commit 59b16f647e
@@ -1,65 +1,112 @@
import { Controller, useForm } from "react-hook-form"; 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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup"; import { zodResolver } from "@hookform/resolvers/zod";
import * as yup from "yup"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; 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 { useOrganization } from "@app/context";
import { useToggle } from "@app/hooks"; import {
import { useAddUserToOrg, useFetchServerStatus } from "@app/hooks/api"; useAddUsersToOrg,
useFetchServerStatus,
useGetOrgRoles,
useGetUserWorkspaces
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const addMemberFormSchema = yup.object({ import { OrgInviteLink } from "./OrgInviteLink";
email: yup.string().email().required().label("Email").trim().lowercase()
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<typeof addMemberFormSchema>; type TAddMemberForm = z.infer<typeof addMemberFormSchema>;
type Props = { type Props = {
popUp: UsePopUpState<["addMember"]>; popUp: UsePopUpState<["addMember"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["addMember"]>, state?: boolean) => void; handlePopUpToggle: (popUpName: keyof UsePopUpState<["addMember"]>, state?: boolean) => void;
completeInviteLink: string; completeInviteLinks: Array<{
setCompleteInviteLink: (link: string) => void; email: string;
link: string;
}> | null;
setCompleteInviteLinks: (links: Array<{ email: string; link: string }> | null) => void;
}; };
export const AddOrgMemberModal = ({ export const AddOrgMemberModal = ({
popUp, popUp,
handlePopUpToggle, handlePopUpToggle,
completeInviteLink, completeInviteLinks,
setCompleteInviteLink setCompleteInviteLinks
}: Props) => { }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { data: organizationRoles } = useGetOrgRoles(currentOrg?.id ?? "");
const { data: serverDetails } = useFetchServerStatus(); const { data: serverDetails } = useFetchServerStatus();
const { mutateAsync: addUserMutateAsync } = useAddUserToOrg(); const { mutateAsync: addUsersMutateAsync } = useAddUsersToOrg();
const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false);
const { const {
control, control,
handleSubmit, handleSubmit,
watch,
reset, reset,
formState: { isSubmitting } formState: { isSubmitting }
} = useForm<TAddMemberForm>({ resolver: yupResolver(addMemberFormSchema) }); } = useForm<TAddMemberForm>({ resolver: zodResolver(addMemberFormSchema) });
const onAddMember = async ({ email }: TAddMemberForm) => { const onAddMembers = async ({ emails, organizationRoleSlug, projectIds }: TAddMemberForm) => {
if (!currentOrg?.id) return; if (!currentOrg?.id) return;
try { 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, 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. // only show this notification when email is configured.
// A [completeInviteLink] will not be sent if smtp is configured // A [completeInviteLink] will not be sent if smtp is configured
if (!data.completeInviteLink) { if (!data.completeInviteLinks) {
createNotification({ createNotification({
text: "Successfully invited user to the organization.", text: "Successfully invited user to the organization.",
type: "success" type: "success"
@@ -80,47 +127,153 @@ export const AddOrgMemberModal = ({
reset(); reset();
}; };
const copyTokenToClipboard = () => { const projectIds = watch("projectIds", []);
navigator.clipboard.writeText(completeInviteLink as string); const { data: projects } = useGetUserWorkspaces();
setInviteLinkCopied.on();
};
return ( return (
<Modal <Modal
isOpen={popUp?.addMember?.isOpen} isOpen={popUp?.addMember?.isOpen}
onOpenChange={(isOpen) => { onOpenChange={(isOpen) => {
handlePopUpToggle("addMember", isOpen); handlePopUpToggle("addMember", isOpen);
setCompleteInviteLink(""); setCompleteInviteLinks(null);
}} }}
> >
<ModalContent <ModalContent
title={`Invite others to ${currentOrg?.name}`} title={`Invite others to ${currentOrg?.name}`}
subTitle={ subTitle={
<div> <div>
{!completeInviteLink && ( {!completeInviteLinks && (
<div> <div>An invite is specific to an email address and expires after 1 day.</div>
An invite is specific to an email address and expires after 1 day.
<br />
For security reasons, you will need to separately add members to projects.
</div>
)} )}
{completeInviteLink && {completeInviteLinks &&
"This Infisical instance does not have a email provider setup. Please share this invite link with the invitee manually"} "This Infisical instance does not have a email provider setup. Please share this invite link with the invitee manually"}
</div> </div>
} }
> >
{!completeInviteLink && ( {!completeInviteLinks && (
<form onSubmit={handleSubmit(onAddMember)}> <form onSubmit={handleSubmit(onAddMembers)}>
<Controller <Controller
control={control} control={control}
defaultValue="" name="emails"
name="email"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}> <FormControl label="Email" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} /> <TextArea
{...field}
className="mt-1 h-20 w-full min-w-[30rem] rounded-md border border-mineshaft-500 bg-mineshaft-900/70 py-1 px-2 text-sm text-bunker-300 outline-none ring-primary-800 ring-opacity-70 transition-all placeholder:text-bunker-400 focus:ring-2"
placeholder="[email protected], [email protected]..."
/>
</FormControl> </FormControl>
)} )}
/> />
<div>
<Controller
control={control}
name="organizationRoleSlug"
render={({ field, fieldState: { error } }) => (
<FormControl
tooltipText="Select which organization role you want to assign to the user."
label="Assign organization role"
isError={Boolean(error)}
errorText={error?.message}
>
<div>
<Select
defaultValue={DEFAULT_ORG_MEMBER_ROLE_SLUG}
{...field}
onValueChange={(val) => field.onChange(val)}
>
{organizationRoles?.map((role) => (
<SelectItem key={role.id} value={role.slug}>
{role.name}
</SelectItem>
))}
</Select>
</div>
</FormControl>
)}
/>
</div>
<div className="-space-y-3">
<Controller
control={control}
name="projectIds"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Add user to projects"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
{projects && projects.length > 0 ? (
<div className="inline-flex w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
{projectIds.length === 1
? projects.find((project) => project.id === projectIds[0])?.name
: `${projectIds.length} projects selected`}
<FontAwesomeIcon icon={faChevronDown} className="text-xs" />
</div>
) : (
<div className="inline-flex w-full cursor-default items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
No projects found
</div>
)}
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="thin-scrollbar z-[100] max-h-80"
>
{projects && projects.length > 0 ? (
projects.map((project) => {
const isSelected = projectIds.includes(String(project.id));
return (
<DropdownMenuItem
onSelect={(event) => projects.length > 0 && event.preventDefault()}
onClick={() => {
if (projectIds.includes(String(project.id))) {
field.onChange(
projectIds.filter(
(projectId: string) => projectId !== String(project.id)
)
);
} else {
field.onChange([...projectIds, String(project.id)]);
}
}}
key={`project-id-${project.id}`}
icon={
isSelected ? (
<FontAwesomeIcon
icon={faCheckCircle}
className="pr-0.5 text-primary"
/>
) : (
<div className="pl-[1.01rem]" />
)
}
iconPos="left"
className="w-[28.4rem] text-sm"
>
{project.name}
</DropdownMenuItem>
);
})
) : (
<div />
)}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
<p className="ml-0.5 text-xs text-mineshaft-400">
Members will automatically be assigned the <a className="font-bold">Developer</a>{" "}
role for the projects you select.
</p>
</div>
<div className="mt-8 flex items-center"> <div className="mt-8 flex items-center">
<Button <Button
className="mr-4" className="mr-4"
@@ -141,20 +294,11 @@ export const AddOrgMemberModal = ({
</div> </div>
</form> </form>
)} )}
{completeInviteLink && ( {completeInviteLinks && (
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400"> <div className="space-y-3">
<p className="mr-4 break-all">{completeInviteLink}</p> {completeInviteLinks.map((invite) => (
<IconButton <OrgInviteLink key={`invite-${invite.email}`} invite={invite} />
ariaLabel="copy icon" ))}
colorSchema="secondary"
className="group relative"
onClick={copyTokenToClipboard}
>
<FontAwesomeIcon icon={isInviteLinkCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
click to copy
</span>
</IconButton>
</div> </div>
)} )}
</ModalContent> </ModalContent>