Closed anikdhabal closed 2 weeks 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:
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
];
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);
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
);
};
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 };
};
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}
/>
);
}
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
To continue the conversation, mention @dosu.
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
Evidence:-
CAL-3916