import { ErrorMessage } from "@hookform/error-message"; import { zodResolver } from "@hookform/resolvers/zod"; import { isValidPhoneNumber } from "libphonenumber-js"; import { useEffect } from "react"; import { Controller, useForm, useWatch } from "react-hook-form"; import { components } from "react-select"; import { z } from "zod"; import { EventLocationType, getEventLocationType, getHumanReadableLocationValue, getMessageForOrganizer, LocationObject, LocationType, } from "@calcom/app-store/locations"; import { classNames } from "@calcom/lib"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { RouterOutputs, trpc } from "@calcom/trpc/react"; import { Button, Dialog, DialogClose, DialogContent, DialogFooter, Form, Icon, PhoneInput } from "@calcom/ui"; import { QueryCell } from "@lib/QueryCell"; import CheckboxField from "@components/ui/form/CheckboxField"; import Select from "@components/ui/form/Select"; type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number]; type OptionTypeBase = { label: string; value: EventLocationType["type"]; disabled?: boolean; }; interface ISetLocationDialog { saveLocation: (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => void; selection?: OptionTypeBase; booking?: BookingItem; defaultValues?: LocationObject[]; setShowLocationModal: React.Dispatch>; isOpenDialog: boolean; setSelectedLocation?: (param: OptionTypeBase | undefined) => void; setEditingLocationType?: (param: string) => void; } const LocationInput = (props: { eventLocationType: EventLocationType; locationFormMethods: ReturnType; id: string; required: boolean; placeholder: string; className?: string; defaultValue?: string; }): JSX.Element | null => { const { eventLocationType, locationFormMethods, ...remainingProps } = props; if (eventLocationType?.organizerInputType === "text") { return ( ); } else if (eventLocationType?.organizerInputType === "phone") { return ( ); } return null; }; export const EditLocationDialog = (props: ISetLocationDialog) => { const { saveLocation, selection, booking, setShowLocationModal, isOpenDialog, defaultValues, setSelectedLocation, setEditingLocationType, } = props; const { t } = useLocale(); const locationsQuery = trpc.viewer.locationOptions.useQuery(); useEffect(() => { if (selection) { locationFormMethods.setValue("locationType", selection?.value); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [selection]); const locationFormSchema = z.object({ locationType: z.string(), phone: z.string().optional().nullable(), locationAddress: z.string().optional(), locationLink: z .string() .optional() .superRefine((val, ctx) => { if ( eventLocationType && !eventLocationType.default && eventLocationType.linkType === "static" && eventLocationType.urlRegExp ) { const valid = z.string().regex(new RegExp(eventLocationType.urlRegExp)).safeParse(val).success; if (!valid) { const sampleUrl = eventLocationType.organizerInputPlaceholder; ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Invalid URL for ${eventLocationType.label}. ${ sampleUrl ? "Sample URL: " + sampleUrl : "" }`, }); } return; } const valid = z.string().url().optional().safeParse(val).success; if (!valid) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Invalid URL`, }); } return; }), displayLocationPublicly: z.boolean().optional(), locationPhoneNumber: z .string() .nullable() .refine((val) => { if (val === null) return false; return isValidPhoneNumber(val); }) .optional(), }); const locationFormMethods = useForm({ mode: "onSubmit", resolver: zodResolver(locationFormSchema), }); const selectedLocation = useWatch({ control: locationFormMethods.control, name: "locationType", }); const eventLocationType = getEventLocationType(selectedLocation); const defaultLocation = defaultValues?.find( (location: { type: EventLocationType["type"] }) => location.type === eventLocationType?.type ); const LocationOptions = (() => { if (eventLocationType && eventLocationType.organizerInputType && LocationInput) { if (!eventLocationType.variable) { console.error("eventLocationType.variable can't be undefined"); return null; } return (
{!booking && (
( locationFormMethods.setValue("displayLocationPublicly", e.target.checked) } informationIconText={t("display_location_info_badge")} /> )} />
)}
); } else { return

{getMessageForOrganizer(selectedLocation, t)}

; } })(); return (
{!booking && (

{t("this_input_will_shown_booking_this_event")}

)}
{booking && ( <>

{t("current_location")}:

{getHumanReadableLocationValue(booking.location, t)}

)}
{ const { locationType: newLocation, displayLocationPublicly } = values; let details = {}; if (newLocation === LocationType.InPerson) { details = { address: values.locationAddress, }; } const eventLocationType = getEventLocationType(newLocation); // TODO: There can be a property that tells if it is to be saved in `link` if ( newLocation === LocationType.Link || (!eventLocationType?.default && eventLocationType?.linkType === "static") ) { details = { link: values.locationLink }; } if (newLocation === LocationType.UserPhone) { details = { hostPhoneNumber: values.locationPhoneNumber }; } if (eventLocationType?.organizerInputType) { details = { ...details, displayLocationPublicly, }; } saveLocation(newLocation, details); setShowLocationModal(false); setSelectedLocation?.(undefined); locationFormMethods.unregister([ "locationType", "locationLink", "locationAddress", "locationPhoneNumber", ]); }}> { if (!locationOptions.length) return null; if (booking) { locationOptions.forEach((location) => { if (location.label === "phone") { location.options.filter((l) => l.value !== "phone"); } else if (location.label === "in person") { location.options.filter((l) => l.value !== "attendeeInPerson"); } }); } return ( ( maxMenuHeight={150} name="location" defaultValue={selection} options={locationOptions} components={{ Option: (props) => (
{props.data.icon && ( cover )} {props.data.label}
), }} formatOptionLabel={(e) => (
{e.icon && app-icon} {e.label}
)} formatGroupLabel={(e) => (

{e.label}

)} isSearchable className="my-4 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 text-sm" onChange={(val) => { if (val) { locationFormMethods.setValue("locationType", val.value); locationFormMethods.unregister([ "locationLink", "locationAddress", "locationPhoneNumber", ]); locationFormMethods.clearErrors([ "locationLink", "locationPhoneNumber", "locationAddress", ]); setSelectedLocation?.(val); } }} /> )} /> ); }} /> {selectedLocation && LocationOptions}
); };