From 517cfde5b8a0e4910ea27f21445a516ea372a3ce Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Thu, 2 Mar 2023 23:45:28 +0530 Subject: [PATCH] Feature/ Manage Booking Questions (#6560) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * WIP * Create Booking Questions builder * Renaming things * wip * wip * Implement Add Guests and other fixes * Fixes after testing * Fix wrong status code 404 * Fixes * Lint fixes * Self review comments addressed * More self review comments addressed * Feedback from zomars * BugFixes after testing * More fixes discovered during review * Update packages/lib/hooks/useHasPaidPlan.ts Co-authored-by: Omar López * More fixes discovered during review * Update packages/ui/components/form/inputs/Input.tsx Co-authored-by: Omar López * More fixes discovered during review * Update packages/features/bookings/lib/getBookingFields.ts Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> * More PR review fixes * Hide label using labelSrOnly * Fix Carinas feedback and implement 2 workflows thingy * Misc fixes * Fixes from Loom comments and PR * Fix a lint errr * Fix cancellation reason * Fix regression in edit due to name conflict check * Update packages/features/form-builder/FormBuilder.tsx Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> * Fix options not set when default value is used * Restoring reqBody to avoid uneeded conflicts with main * Type fix * Update apps/web/components/booking/pages/BookingPage.tsx Co-authored-by: Omar López * Update packages/features/form-builder/FormBuilder.tsx Co-authored-by: Omar López * Update apps/web/components/booking/pages/BookingPage.tsx Co-authored-by: Omar López * Apply suggestions from code review Co-authored-by: Omar López * Show fields but mark them disabled * Apply suggestions from code review Co-authored-by: Omar López * More comments * Fix booking success page crash when a booking doesnt have newly added required fields response * Dark theme asterisk not visible * Make location required in zodSchema as was there in production * Linting * Remove _metadata.ts files for apps that have config.json * Revert "Remove _metadata.ts files for apps that have config.json" This reverts commit d79bdd336cf312a30a8943af94c059947bd91ccd. * Fix lint error * Fix missing condition for samlSPConfig * Delete unexpectedly added file * yarn.lock change not required * fix types * Make checkboxes rounded * Fix defaultLabel being stored as label due to SSR rendering * Shaved 16kb from booking page * Explicit types for profile * Show payment value only if price is greater than 0 * Fix type error * Add back inferred types as they are failing * Fix duplicate label on number --------- Co-authored-by: zomars Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Co-authored-by: Efraín Rochín --- .../booking/BookingDescriptionPayment.tsx | 24 + .../components/booking/pages/BookingPage.tsx | 870 +++++------------- .../components/dialog/EditLocationDialog.tsx | 11 +- .../eventtype/CustomInputTypeForm.tsx | 214 ----- .../components/eventtype/EventAdvancedTab.tsx | 192 +--- apps/web/lib/getBooking.tsx | 80 +- .../lib/mutations/bookings/create-booking.ts | 11 +- .../bookings/create-recurring-booking.ts | 3 +- apps/web/lib/types/booking.ts | 30 - apps/web/pages/[user]/book.tsx | 6 +- apps/web/pages/api/book/event.ts | 4 +- apps/web/pages/booking/[uid].tsx | 124 +-- apps/web/pages/d/[link]/book.tsx | 3 +- apps/web/pages/event-types/[type]/index.tsx | 85 +- apps/web/pages/team/[slug]/[type].tsx | 16 +- apps/web/pages/team/[slug]/book.tsx | 15 +- apps/web/playwright/webhook.e2e.ts | 2 + apps/web/public/static/locales/en/common.json | 8 + .../config/config.tsx | 3 + .../react-awesome-query-builder/widgets.tsx | 115 ++- .../emails/src/components/CustomInputs.tsx | 18 - .../src/components/UserFieldsResponses.tsx | 18 + packages/emails/src/components/index.ts | 2 +- .../src/templates/BaseScheduledEmail.tsx | 4 +- .../features/bookings/lib/getBookingFields.ts | 332 +++++++ .../bookings/lib/getBookingResponsesSchema.ts | 188 ++++ .../features/bookings/lib/handleNewBooking.ts | 190 +++- packages/features/ee/sso/lib/jackson.ts | 6 +- packages/features/ee/teams/api/upgrade.ts | 2 +- .../workflows/components/AddActionDialog.tsx | 16 +- .../components/EventWorkflowsTab.tsx | 2 + .../components/WorkflowStepContainer.tsx | 31 +- .../eventtypes/components/CustomInputItem.tsx | 42 - .../features/eventtypes/components/index.ts | 1 - .../eventtypes/lib/bookingFieldsManager.ts | 141 +++ packages/features/form-builder/Components.tsx | 384 ++++++++ .../features/form-builder/FormBuilder.tsx | 733 +++++++++++++++ .../form-builder/FormBuilderFieldsSchema.ts | 79 ++ packages/features/form-builder/index.ts | 5 + packages/lib/CalEventParser.ts | 17 +- packages/lib/defaultEvents.ts | 9 + packages/lib/getEventTypeById.ts | 5 +- packages/lib/test/builder.ts | 2 + .../migration.sql | 5 + packages/prisma/schema.prisma | 4 + packages/prisma/seed.ts | 4 +- packages/prisma/selects/event-types.ts | 1 + packages/prisma/zod-utils.ts | 52 +- .../trpc/server/routers/viewer/eventTypes.ts | 23 + .../trpc/server/routers/viewer/workflows.tsx | 86 ++ packages/types/Calendar.d.ts | 6 + packages/ui/components/form/inputs/Input.tsx | 2 + packages/ui/components/form/inputs/Label.tsx | 5 +- packages/ui/components/form/select/Select.tsx | 1 + packages/ui/components/form/switch/Switch.tsx | 69 +- .../form/toggleGroup/BooleanToggleGroup.tsx | 4 +- packages/ui/form/AddressInput.tsx | 46 +- packages/ui/form/PhoneInput.tsx | 33 +- 58 files changed, 2921 insertions(+), 1463 deletions(-) create mode 100644 apps/web/components/booking/BookingDescriptionPayment.tsx delete mode 100644 apps/web/components/eventtype/CustomInputTypeForm.tsx delete mode 100644 packages/emails/src/components/CustomInputs.tsx create mode 100644 packages/emails/src/components/UserFieldsResponses.tsx create mode 100644 packages/features/bookings/lib/getBookingFields.ts create mode 100644 packages/features/bookings/lib/getBookingResponsesSchema.ts delete mode 100644 packages/features/eventtypes/components/CustomInputItem.tsx create mode 100644 packages/features/eventtypes/lib/bookingFieldsManager.ts create mode 100644 packages/features/form-builder/Components.tsx create mode 100644 packages/features/form-builder/FormBuilder.tsx create mode 100644 packages/features/form-builder/FormBuilderFieldsSchema.ts create mode 100644 packages/features/form-builder/index.ts create mode 100644 packages/prisma/migrations/20230124154235_add_booking_fields/migration.sql diff --git a/apps/web/components/booking/BookingDescriptionPayment.tsx b/apps/web/components/booking/BookingDescriptionPayment.tsx new file mode 100644 index 0000000000..92fa5e755b --- /dev/null +++ b/apps/web/components/booking/BookingDescriptionPayment.tsx @@ -0,0 +1,24 @@ +import { FormattedNumber, IntlProvider } from "react-intl"; + +import getPaymentAppData from "@calcom/lib/getPaymentAppData"; +import { FiCreditCard } from "@calcom/ui/components/icon"; + +const BookingDescriptionPayment = (props: { eventType: Parameters[0] }) => { + const paymentAppData = getPaymentAppData(props.eventType); + if (!paymentAppData || paymentAppData.price <= 0) return null; + + return ( +

+ + + + +

+ ); +}; + +export default BookingDescriptionPayment; diff --git a/apps/web/components/booking/pages/BookingPage.tsx b/apps/web/components/booking/pages/BookingPage.tsx index 2c5f905b72..c51a1d7e6c 100644 --- a/apps/web/components/booking/pages/BookingPage.tsx +++ b/apps/web/components/booking/pages/BookingPage.tsx @@ -1,28 +1,20 @@ import { zodResolver } from "@hookform/resolvers/zod"; -import { EventTypeCustomInputType, WorkflowActions } from "@prisma/client"; import { useMutation } from "@tanstack/react-query"; -import { isValidPhoneNumber } from "libphonenumber-js"; import { useSession } from "next-auth/react"; +import dynamic from "next/dynamic"; import Head from "next/head"; import { useRouter } from "next/router"; import { useEffect, useMemo, useReducer, useState } from "react"; -import { useFieldArray, useForm, useWatch } from "react-hook-form"; -import { FormattedNumber, IntlProvider } from "react-intl"; +import { useForm, useFormContext } from "react-hook-form"; import { v4 as uuidv4 } from "uuid"; import { z } from "zod"; import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager"; import type { EventLocationType } from "@calcom/app-store/locations"; -import { - getEventLocationType, - getEventLocationValue, - getHumanReadableLocationValue, - locationKeyToString, -} from "@calcom/app-store/locations"; +import { getEventLocationType, locationKeyToString } from "@calcom/app-store/locations"; import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client"; import { getEventTypeAppData } from "@calcom/app-store/utils"; import type { LocationObject } from "@calcom/core/location"; -import { LocationType } from "@calcom/core/location"; import dayjs from "@calcom/dayjs"; import { useEmbedNonStylesConfig, @@ -30,41 +22,27 @@ import { useIsBackgroundTransparent, useIsEmbed, } from "@calcom/embed-core/embed-iframe"; +import { + getBookingFieldsWithSystemFields, + SystemField, +} from "@calcom/features/bookings/lib/getBookingFields"; +import getBookingResponsesSchema, { + getBookingResponsesPartialSchema, +} from "@calcom/features/bookings/lib/getBookingResponsesSchema"; +import { FormBuilderField } from "@calcom/features/form-builder/FormBuilder"; import CustomBranding from "@calcom/lib/CustomBranding"; import classNames from "@calcom/lib/classNames"; import { APP_NAME } from "@calcom/lib/constants"; -import getPaymentAppData from "@calcom/lib/getPaymentAppData"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import useTheme from "@calcom/lib/hooks/useTheme"; 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, - EmailField, - EmailInput, - Form, - Group, - PhoneInput, - RadioField, - Tooltip, -} from "@calcom/ui"; -import { - FiUserPlus, - FiCalendar, - FiX, - FiInfo, - FiCreditCard, - FiRefreshCw, - FiUser, - FiAlertTriangle, -} from "@calcom/ui/components/icon"; +import { Button, Form, Tooltip } from "@calcom/ui"; +import { FiAlertTriangle, FiCalendar, FiRefreshCw, FiUser } from "@calcom/ui/components/icon"; import { asStringOrNull } from "@lib/asStringOrNull"; import { timeZone } from "@lib/clock"; -import { ensureArray } from "@lib/ensureArray"; import useRouterQuery from "@lib/hooks/useRouterQuery"; import createBooking from "@lib/mutations/bookings/create-booking"; import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking"; @@ -78,23 +56,123 @@ import type { BookPageProps } from "../../../pages/[user]/book"; import type { HashLinkPageProps } from "../../../pages/d/[link]/book"; import type { TeamBookingPageProps } from "../../../pages/team/[slug]/book"; -type BookingPageProps = BookPageProps | TeamBookingPageProps | HashLinkPageProps; +/** These are like 40kb that not every user needs */ +const BookingDescriptionPayment = dynamic( + () => import("@components/booking/BookingDescriptionPayment") +) as unknown as typeof import("@components/booking/BookingDescriptionPayment").default; -type BookingFormValues = { - name: string; - email: string; - notes?: string; - locationType?: EventLocationType["type"]; - guests?: { email: string }[]; - address?: string; - attendeeAddress?: string; - phone?: string; - hostPhoneNumber?: string; // Maybe come up with a better way to name this to distingish between two types of phone numbers - customInputs?: { - [key: string]: string | boolean; - }; - rescheduleReason?: string; - smsReminderNumber?: string; +type BookingPageProps = BookPageProps | TeamBookingPageProps | HashLinkPageProps; +const BookingFields = ({ + fields, + locations, + rescheduleUid, + isDynamicGroupBooking, +}: { + fields: BookingPageProps["eventType"]["bookingFields"]; + locations: LocationObject[]; + rescheduleUid?: string; + isDynamicGroupBooking: boolean; +}) => { + const { t } = useLocale(); + const { watch, setValue } = useFormContext(); + const locationResponse = watch("responses.location"); + + return ( + // TODO: It might make sense to extract this logic into BookingFields config, that would allow to quickly configure system fields and their editability in fresh booking and reschedule booking view +
+ {fields.map((field, index) => { + // During reschedule by default all system fields are readOnly. Make them editable on case by case basis. + // Allowing a system field to be edited might require sending emails to attendees, so we need to be careful + let readOnly = + (field.editable === "system" || field.editable === "system-but-optional") && !!rescheduleUid; + let noLabel = false; + let hidden = !!field.hidden; + if (field.name === SystemField.Enum.rescheduleReason) { + if (!rescheduleUid) { + return null; + } + // rescheduleReason is a reschedule specific field and thus should be editable during reschedule + readOnly = false; + } + + if (field.name === SystemField.Enum.smsReminderNumber) { + // `smsReminderNumber` and location.optionValue when location.value===phone are the same data point. We should solve it in a better way in the Form Builder itself. + // I think we should have a way to connect 2 fields together and have them share the same value in Form Builder + if (locationResponse?.value === "phone") { + setValue(`responses.${SystemField.Enum.smsReminderNumber}`, locationResponse?.optionValue); + // Just don't render the field now, as the value is already connected to attendee phone location + return null; + } + // `smsReminderNumber` can be edited during reschedule even though it's a system field + readOnly = false; + } + + if (field.name === SystemField.Enum.guests) { + // No matter what user configured for Guests field, we don't show it for dynamic group booking as that doesn't support guests + hidden = isDynamicGroupBooking ? true : !!field.hidden; + } + + // We don't show `notes` field during reschedule + if (field.name === SystemField.Enum.notes && !!rescheduleUid) { + return null; + } + + // Dynamically populate location field options + if (field.name === SystemField.Enum.location && field.type === "radioInput") { + if (!field.optionsInputs) { + throw new Error("radioInput must have optionsInputs"); + } + const optionsInputs = field.optionsInputs; + + const options = locations.map((location) => { + const eventLocation = getEventLocationType(location.type); + const locationString = locationKeyToString(location); + + if (typeof locationString !== "string" || !eventLocation) { + // It's possible that location app got uninstalled + return null; + } + const type = eventLocation.type; + const optionInput = optionsInputs[type as keyof typeof optionsInputs]; + if (optionInput) { + optionInput.placeholder = t(eventLocation?.attendeeInputPlaceholder || ""); + } + + return { + label: t(locationString), + value: type, + }; + }); + + field.options = options.filter( + (location): location is NonNullable<(typeof options)[number]> => !!location + ); + // If we have only one option and it has an input, we don't show the field label because Option name acts as label. + // e.g. If it's just Attendee Phone Number option then we don't show `Location` label + if (field.options.length === 1) { + if (field.optionsInputs[field.options[0].value]) { + noLabel = true; + } else { + // If there's only one option and it doesn't have an input, we don't show the field at all because it's visible in the left side bar + hidden = true; + } + } + } + + const label = noLabel ? "" : field.label || t(field.defaultLabel || ""); + const placeholder = field.placeholder || t(field.defaultPlaceholder || ""); + + return ( +
+ ); }; const BookingPage = ({ @@ -124,7 +202,6 @@ const BookingPage = ({ }), {} ); - const paymentAppData = getPaymentAppData(eventType); // Define duration now that we support multiple duration eventTypes let duration = eventType.length; if ( @@ -156,8 +233,8 @@ const BookingPage = ({ createPaymentLink({ paymentUid, date, - name: bookingForm.getValues("name"), - email: bookingForm.getValues("email"), + name: bookingForm.getValues("responses.name"), + email: bookingForm.getValues("responses.email"), absolute: false, }) ); @@ -167,7 +244,7 @@ const BookingPage = ({ pathname: `/booking/${uid}`, query: { isSuccessBookingPage: true, - email: bookingForm.getValues("email"), + email: bookingForm.getValues("responses.email"), eventTypeSlug: eventType.slug, ...(rescheduleUid && booking?.startTime && { formerTime: booking.startTime.toString() }), }, @@ -183,7 +260,7 @@ const BookingPage = ({ pathname: `/booking/${uid}`, query: { allRemainingBookings: true, - email: bookingForm.getValues("email"), + email: bookingForm.getValues("responses.email"), eventTypeSlug: eventType.slug, formerTime: booking?.startTime.toString(), }, @@ -194,55 +271,58 @@ const BookingPage = ({ const rescheduleUid = router.query.rescheduleUid as string; useTheme(profile.theme); const date = asStringOrNull(router.query.date); + const querySchema = getBookingResponsesPartialSchema({ + bookingFields: getBookingFieldsWithSystemFields(eventType), + }); + + const parsedQuery = querySchema.parse({ + ...router.query, + // `guest` because we need to support legacy URL with `guest` query param support + // `guests` because the `name` of the corresponding bookingField is `guests` + guests: router.query.guests || router.query.guest, + }); - const [guestToggle, setGuestToggle] = useState(booking && booking.attendees.length > 1); // it would be nice if Prisma at some point in the future allowed for Json; as of now this is not the case. const locations: LocationObject[] = useMemo( () => (eventType.locations as LocationObject[]) || [], [eventType.locations] ); + const [isClientTimezoneAvailable, setIsClientTimezoneAvailable] = useState(false); useEffect(() => { - if (router.query.guest) { - setGuestToggle(true); - } - }, [router.query.guest]); + // THis is to fix hydration error that comes because of different timezone on server and client + setIsClientTimezoneAvailable(true); + }, []); const loggedInIsOwner = eventType?.users[0]?.id === session?.user?.id; - const guestListEmails = !isDynamicGroupBooking - ? booking?.attendees.slice(1).map((attendee) => { - return { email: attendee.email }; - }) - : []; // There should only exists one default userData variable for primaryAttendee. const defaultUserValues = { - email: rescheduleUid - ? booking?.attendees[0].email - : router.query.email - ? (router.query.email as string) - : "", - name: rescheduleUid ? booking?.attendees[0].name : router.query.name ? (router.query.name as string) : "", + email: rescheduleUid ? booking?.attendees[0].email : parsedQuery["email"], + name: rescheduleUid ? booking?.attendees[0].name : parsedQuery["name"], }; const defaultValues = () => { if (!rescheduleUid) { - return { + const defaults = { + responses: {} as Partial["responses"]>, + }; + + const responses = eventType.bookingFields.reduce((responses, field) => { + return { + ...responses, + [field.name]: parsedQuery[field.name], + }; + }, {}); + defaults.responses = { + ...responses, 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).map((guest) => { - return { email: guest as string }; - }), - customInputs: eventType.customInputs.reduce( - (customInputs, input) => ({ - ...customInputs, - [input.id]: router.query[slugify(input.label)], - }), - {} - ), }; + + return defaults; } + if (!booking || !booking.attendees.length) { return {}; } @@ -251,74 +331,41 @@ const BookingPage = ({ return {}; } - const customInputType = booking.customInputs; - return { - name: defaultUserValues.name, - email: defaultUserValues.email || "", - guests: guestListEmails, - notes: booking.description || "", - rescheduleReason: "", - smsReminderNumber: booking.smsReminderNumber || undefined, - customInputs: eventType.customInputs.reduce( - (customInputs, input) => ({ - ...customInputs, - [input.id]: booking.customInputs - ? booking.customInputs[input.label as keyof typeof customInputType] - : "", - }), - {} - ), + const defaults = { + responses: {} as Partial["responses"]>, }; + + const responses = eventType.bookingFields.reduce((responses, field) => { + return { + ...responses, + [field.name]: booking.responses[field.name], + }; + }, {}); + defaults.responses = { + ...responses, + name: defaultUserValues.name || (!loggedInIsOwner && session?.user?.name) || "", + email: defaultUserValues.email || (!loggedInIsOwner && session?.user?.email) || "", + }; + return defaults; }; const bookingFormSchema = z .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)) - .optional() - .nullable(), - attendeeAddress: z.string().optional().nullable(), - smsReminderNumber: z - .string() - .refine((val) => isValidPhoneNumber(val)) - .optional() - .nullable(), + responses: getBookingResponsesSchema({ + bookingFields: getBookingFieldsWithSystemFields(eventType), + }), }) .passthrough(); + type BookingFormValues = { + locationType?: EventLocationType["type"]; + responses: z.infer["responses"]; + }; + const bookingForm = useForm({ 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, - name: "locationType", - defaultValue: ((): EventLocationType["type"] | undefined => { - if (router.query.location) { - return router.query.location as EventLocationType["type"]; - } - if (locations.length === 1) { - return locations[0]?.type; - } - })(), - }); - - const selectedLocation = getEventLocationType(selectedLocationType); - const AttendeeInput = - selectedLocation?.attendeeInputType === "phone" - ? PhoneInput - : selectedLocation?.attendeeInputType === "attendeeAddress" - ? AddressInput - : null; // Calculate the booking date(s) let recurringStrings: string[] = [], @@ -336,40 +383,6 @@ 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] : "", - })); - - // Checking if custom inputs of type Phone number are valid to display error message on UI - if (eventType.customInputs.length) { - let isErrorFound = false; - eventType.customInputs.forEach((customInput) => { - if (customInput.required && customInput.type === EventTypeCustomInputType.PHONE) { - const input = bookingCustomInputs.find((i) => i.label === customInput.label); - try { - z.string({ - errorMap: () => ({ - message: `Missing ${customInput.type} customInput: '${customInput.label}'`, - }), - }) - .refine((val) => isValidPhoneNumber(val), { - message: "Phone number is invalid", - }) - .parse(input?.value); - } catch (err) { - isErrorFound = true; - bookingForm.setError(`customInputs.${customInput.id}`, { - type: "custom", - message: "Invalid Phone number", - }); - } - } - }); - if (isErrorFound) return; - } - telemetry.event( top !== window ? telemetryEventTypes.embedBookingConfirmed : telemetryEventTypes.bookingConfirmed, { isTeamBooking: document.URL.includes("team/") } @@ -388,45 +401,6 @@ const BookingPage = ({ {} ); - if (eventType.customInputs.length > 0) { - // find all required custom inputs and ensure they are filled out in the booking form - const requiredCustomInputs = eventType.customInputs.filter((input) => input.required); - const missingRequiredCustomInputs = requiredCustomInputs.filter( - (input) => !booking?.customInputs?.[input.id] - ); - if (missingRequiredCustomInputs.length > 0) { - missingRequiredCustomInputs.forEach((input) => { - bookingForm.setError(`customInputs.${input.id}`, { - type: "required", - }); - }); - return; - } - } - - // 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(); @@ -443,21 +417,10 @@ const BookingPage = ({ language: i18n.language, rescheduleUid, user: router.query.user, - location: getEventLocationValue(locations, { - type: booking.locationType ? booking.locationType : selectedLocationType || "", - phone: booking.phone, - attendeeAddress: booking.attendeeAddress, - }), metadata, - customInputs: bookingCustomInputs, hasHashedBookingLink, hashedLink, - smsReminderNumber: - selectedLocationType === LocationType.Phone - ? booking.phone - : booking.smsReminderNumber || undefined, ethSignature: gateState.rainbowToken, - guests: booking.guests?.map((guest) => guest.email), })); recurringMutation.mutate(recurringBookings); } else { @@ -472,48 +435,14 @@ const BookingPage = ({ rescheduleUid, bookingUid: router.query.bookingUid as string, user: router.query.user, - location: getEventLocationValue(locations, { - type: (booking.locationType ? booking.locationType : selectedLocationType) || "", - phone: booking.phone, - attendeeAddress: booking.attendeeAddress, - }), metadata, - customInputs: bookingCustomInputs, hasHashedBookingLink, hashedLink, - smsReminderNumber: - selectedLocationType === LocationType.Phone - ? booking.phone - : booking.smsReminderNumber || undefined, ethSignature: gateState.rainbowToken, - guests: booking.guests?.map((guest) => guest.email), }); } }; - // Should be disabled when rescheduleUid is present and data was found in defaultUserValues name/email fields. - const disableInput = !!rescheduleUid && !!defaultUserValues.email && !!defaultUserValues.name; - const disableLocations = !!rescheduleUid; - const disabledExceptForOwner = disableInput && !loggedInIsOwner; - const inputClassName = - "dark:placeholder:text-darkgray-600 focus:border-brand dark:border-darkgray-300 dark:text-darkgray-900 block w-full rounded-md border-gray-300 text-sm focus:ring-black disabled:bg-gray-200 disabled:hover:cursor-not-allowed dark:bg-transparent dark:selection:bg-green-500 disabled:dark:text-gray-500"; - - let isSmsReminderNumberNeeded = false; - let isSmsReminderNumberRequired = false; - - if (eventType.workflows.length > 0) { - eventType.workflows.forEach((workflowReference) => { - if (workflowReference.workflow.steps.length > 0) { - workflowReference.workflow.steps.forEach((step) => { - if (step.action === WorkflowActions.SMS_ATTENDEE) { - isSmsReminderNumberNeeded = true; - isSmsReminderNumberRequired = step.numberRequired || false; - return; - } - }); - } - }); - } const showEventTypeDetails = (isEmbed && !embedUiConfig.hideEventTypeDetails) || !isEmbed; const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {}; @@ -560,18 +489,7 @@ const BookingPage = ({ {showEventTypeDetails && (
- {paymentAppData.price > 0 && ( -

- - - - -

- )} + {!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
@@ -587,8 +505,11 @@ const BookingPage = ({
- {(rescheduleUid || !eventType.recurringEvent?.freq) && `${parseDate(date, i18n)}`} - {!rescheduleUid && + {isClientTimezoneAvailable && + (rescheduleUid || !eventType.recurringEvent?.freq) && + `${parseDate(date, i18n)}`} + {isClientTimezoneAvailable && + !rescheduleUid && eventType.recurringEvent?.freq && recurringStrings.slice(0, 5).map((timeFormatted, key) => { return

{timeFormatted}

; @@ -614,7 +535,9 @@ const BookingPage = ({

- {typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)} + {isClientTimezoneAvailable && + typeof booking.startTime === "string" && + parseDate(dayjs(booking.startTime), i18n)}

)} @@ -648,392 +571,23 @@ const BookingPage = ({
)}
-
-
- -
- -
-
-
- -
- - {bookingForm.formState.errors.email && ( -
- -

{t("email_validation_error")}

-
- )} -
-
- <> - {rescheduleUid ? ( -
- - {t("location")} - -

- {getHumanReadableLocationValue(booking?.location, t)} -

-
- ) : ( - locations.length > 1 && ( -
- - {t("location")} - - {locations.map((location, i) => { - const locationString = locationKeyToString(location); - if (!selectedLocationType) { - bookingForm.setValue("locationType", locations[0].type); - } - if (typeof locationString !== "string") { - // It's possible that location app got uninstalled - return null; - } - return ( - - ); - })} -
- ) - )} - - {/* TODO: Change name and id ="phone" to something generic */} - {AttendeeInput && !disableInput && ( -
- -
- - control={bookingForm.control} - bookingForm={bookingForm} - name={ - selectedLocationType === LocationType.Phone - ? "phone" - : selectedLocationType === LocationType.AttendeeInPerson - ? "attendeeAddress" - : "" - } - placeholder={t(selectedLocation?.attendeeInputPlaceholder || "")} - id={ - selectedLocationType === LocationType.Phone - ? "phone" - : selectedLocationType === LocationType.AttendeeInPerson - ? "attendeeAddress" - : "" - } - required - /> -
- {bookingForm.formState.errors.phone && ( -
- -

{t("invalid_number")}

-
- )} -
- )} - {eventType.customInputs - .sort((a, b) => a.id - b.id) - .map((input) => ( -
- {input.type !== EventTypeCustomInputType.BOOL && ( - - )} - {input.type === EventTypeCustomInputType.TEXTLONG && ( -