diff --git a/apps/web/components/booking/pages/BookingPage.tsx b/apps/web/components/booking/pages/BookingPage.tsx index 1432820ba6..17fbba2e8d 100644 --- a/apps/web/components/booking/pages/BookingPage.tsx +++ b/apps/web/components/booking/pages/BookingPage.tsx @@ -6,9 +6,8 @@ import { useSession } from "next-auth/react"; import Head from "next/head"; import { useRouter } from "next/router"; import { useEffect, useMemo, useReducer, useState } from "react"; -import { Controller, useForm, useWatch } from "react-hook-form"; +import { useFieldArray, useForm, useWatch, Controller } from "react-hook-form"; import { FormattedNumber, IntlProvider } from "react-intl"; -import { ReactMultiEmail } from "react-multi-email"; import { v4 as uuidv4 } from "uuid"; import { z } from "zod"; @@ -40,15 +39,26 @@ import { HttpError } from "@calcom/lib/http-error"; import { getEveryFreqFor } from "@calcom/lib/recurringStrings"; import slugify from "@calcom/lib/slugify"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; -import { AddressInput, Button, EmailInput, Form, PhoneInput, Tooltip, Group, RadioField } from "@calcom/ui"; import { - FiAlertTriangle, + AddressInput, + Button, + EmailField, + EmailInput, + Form, + Group, + PhoneInput, + RadioField, + Tooltip, +} from "@calcom/ui"; +import { + FiUserPlus, FiCalendar, - FiCreditCard, + FiX, FiInfo, + FiCreditCard, FiRefreshCw, FiUser, - FiUserPlus, + FiAlertTriangle, } from "@calcom/ui/components/icon"; import { asStringOrNull } from "@lib/asStringOrNull"; @@ -73,7 +83,7 @@ type BookingFormValues = { email: string; notes?: string; locationType?: EventLocationType["type"]; - guests?: string[]; + guests?: { email: string }[]; address?: string; attendeeAddress?: string; phone?: string; @@ -192,7 +202,9 @@ const BookingPage = ({ const loggedInIsOwner = eventType?.users[0]?.id === session?.user?.id; const guestListEmails = !isDynamicGroupBooking - ? booking?.attendees.slice(1).map((attendee) => attendee.email) + ? booking?.attendees.slice(1).map((attendee) => { + return { email: attendee.email }; + }) : []; // There should only exists one default userData variable for primaryAttendee. @@ -211,7 +223,9 @@ const BookingPage = ({ name: defaultUserValues.name || (!loggedInIsOwner && session?.user?.name) || "", email: defaultUserValues.email || (!loggedInIsOwner && session?.user?.email) || "", notes: (router.query.notes as string) || "", - guests: ensureArray(router.query.guest) as string[], + guests: ensureArray(router.query.guest).map((guest) => { + return { email: guest as string }; + }), customInputs: eventType.customInputs.reduce( (customInputs, input) => ({ ...customInputs, @@ -253,6 +267,7 @@ const BookingPage = ({ .object({ name: z.string().min(1), email: z.string().trim().email(), + guests: z.array(z.object({ email: z.string().email() })).optional(), phone: z .string() .refine((val) => isValidPhoneNumber(val)) @@ -271,6 +286,10 @@ const BookingPage = ({ defaultValues: defaultValues(), resolver: zodResolver(bookingFormSchema), // Since this isn't set to strict we only validate the fields in the schema }); + const guestsField = useFieldArray({ + name: "guests", + control: bookingForm.control, + }); const selectedLocationType = useWatch({ control: bookingForm.control, @@ -309,6 +328,7 @@ const BookingPage = ({ } const bookEvent = (booking: BookingFormValues) => { + bookingForm.clearErrors(); const bookingCustomInputs = Object.keys(booking.customInputs || {}).map((inputId) => ({ label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "", value: booking.customInputs && booking.customInputs[inputId] ? booking.customInputs[inputId] : "", @@ -376,6 +396,29 @@ const BookingPage = ({ } } + // Validate that guests are unique + let alreadyInvited = false; + booking.guests?.forEach((guest, index) => { + if (guest.email === booking.email) { + bookingForm.setError(`guests.${index}`, { type: "validate", message: t("already_invited") }); + alreadyInvited = true; + } + + if (booking.guests) { + let guestCount = 0; + for (const checkGuest of booking.guests) { + if (checkGuest.email === guest.email) guestCount++; + if (guestCount > 1) { + bookingForm.setError(`guests.${index}`, { type: "validate", message: t("already_invited") }); + alreadyInvited = true; + break; + } + } + } + }); + + if (alreadyInvited) return; + if (recurringDates.length) { // Identify set of bookings to one intance of recurring event to support batch changes const recurringEventId = uuidv4(); @@ -406,6 +449,7 @@ const BookingPage = ({ ? booking.phone : booking.smsReminderNumber || undefined, ethSignature: gateState.rainbowToken, + guests: booking.guests?.map((guest) => guest.email), })); recurringMutation.mutate(recurringBookings); } else { @@ -434,6 +478,7 @@ const BookingPage = ({ ? booking.phone : booking.smsReminderNumber || undefined, ethSignature: gateState.rainbowToken, + guests: booking.guests?.map((guest) => guest.email), }); } }; @@ -850,60 +895,7 @@ const BookingPage = ({ )} ))} - {!eventType.disableGuests && guestToggle && ( -
-
- - {!disableInput && ( - ( - guest@example.com} - emails={value} - onChange={onChange} - getLabel={( - email: string, - index: number, - removeEmail: (index: number) => void - ) => { - return ( -
- {email} - {!disableInput && ( - removeEmail(index)}> - × - - )} -
- ); - }} - /> - )} - /> - )} - {/* Custom code when guest emails should not be editable */} - {disableInput && guestListEmails && guestListEmails.length > 0 && ( -
- {/* // @TODO: user owners are appearing as guest here when should be only user input */} - {guestListEmails.map((email, index) => { - return ( -
- {email} -
- ); - })} -
- )} -
-
- )} + {isSmsReminderNumberNeeded && selectedLocationType !== LocationType.Phone && (
+ {!eventType.disableGuests && guestsField.fields.length ? ( +
+
+ + + +
+
+ ) : ( + <> + )} + + {!eventType.disableGuests && !guestsField.fields.length && ( + + )}
- {!eventType.disableGuests && !guestToggle && ( - diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index c7a9b1aa21..3673aae687 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1511,6 +1511,7 @@ "install_google_meet": "Install Google Meet", "install_google_calendar": "Install Google Calendar", "sender_name": "Sender name", + "already_invited": "Attendee already invited", "no_recordings_found": "No recordings found", "reporting": "Reporting", "reporting_feature": "See all incoming from data and download it as a CSV", diff --git a/packages/trpc/server/routers/viewer/teams.tsx b/packages/trpc/server/routers/viewer/teams.tsx index b157ae2cda..4f47189e13 100644 --- a/packages/trpc/server/routers/viewer/teams.tsx +++ b/packages/trpc/server/routers/viewer/teams.tsx @@ -683,7 +683,7 @@ export const viewerTeamsRouter = router({ }, }, }); - type UserMap = Record; + type UserMap = Record; // flattern users to be unique by id const users = teams .flatMap((t) => t.members) diff --git a/packages/ui/components/form/inputs/Input.tsx b/packages/ui/components/form/inputs/Input.tsx index 06ae8e9909..a1176460c0 100644 --- a/packages/ui/components/form/inputs/Input.tsx +++ b/packages/ui/components/form/inputs/Input.tsx @@ -143,7 +143,6 @@ export const InputField = forwardRef(function {...passThrough} {...(type == "search" && { onChange: (e) => { - console.log(e.target.value); setInputValue(e.target.value); props.onChange && props.onChange(e); }, diff --git a/packages/ui/form/AddressInput.tsx b/packages/ui/form/AddressInput.tsx index e3d1c59479..95c2a4a37b 100644 --- a/packages/ui/form/AddressInput.tsx +++ b/packages/ui/form/AddressInput.tsx @@ -10,7 +10,7 @@ type BookingFormValues = { email: string; notes?: string; locationType?: EventLocationType["type"]; - guests?: string[]; + guests?: { email: string }[]; address?: string; attendeeAddress?: string; phone?: string;