feat: Add custom label for organizer default app and link meeting (#23114)

* Create `CalVideoSettings` type

* Create `LocationOptionContainer`

* Extract cal video settings to `CalVideoSettings`

* Refactor `<Locations />` component to use `<CalVideoSettings />`

* Unify naming

* Move location folder back under components

* Type fixes

* Extract types

* Add custom label prop to default app types

* Extract `LocationInput` component

* Create `DefaultLocationSettings` component

* Move `locations` folder

* Add custom label field for link and organizer default locations

* Type fixes

* Save the custom label in db

* Include location custom label when querying db

* Display custom label on booking page

* Add translations

* Type fixes

* Change `customLabel` to `supportsCustomLabel`

* Remove unused code

* Show the customLabel on booking page

* Fix hover tooltip text as well

* Fix displaying exact value on booking page

* Add missing translation

* Remove default label of "Value"

* Replace nullish collescing with OR

* Replace nullish collescing with OR

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
This commit is contained in:
Joe Au-Yeung
2025-08-22 16:34:14 +00:00
committed by GitHub
co-authored by Hariom Balhara Udit Takkar
parent 7ae0d6b7cc
commit 2d90d11e9e
12 changed files with 229 additions and 96 deletions
@@ -3322,7 +3322,7 @@
"pbac_desc_delete_roles": "Delete roles",
"pbac_desc_manage_roles": "All actions on roles across organization teams",
"pbac_desc_create_workflows": "Create and set up new workflows",
"pbac_desc_view_workflows": "View existing workflows and their configurations",
"pbac_desc_view_workflows": "View existing workflows and their configurations",
"pbac_desc_update_workflows": "Edit and modify workflow settings",
"pbac_desc_delete_workflows": "Remove workflows from the system",
"pbac_desc_manage_workflows": "Full management access to all workflows",
@@ -3472,6 +3472,8 @@
"webhook_metadata": "Metadata",
"stats": "Stats",
"booking_status": "Booking status",
"location_custom_label_input_label": "Custom label on booking page",
"meeting_link": "Meeting link",
"my_bookings": "My Bookings",
"phone": "Phone",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
+9 -2
View File
@@ -18,6 +18,7 @@ export type DefaultEventLocationType = {
messageForOrganizer: string;
category: "in person" | "conferencing" | "other" | "phone";
linkType: "static";
supportsCustomLabel?: boolean;
iconUrl: string;
urlRegExp?: string;
@@ -42,6 +43,7 @@ export type DefaultEventLocationType = {
| {
organizerInputType: "phone" | "text" | null;
organizerInputPlaceholder?: string | null;
organizerInputLabel?: string | null;
attendeeInputType?: null;
attendeeInputPlaceholder?: null;
}
@@ -56,7 +58,7 @@ export type DefaultEventLocationType = {
export type EventLocationTypeFromApp = Ensure<
EventLocationTypeFromAppMeta,
"defaultValueVariable" | "variable"
>;
> & { supportsCustomLabel?: boolean; organizerInputLabel?: string };
export type EventLocationType = DefaultEventLocationType | EventLocationTypeFromApp;
@@ -147,18 +149,21 @@ export const defaultLocations: DefaultEventLocationType[] = [
category: "conferencing",
messageForOrganizer: "",
linkType: "static",
supportsCustomLabel: true,
},
{
default: true,
type: DefaultEventLocationTypeEnum.Link,
label: "link_meeting",
organizerInputType: "text",
organizerInputLabel: "meeting_link",
variable: "locationLink",
messageForOrganizer: "Provide a Meeting Link",
defaultValueVariable: "link",
iconUrl: "/link.svg",
category: "other",
linkType: "static",
supportsCustomLabel: true,
},
{
default: true,
@@ -182,6 +187,7 @@ export const defaultLocations: DefaultEventLocationType[] = [
label: "organizer_phone_number",
messageForOrganizer: "Provide your phone number",
organizerInputType: "phone",
organizerInputLabel: "phone_number",
variable: "locationPhoneNumber",
defaultValueVariable: "hostPhoneNumber",
iconUrl: "/phone.svg",
@@ -206,6 +212,7 @@ export type LocationObject = {
address?: string;
displayLocationPublicly?: boolean;
credentialId?: number;
customLabel?: string;
} & Partial<
Record<
"address" | "attendeeAddress" | "link" | "hostPhoneNumber" | "hostDefault" | "phone" | "somewhereElse",
@@ -286,7 +293,7 @@ export const guessEventLocationType = (locationTypeOrValue: string | undefined |
export const LocationType = { ...DefaultEventLocationTypeEnum, ...AppStoreLocationType };
type PrivacyFilteredLocationObject = Optional<LocationObject, "address" | "link">;
type PrivacyFilteredLocationObject = Optional<LocationObject, "address" | "link" | "customLabel">;
export const privacyFilteredLocations = (locations: LocationObject[]): PrivacyFilteredLocationObject[] => {
const locationsAfterPrivacyFilter = locations.map((location) => {
+3
View File
@@ -26,6 +26,7 @@ export async function getLocationGroupedOptions(
icon?: string;
slug?: string;
credentialId?: number;
supportsCustomLabel?: boolean;
}[]
> = {};
@@ -145,6 +146,7 @@ export async function getLocationGroupedOptions(
label: l.label,
value: l.type,
icon: l.iconUrl,
supportsCustomLabel: l.supportsCustomLabel,
},
];
} else {
@@ -153,6 +155,7 @@ export async function getLocationGroupedOptions(
label: l.label,
value: l.type,
icon: l.iconUrl,
supportsCustomLabel: l.supportsCustomLabel,
},
];
}
@@ -7,9 +7,9 @@ import { getEventLocationType, getTranslatedLocation } from "@calcom/app-store/l
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import invertLogoOnDark from "@calcom/lib/invertLogoOnDark";
import classNames from "@calcom/ui/classNames";
import { Icon } from "@calcom/ui/components/icon";
import { Tooltip } from "@calcom/ui/components/tooltip";
import classNames from "@calcom/ui/classNames";
const excludeNullValues = (value: unknown) => !!value;
@@ -47,7 +47,8 @@ function RenderLocationTooltip({ locations }: { locations: LocationObject[] }) {
if (!eventLocationType) {
return null;
}
const translatedLocation = getTranslatedLocation(location, eventLocationType, t);
const translatedLocation =
location.customLabel || getTranslatedLocation(location, eventLocationType, t);
return (
<div key={`${location.type}-${index}`} className="font-sm flex flex-row items-center">
<RenderIcon eventLocationType={eventLocationType} isTooltip />
@@ -74,11 +75,8 @@ export function AvailableEventLocations({ locations }: { locations: LocationObje
// It's possible that the location app got uninstalled
return null;
}
if (eventLocationType.variable === "hostDefault") {
return null;
}
const translatedLocation = getTranslatedLocation(location, eventLocationType, t);
const locationName = location?.customLabel || getTranslatedLocation(location, eventLocationType, t);
return (
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
@@ -87,8 +85,8 @@ export function AvailableEventLocations({ locations }: { locations: LocationObje
) : (
<RenderIcon eventLocationType={eventLocationType} isTooltip={false} />
)}
<Tooltip content={translatedLocation}>
<p className="line-clamp-1">{translatedLocation}</p>
<Tooltip content={locationName}>
<p className="line-clamp-1">{locationName}</p>
</Tooltip>
</div>
);
@@ -19,7 +19,7 @@ export default function getLocationsOptionsForSelect(
return null;
}
const type = eventLocation.type;
const translatedLocation = getTranslatedLocation(location, eventLocation, t);
const translatedLocation = location.customLabel || getTranslatedLocation(location, eventLocation, t);
return {
// XYZ: is considered a namespace in i18next https://www.i18next.com/principles/namespaces and thus it gets cleaned up.
@@ -0,0 +1,95 @@
import { ErrorMessage } from "@hookform/error-message";
import { useFieldArray, useFormContext } from "react-hook-form";
import { getEventLocationType } from "@calcom/app-store/locations";
import type { LocationFormValues, FormValues } from "@calcom/features/eventtypes/lib/types";
import CheckboxField from "@calcom/features/form/components/CheckboxField";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import classNames from "@calcom/ui/classNames";
import { TextField } from "@calcom/ui/components/form";
import LocationInput from "./LocationInput";
import LocationOptionContainer from "./LocationSettingsContainer";
import type { LocationCustomClassNames } from "./types";
const DefaultLocationSettings = ({
field,
index,
disableLocationProp,
customClassNames,
}: {
field: LocationFormValues["locations"][number];
index: number;
disableLocationProp?: boolean;
customClassNames?: LocationCustomClassNames;
}) => {
const { t } = useLocale();
const { getValues, control, formState } = useFormContext<FormValues>();
const { update: updateLocationField } = useFieldArray({
control,
name: "locations",
});
const defaultLocation = field;
const eventLocationType = getEventLocationType(field.type);
if (!eventLocationType) return null;
return (
<LocationOptionContainer>
<LocationInput
label={"organizerInputLabel" in eventLocationType ? eventLocationType?.organizerInputLabel : null}
data-testid={`${eventLocationType.type}-location-input`}
defaultValue={defaultLocation ? defaultLocation[eventLocationType.defaultValueVariable] : undefined}
eventLocationType={eventLocationType}
index={index}
customClassNames={customClassNames?.organizerContactInput?.locationInput}
disableLocationProp={disableLocationProp}
/>
<ErrorMessage
errors={formState.errors?.locations?.[index]}
name={eventLocationType.defaultValueVariable}
className={classNames(
"text-error my-1 ml-6 text-sm",
customClassNames?.organizerContactInput?.errorMessage
)}
as="div"
id="location-error"
/>
{eventLocationType.organizerInputType && (
<CheckboxField
name={`locations[${index}].displayLocationPublicly`}
data-testid="display-location"
disabled={disableLocationProp}
defaultChecked={defaultLocation?.displayLocationPublicly}
description={t("display_location_label")}
className={customClassNames?.organizerContactInput?.publicDisplayCheckbox?.checkbox}
onChange={(e) => {
const fieldValues = getValues("locations")[index];
updateLocationField(index, {
...fieldValues,
displayLocationPublicly: e.target.checked,
});
}}
informationIconText={t("display_location_info_badge")}
/>
)}
{eventLocationType?.supportsCustomLabel && (
<TextField
label={t("location_custom_label_input_label")}
name={`locations[${index}].customLabel`}
type="text"
defaultValue={defaultLocation?.customLabel}
onChange={(e) => {
const fieldValues = getValues("locations")[index];
updateLocationField(index, {
...fieldValues,
customLabel: e.target.value,
});
}}
/>
)}
</LocationOptionContainer>
);
};
export default DefaultLocationSettings;
@@ -0,0 +1,76 @@
import { useFormContext, Controller } from "react-hook-form";
import type { EventLocationType } from "@calcom/app-store/locations";
import PhoneInput from "@calcom/features/components/phone-input";
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import classNames from "@calcom/ui/classNames";
import { TextField } from "@calcom/ui/components/form";
import type { LocationInputCustomClassNames } from "./types";
const LocationInput = (props: {
eventLocationType: EventLocationType;
defaultValue?: string;
index: number;
customClassNames?: LocationInputCustomClassNames;
disableLocationProp?: boolean;
label?: string | null;
}) => {
const { t } = useLocale();
const { eventLocationType, index, customClassNames, disableLocationProp, label, ...remainingProps } = props;
const formMethods = useFormContext<FormValues>();
if (eventLocationType?.organizerInputType === "text") {
const { defaultValue, ...rest } = remainingProps;
return (
<Controller
name={`locations.${index}.${eventLocationType.defaultValueVariable}`}
defaultValue={defaultValue}
render={({ field: { onChange, value } }) => {
return (
<TextField
label={label ? t(label) : null}
name={`locations[${index}].${eventLocationType.defaultValueVariable}`}
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
type="text"
required
onChange={onChange}
value={value}
{...(disableLocationProp ? { disabled: true } : {})}
className={classNames("my-0", customClassNames?.addressInput)}
{...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={disableLocationProp}
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
name={`locations[${index}].${eventLocationType.defaultValueVariable}`}
className={customClassNames?.phoneInput}
value={value}
onChange={onChange}
{...rest}
/>
);
}}
/>
);
}
return null;
};
export default LocationInput;
@@ -2,35 +2,32 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import { ErrorMessage } from "@hookform/error-message";
import Link from "next/link";
import { useEffect, useState } from "react";
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
import { useFieldArray, useFormContext } from "react-hook-form";
import type { UseFormGetValues, UseFormSetValue, Control, FormState } from "react-hook-form";
import type { EventLocationType } from "@calcom/app-store/locations";
import { getEventLocationType, MeetLocationType } from "@calcom/app-store/locations";
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
import PhoneInput from "@calcom/features/components/phone-input";
import type {
LocationFormValues,
EventTypeSetupProps,
CheckboxClassNames,
FormValues,
} from "@calcom/features/eventtypes/lib/types";
import CheckboxField from "@calcom/features/form/components/CheckboxField";
import type {
LocationSelectCustomClassNames,
SingleValueLocationOption,
} from "@calcom/features/form/components/LocationSelect";
import type { SingleValueLocationOption } from "@calcom/features/form/components/LocationSelect";
import LocationSelect from "@calcom/features/form/components/LocationSelect";
import ServerTrans from "@calcom/lib/components/ServerTrans";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import classNames from "@calcom/ui/classNames";
import { Button } from "@calcom/ui/components/button";
import { Input } from "@calcom/ui/components/form";
import { Icon } from "@calcom/ui/components/icon";
import { showToast } from "@calcom/ui/components/toast";
import CalVideoSettings from "./CalVideoSettings";
import DefaultLocationSettings from "./DefaultLocationSettings";
import LocationInput from "./LocationInput";
import type { LocationCustomClassNames } from "./types";
export type TEventTypeLocation = Pick<EventTypeSetupProps["eventType"], "locations" | "calVideoSettings">;
export type TLocationOptions = Pick<EventTypeSetupProps, "locationOptions">["locationOptions"];
@@ -42,19 +39,6 @@ type LocationInputCustomClassNames = {
phoneInput?: string;
};
export type LocationCustomClassNames = {
container?: string;
locationSelect?: LocationSelectCustomClassNames;
removeLocationButton?: string;
removeLocationIcon?: string;
addLocationButton?: string;
organizerContactInput?: {
errorMessage?: string;
locationInput?: LocationInputCustomClassNames;
publicDisplayCheckbox?: CheckboxClassNames;
};
};
type LocationsProps = {
team: { id: number } | null;
destinationCalendar: TDestinationCalendar;
@@ -107,66 +91,6 @@ const getLocationInfo = ({
return { locationAvailable, locationDetails };
};
const LocationInput = (props: {
eventLocationType: EventLocationType;
defaultValue?: string;
index: number;
customClassNames?: LocationInputCustomClassNames;
disableLocationProp?: boolean;
}) => {
const { t } = useLocale();
const { eventLocationType, index, customClassNames, disableLocationProp, ...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}
{...(disableLocationProp ? { disabled: true } : {})}
className={classNames("my-0", customClassNames?.addressInput)}
{...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={disableLocationProp}
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
name={`locations[${index}].${eventLocationType.defaultValueVariable}`}
className={customClassNames?.phoneInput}
value={value}
onChange={onChange}
{...rest}
/>
);
}}
/>
);
}
return null;
};
const Locations: React.FC<LocationsProps> = ({
isChildrenManagedEventType,
disableLocationProp,
@@ -343,7 +267,14 @@ const Locations: React.FC<LocationsProps> = ({
{isCalVideo && !isPlatform && <CalVideoSettings />}
{eventLocationType?.organizerInputType && (
{eventLocationType?.supportsCustomLabel && eventLocationType?.type ? (
<DefaultLocationSettings
field={field}
index={index}
disableLocationProp={disableLocationProp}
customClassNames={customClassNames}
/>
) : eventLocationType?.organizerInputType ? (
<div className="mt-2 space-y-2">
<div className="w-full">
<div className="flex gap-2">
@@ -397,7 +328,7 @@ const Locations: React.FC<LocationsProps> = ({
/>
</div>
</div>
)}
) : null}
</li>
);
})}
@@ -0,0 +1,19 @@
import type { LocationSelectCustomClassNames } from "@calcom/features/form/components/LocationSelect";
export type LocationCustomClassNames = {
container?: string;
locationSelect?: LocationSelectCustomClassNames;
removeLocationButton?: string;
removeLocationIcon?: string;
addLocationButton?: string;
organizerContactInput?: {
errorMessage?: string;
locationInput?: LocationInputCustomClassNames;
publicDisplayCheckbox?: CheckboxClassNames;
};
};
export type LocationInputCustomClassNames = {
addressInput?: string;
phoneInput?: string;
};
@@ -26,8 +26,8 @@ import { Select } from "@calcom/ui/components/form";
import { SettingsToggle } from "@calcom/ui/components/form";
import { Skeleton } from "@calcom/ui/components/skeleton";
import type { LocationCustomClassNames } from "../../locations/Locations";
import Locations from "../../locations/Locations";
import type { LocationCustomClassNames } from "../../locations/types";
export type EventSetupTabCustomClassNames = {
wrapper?: string;
@@ -52,6 +52,7 @@ type EventLocation = {
hostDefault?: string;
credentialId?: number;
teamName?: string;
customLabel?: string;
};
type PhoneCallConfig = {
+1
View File
@@ -188,6 +188,7 @@ export const eventTypeLocations = z.array(
hostPhoneNumber: z.string().optional(),
credentialId: z.number().optional(),
teamName: z.string().optional(),
customLabel: z.string().optional(),
})
);