calcom / cal.com

Scheduling infrastructure for absolutely everyone.
https://cal.com
Other
30.74k stars 7.39k forks source link

[CAL-3916] bug: issue in the event location field #15225

Closed anikdhabal closed 2 weeks ago

anikdhabal commented 3 months ago

Found a bug? Please fill out the sections below. 👍

Issue Summary

After changing to any other conference app location in managedEvent, the same link is present in the input field as in the previous app.

Steps to Reproduce

  1. Select a conference app for the event location that has a static link and save it after setting the link.
  2. Change to any other conference app with a static link type.
  3. The previous link is still present in the input field.

Evidence:-

Screenshot 2024-05-29 003719

CAL-3916

dosubot[bot] commented 3 months ago

To address the issues with the event location field not accepting input correctly, not displaying the entered location, and exhibiting unexpected behavior such as clearing itself or not saving the input, you should ensure that the defaultValueVariable and variable properties are correctly set for each location type. These properties are crucial for mapping the input fields to the correct variables.

Here is a checklist and some code snippets to help you debug and fix the issues:

  1. Check defaultValueVariable and variable Properties: Ensure that each location type has the defaultValueVariable and variable properties correctly set. These properties are used to store and retrieve the input values.

    const defaultLocations: DefaultEventLocationType[] = [
     {
       default: true,
       type: DefaultEventLocationTypeEnum.AttendeeInPerson,
       label: "in_person_attendee_address",
       variable: "address",
       organizerInputType: null,
       messageForOrganizer: "Cal will ask your invitee to enter an address before scheduling.",
       attendeeInputType: "attendeeAddress",
       attendeeInputPlaceholder: "enter_address",
       defaultValueVariable: "attendeeAddress",
       iconUrl: "/map-pin-dark.svg",
       category: "in person",
     },
     // ... other location types
    ];
  2. Ensure Correct Mapping in getEventLocationType: Verify that the getEventLocationType function correctly maps the location type to the corresponding location object.

    export const getEventLocationType = (locationType: string | undefined | null) =>
     locationsTypes.find((l) => l.type === locationType);
  3. Handle Input Saving and Display: Ensure that the input values are correctly saved and displayed by using the defaultValueVariable to store and retrieve the values.

    export const getEventLocationValue = (eventLocations: LocationObject[], bookingLocation: LocationObject) => {
     const eventLocationType = getEventLocationType(bookingLocation?.type);
     if (!eventLocationType) {
       return "";
     }
     const defaultValueVariable = eventLocationType.defaultValueVariable;
     if (!defaultValueVariable) {
       console.error(`${defaultValueVariable} not set for ${bookingLocation.type}`);
       return "";
     }
     const eventLocation = getEventLocationWithType(eventLocations, bookingLocation?.type);
     if (!eventLocation) {
       console.error(`Could not find eventLocation for ${bookingLocation}`);
       return "";
     }
    
     return (
       bookingLocation[defaultValueVariable] || eventLocation[defaultValueVariable] || eventLocationType.type
     );
    };
  4. Debug Unexpected Behavior: If the field is clearing itself or not saving the input, check for any conditions that might reset the field or prevent the value from being saved.

    export const getLocationValueForDB = (
     bookingLocationTypeOrValue: EventLocationType["type"],
     eventLocations: LocationObject[]
    ) => {
     let bookingLocation = bookingLocationTypeOrValue;
     let conferenceCredentialId = undefined;
     eventLocations.forEach((location) => {
       if (location.type === bookingLocationTypeOrValue) {
         const eventLocationType = getEventLocationType(bookingLocationTypeOrValue);
         conferenceCredentialId = location.credentialId;
         if (!eventLocationType) {
           return;
         }
         if (!eventLocationType.default && eventLocationType.linkType === "dynamic") {
           return;
         }
    
         bookingLocation = location[eventLocationType.defaultValueVariable] || bookingLocation;
       }
     });
    
     if (bookingLocation.trim().length === 0) {
       bookingLocation = DailyLocationType;
     }
    
     return { bookingLocation, conferenceCredentialId };
    };
  5. Ensure Proper Integration with UI Components: Verify that the LocationSelect component is correctly integrated and configured to handle the location input. This includes ensuring that the Select component from react-select is properly set up to display and manage the location options.

    import type { GroupBase, Props, SingleValue } from "react-select";
    import { components } from "react-select";
    
    import type { EventLocationType } from "@calcom/app-store/locations";
    import { classNames } from "@calcom/lib";
    import invertLogoOnDark from "@calcom/lib/invertLogoOnDark";
    import { Select } from "@calcom/ui";
    
    export type LocationOption = {
     label: string;
     value: EventLocationType["type"];
     icon?: string;
     disabled?: boolean;
     address?: string;
     credentialId?: number;
     teamName?: string;
    };
    
    export type SingleValueLocationOption = SingleValue<LocationOption>;
    
    export type GroupOptionType = GroupBase<LocationOption>;
    
    const OptionWithIcon = ({ icon, label }: { icon?: string; label: string }) => {
     return (
       <div className="flex items-center gap-3">
         {icon && <img src={icon} alt="cover" className={classNames("h-3.5 w-3.5", invertLogoOnDark(icon))} />}
         <span className={classNames("text-sm font-medium")}>{label}</span>
       </div>
     );
    };
    
    export default function LocationSelect(props: Props<LocationOption, false, GroupOptionType>) {
     return (
       <Select<LocationOption>
         name="location"
         id="location-select"
         data-testid="location-select"
         components={{
           Option: (props) => {
             return (
               <components.Option {...props}>
                 <OptionWithIcon icon={props.data.icon} label={props.data.label} />
               </components.Option>
             );
           },
           SingleValue: (props) => (
             <components.SingleValue {...props}>
               <OptionWithIcon icon={props.data.icon} label={props.data.label} />
             </components.SingleValue>
           ),
         }}
         formatOptionLabel={(e) => (
           <div className="flex items-center gap-3">
             {e.icon && (
               <img
                 src={e.icon}
                 alt="app-icon"
                 className={classNames(e.icon.includes("-dark") && "dark:invert", "h-5 w-5")}
               />
             )}
             <span>{e.label}</span>
           </div>
         )}
         formatGroupLabel={(e) => <p className="text-default text-xs font-medium">{e.label}</p>}
         {...props}
       />
     );
    }
  6. Ensure Proper Integration with the EventSetupTab Component: Verify that the EventSetupTab component is correctly set up to handle the location input and display the entered location.

    
    import { useAutoAnimate } from "@formkit/auto-animate/react";
    import { ErrorMessage } from "@hookform/error-message";
    import { Trans } from "next-i18next";
    import Link from "next/link";
    import type { EventTypeSetupProps } from "pages/event-types/[type]";
    import { useEffect, useState } from "react";
    import { Controller, useFormContext, useFieldArray } from "react-hook-form";
    import type { MultiValue } from "react-select";
    
    import type { EventLocationType } from "@calcom/app-store/locations";
    import { getEventLocationType, MeetLocationType } from "@calcom/app-store/locations";
    import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
    import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
    import type { FormValues } from "@calcom/features/eventtypes/lib/types";
    import { WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
    import { useLocale } from "@calcom/lib/hooks/useLocale";
    import { md } from "@calcom/lib/markdownIt";
    import { slugify } from "@calcom/lib/slugify";
    import turndown from "@calcom/lib/turndownService";
    import {
     Label,
     Select,
     SettingsToggle,
     Skeleton,
     TextField,
     Icon,
     Editor,
     SkeletonContainer,
     SkeletonText,
     Input,
     PhoneInput,
     Button,
     showToast,
    } from "@calcom/ui";
    
    import CheckboxField from "@components/ui/form/CheckboxField";
    import type { SingleValueLocationOption } from "@components/ui/form/LocationSelect";
    import LocationSelect from "@components/ui/form/LocationSelect";
    
    const getLocationFromType = (
     type: EventLocationType["type"],
     locationOptions: Pick<EventTypeSetupProps, "locationOptions">["locationOptions"]
    ) => {
     for (const locationOption of locationOptions) {
       const option = locationOption.options.find((option) => option.value === type);
       if (option) {
         return option;
       }
     }
    };
    const getLocationInfo = ({
     eventType,
     locationOptions,
    }: Pick<EventTypeSetupProps, "eventType" | "locationOptions">) => {
     const locationAvailable =
       eventType.locations &&
       eventType.locations.length > 0 &&
       locationOptions.some((op) => op.options.find((opt) => opt.value === eventType.locations[0].type));
     const locationDetails = eventType.locations &&
       eventType.locations.length > 0 &&
       !locationAvailable && {
         slug: eventType.locations[0].type.replace("integrations:", "").replace(":", "-").replace("_video", ""),
         name: eventType.locations[0].type
           .replace("integrations:", "")
           .replace(":", " ")
           .replace("_video", "")
           .split(" ")
           .map((word) => word[0].toUpperCase() + word.slice(1))
           .join(" "),
       };
     return { locationAvailable, locationDetails };
    };
    
    const DescriptionEditor = ({ isEditable }: { isEditable: boolean }) => {
     const formMethods = useFormContext<FormValues>();
     const [mounted, setIsMounted] = useState(false);
     const { t } = useLocale();
     const [firstRender, setFirstRender] = useState(true);
     useEffect(() => {
       setIsMounted(true);
     }, []);
    
     return mounted ? (
       <Editor
         getText={() => md.render(formMethods.getValues("description") || "")}
         setText={(value: string) => formMethods.setValue("description", turndown(value), { shouldDirty: true })}
         excludedToolbarItems={["blockType"]}
         placeholder={t("quick_video_meeting")}
         editable={isEditable}
         firstRender={firstRender}
         setFirstRender={setFirstRender}
       />
     ) : (
       <SkeletonContainer>
         <SkeletonText className="block h-24 w-full" />
       </SkeletonContainer>
     );
    };
    export const EventSetupTab = (
     props: Pick<
       EventTypeSetupProps,
       "eventType" | "locationOptions" | "team" | "teamMembers" | "destinationCalendar"
     >
    ) => {
     const { t } = useLocale();
     const formMethods = useFormContext<FormValues>();
     const { eventType, team, destinationCalendar } = props;
     const [multipleDuration, setMultipleDuration] = useState(
       formMethods.getValues("metadata")?.multipleDuration
     );
     const orgBranding = useOrgBranding();
     const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
    
     const locationOptions = props.locationOptions.map((locationOption) => {
       const options = locationOption.options.filter((option) => {
         // Skip "Organizer's Default App" for non-team members
         return !team ? option.label !== t("organizer_default_conferencing_app") : true;
       });
    
       return {
         ...locationOption,
         options,
       };
     });
    
     const multipleDurationOptions = [
       5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 150, 180, 240, 300, 360, 420, 480,
     ].map((mins) => ({
       value: mins,
       label: t("multiple_duration_mins", { count: mins }),
     }));
    
     const [selectedMultipleDuration, setSelectedMultipleDuration] = useState<
       MultiValue<{
         value: number;
         label: string;
       }>
     >(multipleDurationOptions.filter((mdOpt) => multipleDuration?.includes(mdOpt.value)));
     const [defaultDuration, setDefaultDuration] = useState(
       selectedMultipleDuration.find((opt) => opt.value === formMethods.getValues("length")) ?? null
     );
    
     const { isChildrenManagedEventType, isManagedEventType, shouldLockIndicator, shouldLockDisableProps } =
       useLockedFieldsManager({ eventType, translate: t, formMethods });
     const Locations = () => {
       const { t } = useLocale();
       const {
         fields: locationFields,
         append,
         remove,
         update: updateLocationField,
       } = useFieldArray({
         control: formMethods.control,
         name: "locations",
       });
    
       const [animationRef] = useAutoAnimate<HTMLUListElement>();
    const validLocations = formMethods.getValues("locations").filter((location) => {
         const eventLocation = getEventLocationType(location.type);
         if (!eventLocation) {
           // It's possible that the location app in use got uninstalled.
           return false;
         }
         return true;
       });
    
       const defaultValue = isManagedEventType
         ? locationOptions.find((op) => op.label === t("default"))?.options[0]
         : undefined;
    
       const { locationDetails, locationAvailable } = getLocationInfo(props);
    
       const LocationInput = (props: {
         eventLocationType: EventLocationType;
         defaultValue?: string;
         index: number;
       }) => {
         const { eventLocationType, index, ...remainingProps } = props;
         if (eventLocationType?.organizerInputType === "text") {
           const { defaultValue, ...rest } = remainingProps;
    
           return (
             <Controller
               name={`locations.${index}.${eventLocationType.defaultValueVariable}`}
               defaultValue={defaultValue}
               render={({ field: { onChange, value } }) => {
                 return (
                   <Input
                     name={`locations[${index}].${eventLocationType.defaultValueVariable}`}
                     placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
                     type="text"
                     required
                     onChange={onChange}
                     value={value}
                     {...(shouldLockDisableProps("locations").disabled ? { disabled: true } : {})}
                     className="my-0"
                     {...rest}
                   />
                 );
               }}
             />
           );
         } else if (eventLocationType?.organizerInputType === "phone") {
           const { defaultValue, ...rest } = remainingProps;
    return (
             <Controller
               name={`locations.${index}.${eventLocationType.defaultValueVariable}`}
               defaultValue={defaultValue}
               render={({ field: { onChange, value } }) => {
                 return (
                   <PhoneInput
                     required
                     disabled={shouldLockDisableProps("locations").disabled}
                     placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
                     name={`locations[${index}].${eventLocationType.defaultValueVariable}`}
                     value={value}
                     onChange={onChange}
                     {...rest}
                   />
                 );
               }}
             />
           );
         }
         return null;
       };
    
       const [showEmptyLocationSelect, setShowEmptyLocationSelect] = useState(false);
       const defaultInitialLocation = defaultValue || null;
       const [selectedNewOption, setSelectedNewOption] = useState<SingleValueLocationOption | null>(
         defaultInitialLocation
       );
    
       return (
         <div className="w-full">
           <ul ref={animationRef} className="space-y-2">
             {locationFields.map((field, index) => {
               const eventLocationType = getEventLocationType(field.type);
               const defaultLocation = field;
    
               const option = getLocationFromType(field.type, locationOptions);
    return (
                 <li key={field.id}>
                   <div className="flex w-full items-center">
                     <LocationSelect
                       name={`locations[${index}].type`}
                       placeholder={t("select")}
                       options={locationOptions}
                       isDisabled={shouldLockDisableProps("locations").disabled}
                       defaultValue={option}
                       isSearchable={false}
                       className="block min-w-0 flex-1 rounded-sm text-sm"
                       menuPlacement="auto"
                       onChange={(e: SingleValueLocationOption) => {
                         if (e?.value) {
                           const newLocationType = e.value;
                           const eventLocationType = getEventLocationType(newLocationType);
                           if (!eventLocationType) {
                             return;
                           }
                           const canAddLocation =
                             eventLocationType.organizerInputType ||
                             !validLocations.find((location) => location.type === newLocationType);
    if (canAddLocation) {
                             updateLocationField(index, {
                               type: newLocationType,
                               ...(e.credentialId && {
                                 credentialId: e.credentialId,
                                 teamName: e.teamName,
                               }),
                             });
                           } else {
                             updateLocationField(index, {
                               type: field.type,
                               ...(field.credentialId && {
                                 credentialId: field.credentialId,
                                 teamName: field.teamName,
                               }),
                             });
                             showToast(t("location_already_exists"), "warning");
                           }
                           // Whenever location changes, we need to reset the locations item in booking questions list else it overflows
                           // previously added values resulting in wrong behaviour
                           const existingBookingFields = formMethods.getValues("bookingFields");
                           const findLocation = existingBookingFields.findIndex(
                             (field) => field.name === "location"
                           );
                           if (findLocation >= 0) {
                             existingBookingFields[findLocation] = {
                               ...existingBookingFields[findLocation],
                               type: "radioInput",
                               label: "",
                               placeholder: "",
                             };
                             formMethods.setValue("bookingFields", existingBookingFields, {
                               shouldDirty: true,
                             });
                           }
                         }
                       }}
                     />
                     {!(shouldLockDisableProps("locations").disabled && isChildrenManagedEventType) && (
    <button
                         data-testid={`delete-locations.${index}.type`}
                         className="min-h-9 block h-9 px-2"
                         type="button"
                         onClick={() => remove(index)}
                         aria-label={t("remove")}>
                         <div className="h-4 w-4">
                           <Icon name="x" className="border-l-1 hover:text-emphasis text-subtle h-4 w-4" />
                         </div>
                       </button>
                     )}
                   </div>
    {eventLocationType?.organizerInputType && (
                     <div className="mt-2 space-y-2">
                       <div className="w-full">
                         <div className="flex gap-2">
                           <div class
#### Details **About Dosu** This response is meant to be useful and save you time. It is **not meant to be a precise solution**, but rather a starting point for your own research. Help me be more useful! Please leave a 👍 if this is helpful and 👎 if it is irrelevant.

To continue the conversation, mention @dosu.