From d1a877e70db9b6d734b1c4b147f81756ac80d64e Mon Sep 17 00:00:00 2001 From: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Date: Tue, 31 Jan 2023 12:36:38 -0500 Subject: [PATCH] Add additional guests (#6419) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add additional guests * WIP * WIP * Type fix * WIP * Add validation for already invited attendees * Revert type to email * remove controlled fields * disables signups via env variable (#6212) * disables signups via env variable * Apply suggestions from code review Co-authored-by: Peer Richelsen Co-authored-by: Omar López * Reusable Upgrade component for teams only features (#6473) Co-authored-by: CarinaWolli * i18n udpate * fix: darkmode in booking/[id] page (#6458) * fix: text-area style * fix: border styles * fix: german translation for reschedule booking email subject (#6483) * fix: german translation for reschedule subject * fix: add a full stop * fix: app store spacing issues (#6481) * feat: add equal spacing with gap * fix: title style * fix: title style * fix: remove margin * fix: remove margin * fix: sub header spacing and slider button * fix: category card text * fix: app store spacing issues * fix: tab in tablet * fix: shell * fix: styling of dropdown component (#6440) * fix: styling of dropdown component Signed-off-by: Udit Takkar * Apply suggestions from code review * fix: use dropdown item in all pages Signed-off-by: Udit Takkar * chore: undo Signed-off-by: Udit Takkar Signed-off-by: Udit Takkar Co-authored-by: Peer Richelsen * readding sidebar tips for everyone, added 3 more features to teams empty screen (#6475) * readding sidebar tips for everyone * added 3 more features to teams plan * responsive fixes * responsive fixes * small responsive fix * CTA alignment fixed * Minor alignment fixes to Shell for event-types/[type] Co-authored-by: Alex van Andel * fix: wrong i18n placeholder in many locales (#6496) * fix: czech translation * fix: spanish translation * fix: italian translation * fix: japanese translation * fix: korean translation * fix: dutch translation * fix: norwegian translation * fix: polish translation * fix: portuguese translation * fix: portuguese translation * fix: romanian translation * fix: russian translation * fix: serbian translation * fix: swedish translation * fix: turkish translation * fix: ukrainian translation * fix: vietnamese translation * fix: chinese simplified translation * fix: chinese traditional translation * Disable redirect for cancellation page (#6498) * [CAL-677] /event-type : Event Setup sidebar item doesn't reflect multiple durations (#6390) Co-authored-by: gitstart-calcom * Added fallback to hostsFixed too which is used in COLLECTIVE (#6500) * Don't update the field when changing the select (#6486) * Don't update the field when changing the select * Fixing missing label * Applied the correct y-8 vertical margin * Changed the description vertical offset from title a bit more * Add Trash icon on Availability list dropdown * fix: tablet sidebar profile being too wide and not centered (#6490) * feat: updated app list items styling issues inside settings/conferencing * feat: fixed tablet sidebar width and not centered issue * fix: removed un-related changes from this pr * deleted .env * fix: added the accidentaly deleted .env file * fix: removed unwanted .env file from the pr Co-authored-by: Peer Richelsen * Bring back PublicShell (#6505) * Add red text to error message * WIP * Rearrange guest input * Fix validate unique guests logic * Allow guests on reschedule * Lint fix * More lint fixes * Lint fixes * More lint fixes * Fix remove guest button margin --------- Signed-off-by: Udit Takkar Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Jeff Loiselle Co-authored-by: Peer Richelsen Co-authored-by: Omar López Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Co-authored-by: CarinaWolli Co-authored-by: Peer Richelsen Co-authored-by: Nafees Nazik <84864519+G3root@users.noreply.github.com> Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Co-authored-by: Alex van Andel Co-authored-by: Hariom Balhara Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com> Co-authored-by: gitstart-calcom Co-authored-by: Ronit Panda <72537293+rtpa25@users.noreply.github.com> Co-authored-by: Alan --- .../components/booking/pages/BookingPage.tsx | 205 +++++++++++------- apps/web/public/static/locales/en/common.json | 1 + packages/trpc/server/routers/viewer/teams.tsx | 2 +- packages/ui/components/form/inputs/Input.tsx | 1 - packages/ui/form/AddressInput.tsx | 2 +- 5 files changed, 134 insertions(+), 77 deletions(-) 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 ? ( +
+
+ +
    + {guestsField.fields.map((field, index) => ( +
  • + } + required + addOnSuffix={ + + + + } + /> + {bookingForm.formState.errors.guests?.[index] && ( +
    + +

    + {bookingForm.formState.errors.guests?.[index]?.message} +

    +
    + )} +
  • + ))} +
+ +
+
+ ) : ( + <> + )} + + {!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;