diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index fad3de8546..de359414e0 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1552,6 +1552,9 @@ "member_already_invited": "Member has already been invited", "already_in_use_error": "Username already in use", "enter_email_or_username": "Enter an email or username", + "enter_email": "Enter an email", + "enter_emails": "Enter emails", + "too_many_invites": "You are limited to inviting a maximum of {{nbUsers}} users at once.", "team_name_taken": "This name is already taken", "must_enter_team_name": "Must enter a team name", "team_url_required": "Must enter a team URL", diff --git a/packages/features/ee/teams/components/MemberInvitationModal.tsx b/packages/features/ee/teams/components/MemberInvitationModal.tsx index bde9365f72..6d555450b8 100644 --- a/packages/features/ee/teams/components/MemberInvitationModal.tsx +++ b/packages/features/ee/teams/components/MemberInvitationModal.tsx @@ -6,11 +6,12 @@ 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 } from "@calcom/lib/constants"; +import { IS_TEAM_BILLING_ENABLED, MAX_NB_INVITES } 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 { isEmail } from "@calcom/trpc/server/routers/viewer/teams/util"; import { Button, Dialog, @@ -199,7 +200,10 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps) setModalInputMode(val as ModalMode)} + onValueChange={(val) => { + setModalInputMode(val as ModalMode); + newMemberFormMethods.clearErrors(); + }} defaultValue={modalImportMode} options={toggleGroupOptions} /> @@ -213,8 +217,10 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps) name="emailOrUsername" control={newMemberFormMethods.control} rules={{ - required: t("enter_email_or_username"), + required: isOrg ? t("enter_email") : t("enter_email_or_username"), validate: (value) => { + // orgs can only invite members by email + if (typeof value === "string" && isOrg && !isEmail(value)) return t("enter_email"); if (typeof value === "string") return validateUniqueInvite(value) || t("member_already_invited"); }, @@ -241,7 +247,14 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps) name="emailOrUsername" control={newMemberFormMethods.control} rules={{ - required: t("enter_email_or_username"), + required: t("enter_email"), + validate: (value) => { + if (Array.isArray(value) && value.some((email) => !isEmail(email))) + return t("enter_emails"); + if (Array.isArray(value) && value.length > MAX_NB_INVITES) + return t("too_many_invites", { nbUsers: MAX_NB_INVITES }); + if (typeof value === "string" && !isEmail(value)) return t("enter_email"); + }, }} render={({ field: { onChange, value }, fieldState: { error } }) => ( <> diff --git a/packages/lib/constants.ts b/packages/lib/constants.ts index 6f1a66ab9a..25cd09580c 100644 --- a/packages/lib/constants.ts +++ b/packages/lib/constants.ts @@ -119,3 +119,6 @@ export const AB_TEST_BUCKET_PROBABILITY = defaultOnNaN( export const IS_PREMIUM_USERNAME_ENABLED = (IS_CALCOM || (process.env.NEXT_PUBLIC_IS_E2E && IS_STRIPE_ENABLED)) && process.env.NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE_MONTHLY; + +// Max number of invites to join a team/org that can be sent at once +export const MAX_NB_INVITES = 100; diff --git a/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.schema.ts b/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.schema.ts index 19f354a59e..f081508bc5 100644 --- a/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.schema.ts +++ b/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.schema.ts @@ -1,5 +1,6 @@ import { z } from "zod"; +import { MAX_NB_INVITES } from "@calcom/lib/constants"; import { MembershipRole } from "@calcom/prisma/enums"; export const ZInviteMemberInputSchema = z.object({ @@ -15,13 +16,13 @@ export const ZInviteMemberInputSchema = z.object({ .refine( (value) => { if (Array.isArray(value)) { - if (value.length > 100) { + if (value.length > MAX_NB_INVITES) { return false; } } return true; }, - { message: "You are limited to inviting a maximum of 100 users at once." } + { message: `You are limited to inviting a maximum of ${MAX_NB_INVITES} users at once.` } ) .refine( (value) => {