Skip to content

Commit 2d90d11

Browse files
joeauyeunghariombalharaUdit-takkar
authored
feat: Add custom label for organizer default app and link meeting (calcom#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>
1 parent 7ae0d6b commit 2d90d11

12 files changed

Lines changed: 229 additions & 96 deletions

File tree

apps/web/public/static/locales/en/common.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3322,7 +3322,7 @@
33223322
"pbac_desc_delete_roles": "Delete roles",
33233323
"pbac_desc_manage_roles": "All actions on roles across organization teams",
33243324
"pbac_desc_create_workflows": "Create and set up new workflows",
3325-
"pbac_desc_view_workflows": "View existing workflows and their configurations",
3325+
"pbac_desc_view_workflows": "View existing workflows and their configurations",
33263326
"pbac_desc_update_workflows": "Edit and modify workflow settings",
33273327
"pbac_desc_delete_workflows": "Remove workflows from the system",
33283328
"pbac_desc_manage_workflows": "Full management access to all workflows",
@@ -3472,6 +3472,8 @@
34723472
"webhook_metadata": "Metadata",
34733473
"stats": "Stats",
34743474
"booking_status": "Booking status",
3475+
"location_custom_label_input_label": "Custom label on booking page",
3476+
"meeting_link": "Meeting link",
34753477
"my_bookings": "My Bookings",
34763478
"phone": "Phone",
34773479
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"

packages/app-store/locations.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export type DefaultEventLocationType = {
1818
messageForOrganizer: string;
1919
category: "in person" | "conferencing" | "other" | "phone";
2020
linkType: "static";
21+
supportsCustomLabel?: boolean;
2122

2223
iconUrl: string;
2324
urlRegExp?: string;
@@ -42,6 +43,7 @@ export type DefaultEventLocationType = {
4243
| {
4344
organizerInputType: "phone" | "text" | null;
4445
organizerInputPlaceholder?: string | null;
46+
organizerInputLabel?: string | null;
4547
attendeeInputType?: null;
4648
attendeeInputPlaceholder?: null;
4749
}
@@ -56,7 +58,7 @@ export type DefaultEventLocationType = {
5658
export type EventLocationTypeFromApp = Ensure<
5759
EventLocationTypeFromAppMeta,
5860
"defaultValueVariable" | "variable"
59-
>;
61+
> & { supportsCustomLabel?: boolean; organizerInputLabel?: string };
6062

6163
export type EventLocationType = DefaultEventLocationType | EventLocationTypeFromApp;
6264

@@ -147,18 +149,21 @@ export const defaultLocations: DefaultEventLocationType[] = [
147149
category: "conferencing",
148150
messageForOrganizer: "",
149151
linkType: "static",
152+
supportsCustomLabel: true,
150153
},
151154
{
152155
default: true,
153156
type: DefaultEventLocationTypeEnum.Link,
154157
label: "link_meeting",
155158
organizerInputType: "text",
159+
organizerInputLabel: "meeting_link",
156160
variable: "locationLink",
157161
messageForOrganizer: "Provide a Meeting Link",
158162
defaultValueVariable: "link",
159163
iconUrl: "/link.svg",
160164
category: "other",
161165
linkType: "static",
166+
supportsCustomLabel: true,
162167
},
163168
{
164169
default: true,
@@ -182,6 +187,7 @@ export const defaultLocations: DefaultEventLocationType[] = [
182187
label: "organizer_phone_number",
183188
messageForOrganizer: "Provide your phone number",
184189
organizerInputType: "phone",
190+
organizerInputLabel: "phone_number",
185191
variable: "locationPhoneNumber",
186192
defaultValueVariable: "hostPhoneNumber",
187193
iconUrl: "/phone.svg",
@@ -206,6 +212,7 @@ export type LocationObject = {
206212
address?: string;
207213
displayLocationPublicly?: boolean;
208214
credentialId?: number;
215+
customLabel?: string;
209216
} & Partial<
210217
Record<
211218
"address" | "attendeeAddress" | "link" | "hostPhoneNumber" | "hostDefault" | "phone" | "somewhereElse",
@@ -286,7 +293,7 @@ export const guessEventLocationType = (locationTypeOrValue: string | undefined |
286293

287294
export const LocationType = { ...DefaultEventLocationTypeEnum, ...AppStoreLocationType };
288295

289-
type PrivacyFilteredLocationObject = Optional<LocationObject, "address" | "link">;
296+
type PrivacyFilteredLocationObject = Optional<LocationObject, "address" | "link" | "customLabel">;
290297

291298
export const privacyFilteredLocations = (locations: LocationObject[]): PrivacyFilteredLocationObject[] => {
292299
const locationsAfterPrivacyFilter = locations.map((location) => {

packages/app-store/server.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export async function getLocationGroupedOptions(
2626
icon?: string;
2727
slug?: string;
2828
credentialId?: number;
29+
supportsCustomLabel?: boolean;
2930
}[]
3031
> = {};
3132

@@ -145,6 +146,7 @@ export async function getLocationGroupedOptions(
145146
label: l.label,
146147
value: l.type,
147148
icon: l.iconUrl,
149+
supportsCustomLabel: l.supportsCustomLabel,
148150
},
149151
];
150152
} else {
@@ -153,6 +155,7 @@ export async function getLocationGroupedOptions(
153155
label: l.label,
154156
value: l.type,
155157
icon: l.iconUrl,
158+
supportsCustomLabel: l.supportsCustomLabel,
156159
},
157160
];
158161
}

packages/features/bookings/components/event-meta/AvailableEventLocations.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import { getEventLocationType, getTranslatedLocation } from "@calcom/app-store/l
77
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
88
import { useLocale } from "@calcom/lib/hooks/useLocale";
99
import invertLogoOnDark from "@calcom/lib/invertLogoOnDark";
10+
import classNames from "@calcom/ui/classNames";
1011
import { Icon } from "@calcom/ui/components/icon";
1112
import { Tooltip } from "@calcom/ui/components/tooltip";
12-
import classNames from "@calcom/ui/classNames";
1313

1414
const excludeNullValues = (value: unknown) => !!value;
1515

@@ -47,7 +47,8 @@ function RenderLocationTooltip({ locations }: { locations: LocationObject[] }) {
4747
if (!eventLocationType) {
4848
return null;
4949
}
50-
const translatedLocation = getTranslatedLocation(location, eventLocationType, t);
50+
const translatedLocation =
51+
location.customLabel || getTranslatedLocation(location, eventLocationType, t);
5152
return (
5253
<div key={`${location.type}-${index}`} className="font-sm flex flex-row items-center">
5354
<RenderIcon eventLocationType={eventLocationType} isTooltip />
@@ -74,11 +75,8 @@ export function AvailableEventLocations({ locations }: { locations: LocationObje
7475
// It's possible that the location app got uninstalled
7576
return null;
7677
}
77-
if (eventLocationType.variable === "hostDefault") {
78-
return null;
79-
}
8078

81-
const translatedLocation = getTranslatedLocation(location, eventLocationType, t);
79+
const locationName = location?.customLabel || getTranslatedLocation(location, eventLocationType, t);
8280

8381
return (
8482
<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
8785
) : (
8886
<RenderIcon eventLocationType={eventLocationType} isTooltip={false} />
8987
)}
90-
<Tooltip content={translatedLocation}>
91-
<p className="line-clamp-1">{translatedLocation}</p>
88+
<Tooltip content={locationName}>
89+
<p className="line-clamp-1">{locationName}</p>
9290
</Tooltip>
9391
</div>
9492
);

packages/features/bookings/lib/getLocationOptionsForSelect.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export default function getLocationsOptionsForSelect(
1919
return null;
2020
}
2121
const type = eventLocation.type;
22-
const translatedLocation = getTranslatedLocation(location, eventLocation, t);
22+
const translatedLocation = location.customLabel || getTranslatedLocation(location, eventLocation, t);
2323

2424
return {
2525
// XYZ: is considered a namespace in i18next https://www.i18next.com/principles/namespaces and thus it gets cleaned up.
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { ErrorMessage } from "@hookform/error-message";
2+
import { useFieldArray, useFormContext } from "react-hook-form";
3+
4+
import { getEventLocationType } from "@calcom/app-store/locations";
5+
import type { LocationFormValues, FormValues } from "@calcom/features/eventtypes/lib/types";
6+
import CheckboxField from "@calcom/features/form/components/CheckboxField";
7+
import { useLocale } from "@calcom/lib/hooks/useLocale";
8+
import classNames from "@calcom/ui/classNames";
9+
import { TextField } from "@calcom/ui/components/form";
10+
11+
import LocationInput from "./LocationInput";
12+
import LocationOptionContainer from "./LocationSettingsContainer";
13+
import type { LocationCustomClassNames } from "./types";
14+
15+
const DefaultLocationSettings = ({
16+
field,
17+
index,
18+
disableLocationProp,
19+
customClassNames,
20+
}: {
21+
field: LocationFormValues["locations"][number];
22+
index: number;
23+
disableLocationProp?: boolean;
24+
customClassNames?: LocationCustomClassNames;
25+
}) => {
26+
const { t } = useLocale();
27+
const { getValues, control, formState } = useFormContext<FormValues>();
28+
const { update: updateLocationField } = useFieldArray({
29+
control,
30+
name: "locations",
31+
});
32+
const defaultLocation = field;
33+
const eventLocationType = getEventLocationType(field.type);
34+
35+
if (!eventLocationType) return null;
36+
37+
return (
38+
<LocationOptionContainer>
39+
<LocationInput
40+
label={"organizerInputLabel" in eventLocationType ? eventLocationType?.organizerInputLabel : null}
41+
data-testid={`${eventLocationType.type}-location-input`}
42+
defaultValue={defaultLocation ? defaultLocation[eventLocationType.defaultValueVariable] : undefined}
43+
eventLocationType={eventLocationType}
44+
index={index}
45+
customClassNames={customClassNames?.organizerContactInput?.locationInput}
46+
disableLocationProp={disableLocationProp}
47+
/>
48+
<ErrorMessage
49+
errors={formState.errors?.locations?.[index]}
50+
name={eventLocationType.defaultValueVariable}
51+
className={classNames(
52+
"text-error my-1 ml-6 text-sm",
53+
customClassNames?.organizerContactInput?.errorMessage
54+
)}
55+
as="div"
56+
id="location-error"
57+
/>
58+
{eventLocationType.organizerInputType && (
59+
<CheckboxField
60+
name={`locations[${index}].displayLocationPublicly`}
61+
data-testid="display-location"
62+
disabled={disableLocationProp}
63+
defaultChecked={defaultLocation?.displayLocationPublicly}
64+
description={t("display_location_label")}
65+
className={customClassNames?.organizerContactInput?.publicDisplayCheckbox?.checkbox}
66+
onChange={(e) => {
67+
const fieldValues = getValues("locations")[index];
68+
updateLocationField(index, {
69+
...fieldValues,
70+
displayLocationPublicly: e.target.checked,
71+
});
72+
}}
73+
informationIconText={t("display_location_info_badge")}
74+
/>
75+
)}
76+
{eventLocationType?.supportsCustomLabel && (
77+
<TextField
78+
label={t("location_custom_label_input_label")}
79+
name={`locations[${index}].customLabel`}
80+
type="text"
81+
defaultValue={defaultLocation?.customLabel}
82+
onChange={(e) => {
83+
const fieldValues = getValues("locations")[index];
84+
updateLocationField(index, {
85+
...fieldValues,
86+
customLabel: e.target.value,
87+
});
88+
}}
89+
/>
90+
)}
91+
</LocationOptionContainer>
92+
);
93+
};
94+
95+
export default DefaultLocationSettings;
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { useFormContext, Controller } from "react-hook-form";
2+
3+
import type { EventLocationType } from "@calcom/app-store/locations";
4+
import PhoneInput from "@calcom/features/components/phone-input";
5+
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
6+
import { useLocale } from "@calcom/lib/hooks/useLocale";
7+
import classNames from "@calcom/ui/classNames";
8+
import { TextField } from "@calcom/ui/components/form";
9+
10+
import type { LocationInputCustomClassNames } from "./types";
11+
12+
const LocationInput = (props: {
13+
eventLocationType: EventLocationType;
14+
defaultValue?: string;
15+
index: number;
16+
customClassNames?: LocationInputCustomClassNames;
17+
disableLocationProp?: boolean;
18+
label?: string | null;
19+
}) => {
20+
const { t } = useLocale();
21+
const { eventLocationType, index, customClassNames, disableLocationProp, label, ...remainingProps } = props;
22+
const formMethods = useFormContext<FormValues>();
23+
24+
if (eventLocationType?.organizerInputType === "text") {
25+
const { defaultValue, ...rest } = remainingProps;
26+
27+
return (
28+
<Controller
29+
name={`locations.${index}.${eventLocationType.defaultValueVariable}`}
30+
defaultValue={defaultValue}
31+
render={({ field: { onChange, value } }) => {
32+
return (
33+
<TextField
34+
label={label ? t(label) : null}
35+
name={`locations[${index}].${eventLocationType.defaultValueVariable}`}
36+
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
37+
type="text"
38+
required
39+
onChange={onChange}
40+
value={value}
41+
{...(disableLocationProp ? { disabled: true } : {})}
42+
className={classNames("my-0", customClassNames?.addressInput)}
43+
{...rest}
44+
/>
45+
);
46+
}}
47+
/>
48+
);
49+
} else if (eventLocationType?.organizerInputType === "phone") {
50+
const { defaultValue, ...rest } = remainingProps;
51+
52+
return (
53+
<Controller
54+
name={`locations.${index}.${eventLocationType.defaultValueVariable}`}
55+
defaultValue={defaultValue}
56+
render={({ field: { onChange, value } }) => {
57+
return (
58+
<PhoneInput
59+
required
60+
disabled={disableLocationProp}
61+
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
62+
name={`locations[${index}].${eventLocationType.defaultValueVariable}`}
63+
className={customClassNames?.phoneInput}
64+
value={value}
65+
onChange={onChange}
66+
{...rest}
67+
/>
68+
);
69+
}}
70+
/>
71+
);
72+
}
73+
return null;
74+
};
75+
76+
export default LocationInput;

0 commit comments

Comments
 (0)