mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 08:26:29 +00:00
Update AddOrgMemberModal.tsx
This commit is contained in:
+198
-54
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user