import { BuildingIcon, PaperclipIcon, UserIcon, Users } from "lucide-react"; import { Trans } from "next-i18next"; import { useMemo, useState, useRef } from "react"; import type { FormEvent } from "react"; import { Controller, useForm } from "react-hook-form"; import TeamInviteFromOrg from "@calcom/ee/organizations/components/TeamInviteFromOrg"; import { classNames } from "@calcom/lib"; import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { MembershipRole } from "@calcom/prisma/enums"; import type { RouterOutputs } from "@calcom/trpc"; import { trpc } from "@calcom/trpc"; import { Button, CheckboxField, Dialog, DialogContent, DialogFooter, Form, Label, showToast, TextField, ToggleGroup, Select, TextAreaField, } from "@calcom/ui"; import { Link } from "@calcom/ui/components/icon"; import type { PendingMember } from "../lib/types"; import { GoogleWorkspaceInviteButton } from "./GoogleWorkspaceInviteButton"; type MemberInvitationModalProps = { isOpen: boolean; justEmailInvites?: boolean; onExit: () => void; orgMembers?: RouterOutputs["viewer"]["organizations"]["getMembers"]; onSubmit: (values: NewMemberForm, resetFields: () => void) => void; onSettingsOpen?: () => void; teamId: number; members?: PendingMember[]; token?: string; isLoading?: boolean; disableCopyLink?: boolean; isOrg?: boolean; }; type MembershipRoleOption = { value: MembershipRole; label: string; }; export interface NewMemberForm { emailOrUsername: string | string[]; role: MembershipRole; sendInviteEmail: boolean; } type ModalMode = "INDIVIDUAL" | "BULK" | "ORGANIZATION"; interface FileEvent extends FormEvent { target: EventTarget & T; } function toggleElementInArray(value: string[] | string | undefined, element: string): string[] { const array = value ? (Array.isArray(value) ? value : [value]) : []; return array.includes(element) ? array.filter((item) => item !== element) : [...array, element]; } export default function MemberInvitationModal(props: MemberInvitationModalProps) { const { t } = useLocale(); const { disableCopyLink = false, isOrg = false } = props; const trpcContext = trpc.useContext(); const [modalImportMode, setModalInputMode] = useState( props?.orgMembers && props.orgMembers?.length > 0 ? "ORGANIZATION" : "INDIVIDUAL" ); const createInviteMutation = trpc.viewer.teams.createInvite.useMutation({ onSuccess(token) { copyInviteLinkToClipboard(token); trpcContext.viewer.teams.get.invalidate(); trpcContext.viewer.teams.list.invalidate(); }, onError: (error) => { showToast(error.message, "error"); }, }); const copyInviteLinkToClipboard = async (token: string) => { const isOrgInvite = isOrg; const teamInviteLink = `${WEBAPP_URL}/teams?token=${token}`; const orgInviteLink = `${WEBAPP_URL}/signup?token=${token}&callbackUrl=/getting-started`; const inviteLink = isOrgInvite || (props?.orgMembers && props.orgMembers?.length > 0) ? orgInviteLink : teamInviteLink; await navigator.clipboard.writeText(inviteLink); showToast(t("invite_link_copied"), "success"); }; const options: MembershipRoleOption[] = useMemo(() => { return [ { value: MembershipRole.MEMBER, label: t("member") }, { value: MembershipRole.ADMIN, label: t("admin") }, { value: MembershipRole.OWNER, label: t("owner") }, ]; }, [t]); const toggleGroupOptions = useMemo(() => { const array = [ { value: "INDIVIDUAL", label: t("invite_team_individual_segment"), iconLeft: , }, { value: "BULK", label: t("invite_team_bulk_segment"), iconLeft: }, ]; if (props?.orgMembers && props.orgMembers?.length > 0) { array.unshift({ value: "ORGANIZATION", label: t("organization"), iconLeft: , }); } return array; }, [t, props.orgMembers]); const newMemberFormMethods = useForm(); const validateUniqueInvite = (value: string) => { if (!props?.members?.length) return true; return !( props?.members.some((member) => member?.username === value) || props?.members.some((member) => member?.email === value) ); }; const handleFileUpload = (e: FileEvent) => { if (!e.target.files?.length) { return; } const file = e.target.files[0]; if (file) { const reader = new FileReader(); const emailRegex = /^([A-Z0-9_+-]+\.?)*[A-Z0-9_+-]@([A-Z0-9][A-Z0-9-]*\.)+[A-Z]{2,}$/i; reader.onload = (e) => { const contents = e?.target?.result as string; const lines = contents.split("\n"); const validEmails = []; for (const line of lines) { const columns = line.split(/,|;|\|| /); for (const column of columns) { const email = column.trim().toLowerCase(); if (emailRegex.test(email)) { validEmails.push(email); break; // Stop checking columns if a valid email is found in this line } } } newMemberFormMethods.setValue("emailOrUsername", validEmails); }; reader.readAsText(file); } }; const resetFields = () => { newMemberFormMethods.reset(); newMemberFormMethods.setValue("emailOrUsername", ""); setModalInputMode("INDIVIDUAL"); }; const importRef = useRef(null); return ( { props.onExit(); newMemberFormMethods.reset(); }}> Note: This will cost an extra seat ($15/m){" "} on your subscription. ) : null }>
setModalInputMode(val as ModalMode)} defaultValue={modalImportMode} options={toggleGroupOptions} />
props.onSubmit(values, resetFields)}>
{/* Indivdual Invite */} {modalImportMode === "INDIVIDUAL" && ( { if (typeof value === "string") return validateUniqueInvite(value) || t("member_already_invited"); }, }} render={({ field: { onChange }, fieldState: { error } }) => ( <> onChange(e.target.value.trim().toLowerCase())} /> {error && {error.message}} )} /> )} {/* Bulk Invite */} {modalImportMode === "BULK" && (
( <> {/* TODO: Make this a fancy email input that styles on a successful email. */} { const targetValues = e.target.value.split(","); const emails = targetValues.length === 1 ? targetValues[0].trim().toLocaleLowerCase() : targetValues.map((email) => email.trim().toLocaleLowerCase()); return onChange(emails); }} /> {error && {error.message}} )} /> { newMemberFormMethods.setValue("emailOrUsername", data); }} />
)} {modalImportMode === "ORGANIZATION" && ( ( <> { // If 'value' is not an array, create a new array with 'userEmail' to allow future updates to the array. // If 'value' is an array, update the array by either adding or removing 'userEmail'. const newValue = toggleElementInArray(value, userEmail); onChange(newValue); }} orgMembers={props.orgMembers} /> )} /> )} (