feat: remove location modal in event setup (#11796)

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
teste2e-createManagedBooking^2
Udit Takkar 2023-10-24 17:59:54 +05:30 committed by GitHub
parent b934c74c30
commit 9250b91bb0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 408 additions and 247 deletions

View File

@ -1,27 +1,22 @@
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import { zodResolver } from "@hookform/resolvers/zod"; import { ErrorMessage } from "@hookform/error-message";
import { isValidPhoneNumber } from "libphonenumber-js";
import { Trans } from "next-i18next"; import { Trans } from "next-i18next";
import Link from "next/link"; import Link from "next/link";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]"; import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Controller, useForm, useFormContext } from "react-hook-form"; import { Controller, useFormContext, useFieldArray } from "react-hook-form";
import type { MultiValue } from "react-select"; import type { MultiValue } from "react-select";
import { z } from "zod";
import type { EventLocationType } from "@calcom/app-store/locations"; import type { EventLocationType } from "@calcom/app-store/locations";
import { getEventLocationType, MeetLocationType, LocationType } from "@calcom/app-store/locations"; import { getEventLocationType, LocationType, MeetLocationType } from "@calcom/app-store/locations";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager"; import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider"; import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
import { classNames } from "@calcom/lib";
import { CAL_URL } from "@calcom/lib/constants"; import { CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import invertLogoOnDark from "@calcom/lib/invertLogoOnDark";
import { md } from "@calcom/lib/markdownIt"; import { md } from "@calcom/lib/markdownIt";
import { slugify } from "@calcom/lib/slugify"; import { slugify } from "@calcom/lib/slugify";
import turndown from "@calcom/lib/turndownService"; import turndown from "@calcom/lib/turndownService";
import { import {
Button,
Label, Label,
Select, Select,
SettingsToggle, SettingsToggle,
@ -30,11 +25,16 @@ import {
Editor, Editor,
SkeletonContainer, SkeletonContainer,
SkeletonText, SkeletonText,
Input,
PhoneInput,
Button,
showToast,
} from "@calcom/ui"; } from "@calcom/ui";
import { Edit2, Check, X, Plus } from "@calcom/ui/components/icon"; import { Plus, X, Check } from "@calcom/ui/components/icon";
import { CornerDownRight } from "@calcom/ui/components/icon";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog"; import CheckboxField from "@components/ui/form/CheckboxField";
import type { SingleValueLocationOption, LocationOption } from "@components/ui/form/LocationSelect"; import type { SingleValueLocationOption } from "@components/ui/form/LocationSelect";
import LocationSelect from "@components/ui/form/LocationSelect"; import LocationSelect from "@components/ui/form/LocationSelect";
const getLocationFromType = ( const getLocationFromType = (
@ -114,9 +114,6 @@ export const EventSetupTab = (
const { t } = useLocale(); const { t } = useLocale();
const formMethods = useFormContext<FormValues>(); const formMethods = useFormContext<FormValues>();
const { eventType, team, destinationCalendar } = props; const { eventType, team, destinationCalendar } = props;
const [showLocationModal, setShowLocationModal] = useState(false);
const [editingLocationType, setEditingLocationType] = useState<string>("");
const [selectedLocation, setSelectedLocation] = useState<LocationOption | undefined>(undefined);
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata?.multipleDuration); const [multipleDuration, setMultipleDuration] = useState(eventType.metadata?.multipleDuration);
const orgBranding = useOrgBranding(); const orgBranding = useOrgBranding();
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled"); const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
@ -150,83 +147,6 @@ export const EventSetupTab = (
selectedMultipleDuration.find((opt) => opt.value === eventType.length) ?? null selectedMultipleDuration.find((opt) => opt.value === eventType.length) ?? null
); );
const openLocationModal = (type: EventLocationType["type"], address = "") => {
const option = getLocationFromType(type, locationOptions);
if (option && option.value === LocationType.InPerson) {
const inPersonOption = {
...option,
address,
};
setSelectedLocation(inPersonOption);
} else {
setSelectedLocation(option);
}
setShowLocationModal(true);
};
const removeLocation = (selectedLocation: (typeof eventType.locations)[number]) => {
formMethods.setValue(
"locations",
formMethods.getValues("locations").filter((location) => {
if (location.type === LocationType.InPerson) {
return location.address !== selectedLocation.address;
}
return location.type !== selectedLocation.type;
}),
{ shouldValidate: true }
);
};
const saveLocation = (newLocationType: EventLocationType["type"], details = {}) => {
const locationType = editingLocationType !== "" ? editingLocationType : newLocationType;
const existingIdx = formMethods.getValues("locations").findIndex((loc) => locationType === loc.type);
if (existingIdx !== -1) {
const copy = formMethods.getValues("locations");
if (editingLocationType !== "") {
copy[existingIdx] = {
...details,
type: newLocationType,
};
}
formMethods.setValue("locations", [
...copy,
...(newLocationType === LocationType.InPerson && editingLocationType === ""
? [{ ...details, type: newLocationType }]
: []),
]);
} else {
formMethods.setValue(
"locations",
formMethods.getValues("locations").concat({ type: newLocationType, ...details })
);
}
setEditingLocationType("");
setShowLocationModal(false);
};
const locationFormSchema = z.object({
locationType: z.string(),
locationAddress: z.string().optional(),
displayLocationPublicly: z.boolean().optional(),
locationPhoneNumber: z
.string()
.refine((val) => isValidPhoneNumber(val))
.optional(),
locationLink: z.string().url().optional(), // URL validates as new URL() - which requires HTTPS:// In the input field
});
const locationFormMethods = useForm<{
locationType: EventLocationType["type"];
locationPhoneNumber?: string;
locationAddress?: string; // TODO: We should validate address or fetch the address from googles api to see if its valid?
locationLink?: string; // Currently this only accepts links that are HTTPS://
displayLocationPublicly?: boolean;
}>({
resolver: zodResolver(locationFormSchema),
});
const { isChildrenManagedEventType, isManagedEventType, shouldLockIndicator, shouldLockDisableProps } = const { isChildrenManagedEventType, isManagedEventType, shouldLockIndicator, shouldLockDisableProps } =
useLockedFieldsManager( useLockedFieldsManager(
eventType, eventType,
@ -236,6 +156,15 @@ export const EventSetupTab = (
const Locations = () => { const Locations = () => {
const { t } = useLocale(); const { t } = useLocale();
const {
fields: locationFields,
append,
remove,
update: updateLocationField,
} = useFieldArray({
control: formMethods.control,
name: "locations",
});
const [animationRef] = useAutoAnimate<HTMLUListElement>(); const [animationRef] = useAutoAnimate<HTMLUListElement>();
@ -254,131 +183,266 @@ export const EventSetupTab = (
const { locationDetails, locationAvailable } = getLocationInfo(props); 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}`}
control={formMethods.control}
defaultValue={defaultValue}
render={({ field: { onChange, value } }) => {
return (
<>
<Input
name={`locations[${index}].${eventLocationType.defaultValueVariable}`}
type="text"
required
onChange={onChange}
value={value}
className="my-0"
{...rest}
/>
<ErrorMessage
errors={formMethods.formState.errors.locations?.[index]}
name={eventLocationType.defaultValueVariable}
className="text-error my-1 text-sm"
as="div"
/>
</>
);
}}
/>
);
} else if (eventLocationType?.organizerInputType === "phone") {
const { defaultValue, ...rest } = remainingProps;
return (
<Controller
name={`locations.${index}.${eventLocationType.defaultValueVariable}`}
control={formMethods.control}
defaultValue={defaultValue}
render={({ field: { onChange, value } }) => {
return (
<>
<PhoneInput
required
name={`locations[${index}].${eventLocationType.defaultValueVariable}`}
value={value}
onChange={onChange}
{...rest}
/>
<ErrorMessage
errors={formMethods.formState.errors.locations?.[index]}
name={eventLocationType.defaultValueVariable}
className="text-error my-1 text-sm"
as="div"
/>
</>
);
}}
/>
);
}
return null;
};
const [showEmptyLocationSelect, setShowEmptyLocationSelect] = useState(false);
const [selectedNewOption, setSelectedNewOption] = useState<SingleValueLocationOption | null>(null);
return ( return (
<div className="w-full"> <div className="w-full">
{validLocations.length === 0 && ( <ul ref={animationRef} className="space-y-2">
<div className="flex"> {locationFields.map((field, index) => {
<LocationSelect const eventLocationType = getEventLocationType(field.type);
placeholder={t("select")} const defaultLocation = formMethods
options={locationOptions} .getValues("locations")
isDisabled={shouldLockDisableProps("locations").disabled} ?.find((location: { type: EventLocationType["type"]; address?: string }) => {
defaultValue={defaultValue} if (location.type === LocationType.InPerson) {
isSearchable={false} return location.type === eventLocationType?.type && location.address === field?.address;
className="block w-full min-w-0 flex-1 rounded-sm text-sm" } else {
menuPlacement="auto" return location.type === eventLocationType?.type;
onChange={(e: SingleValueLocationOption) => {
if (e?.value) {
const newLocationType = e.value;
const eventLocationType = getEventLocationType(newLocationType);
if (!eventLocationType) {
return;
}
locationFormMethods.setValue("locationType", newLocationType);
if (eventLocationType.organizerInputType) {
openLocationModal(newLocationType);
} else {
saveLocation(newLocationType);
}
} }
}} });
/>
</div>
)}
{validLocations.length > 0 && (
<ul ref={animationRef}>
{validLocations.map((location, index) => {
const eventLocationType = getEventLocationType(location.type);
if (!eventLocationType) {
return null;
}
const eventLabel = const option = getLocationFromType(field.type, locationOptions);
location[eventLocationType.defaultValueVariable] || t(eventLocationType.label);
return ( return (
<li <li key={field.id}>
key={`${location.type}${index}`} <div className="flex w-full items-center">
className="border-default text-default mb-2 h-9 rounded-md border px-2 py-1.5 hover:cursor-pointer"> <LocationSelect
<div className="flex items-center justify-between"> name={`locations[${index}].type`}
<div className="flex items-center"> placeholder={t("select")}
<img options={locationOptions}
src={eventLocationType.iconUrl} isDisabled={shouldLockDisableProps("locations").disabled}
className={classNames( defaultValue={option}
"h-4 w-4", isSearchable={false}
classNames(invertLogoOnDark(eventLocationType.iconUrl)) className="block min-w-0 flex-1 rounded-sm text-sm"
)} menuPlacement="auto"
alt={`${eventLocationType.label} logo`} onChange={(e: SingleValueLocationOption) => {
/> if (e?.value) {
<span className="ms-1 line-clamp-1 text-sm">{`${eventLabel} ${ const newLocationType = e.value;
location.teamName ? `(${location.teamName})` : "" const eventLocationType = getEventLocationType(newLocationType);
}`}</span> if (!eventLocationType) {
return;
}
const canAddLocation =
eventLocationType.organizerInputType ||
!validLocations.find((location) => location.type === newLocationType);
if (canAddLocation) {
updateLocationField(index, { type: newLocationType });
} else {
updateLocationField(index, { type: field.type });
showToast(t("location_already_exists"), "warning");
}
}
}}
/>
<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">
<X className="border-l-1 hover:text-emphasis text-subtle h-4 w-4" />
</div> </div>
<div className="flex"> </button>
<button </div>
type="button"
onClick={() => { {eventLocationType?.organizerInputType && (
locationFormMethods.setValue("locationType", location.type); <div className="mt-2 space-y-2">
locationFormMethods.unregister("locationLink"); <div className="flex gap-2">
if (location.type === LocationType.InPerson) { <div className="flex items-center justify-center">
locationFormMethods.setValue("locationAddress", location.address); <CornerDownRight className="h-4 w-4" />
} else { </div>
locationFormMethods.unregister("locationAddress"); <div className="w-full">
<LocationInput
defaultValue={
defaultLocation
? defaultLocation[eventLocationType.defaultValueVariable]
: undefined
} }
locationFormMethods.unregister("locationPhoneNumber"); eventLocationType={eventLocationType}
setEditingLocationType(location.type); index={index}
openLocationModal(location.type, location.address); />
</div>
</div>
<div className="ml-6">
<CheckboxField
data-testid="display-location"
defaultChecked={defaultLocation?.displayLocationPublicly}
description={t("display_location_label")}
onChange={(e) => {
const fieldValues = formMethods.getValues().locations[index];
updateLocationField(index, {
...fieldValues,
displayLocationPublicly: e.target.checked,
});
}} }}
aria-label={t("edit")} informationIconText={t("display_location_info_badge")}
className="hover:text-emphasis text-subtle mr-1 p-1"> />
<Edit2 className="h-4 w-4" />
</button>
<button type="button" onClick={() => removeLocation(location)} aria-label={t("remove")}>
<X className="border-l-1 hover:text-emphasis text-subtle h-6 w-6 pl-1 " />
</button>
</div> </div>
</div> </div>
</li> )}
);
})}
{validLocations.some(
(location) =>
location.type === MeetLocationType && destinationCalendar?.integration !== "google_calendar"
) && (
<div className="text-default flex text-sm">
<Check className="mr-1.5 mt-0.5 h-2 w-2.5" />
<Trans i18nKey="event_type_requres_google_cal">
<p>
The Add to calendar for this event type needs to be a Google Calendar for Meet to work.
Change it{" "}
<Link
href={`${CAL_URL}/event-types/${eventType.id}?tabName=advanced`}
className="underline">
here.
</Link>{" "}
</p>
</Trans>
</div>
)}
{isChildrenManagedEventType && !locationAvailable && locationDetails && (
<p className="pl-1 text-sm leading-none text-red-600">
{t("app_not_connected", { appName: locationDetails.name })}{" "}
<a className="underline" href={`${CAL_URL}/apps/${locationDetails.slug}`}>
{t("connect_now")}
</a>
</p>
)}
{validLocations.length > 0 && !isManagedEventType && !isChildrenManagedEventType && (
<li>
<Button
data-testid="add-location"
StartIcon={Plus}
color="minimal"
onClick={() => setShowLocationModal(true)}>
{t("add_location")}
</Button>
</li> </li>
)} );
</ul> })}
)} {(validLocations.length === 0 || showEmptyLocationSelect) && (
<div className="flex">
<LocationSelect
defaultMenuIsOpen={showEmptyLocationSelect}
autoFocus
placeholder={t("select")}
options={locationOptions}
value={selectedNewOption}
isDisabled={shouldLockDisableProps("locations").disabled}
defaultValue={defaultValue}
isSearchable={false}
className="block w-full 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 canAppendLocation =
eventLocationType.organizerInputType ||
!validLocations.find((location) => location.type === newLocationType);
if (canAppendLocation) {
append({ type: newLocationType });
setSelectedNewOption(e);
} else {
showToast(t("location_already_exists"), "warning");
setSelectedNewOption(null);
}
}
}}
/>
</div>
)}
{validLocations.some(
(location) =>
location.type === MeetLocationType && destinationCalendar?.integration !== "google_calendar"
) && (
<div className="text-default flex items-center text-sm">
<div className="mr-1.5 h-3 w-3">
<Check className="h-3 w-3" />
</div>
<Trans i18nKey="event_type_requres_google_cal">
<p>
The Add to calendar for this event type needs to be a Google Calendar for Meet to work.
Change it{" "}
<Link
href={`${CAL_URL}/event-types/${eventType.id}?tabName=advanced`}
className="underline">
here.
</Link>{" "}
</p>
</Trans>
</div>
)}
{isChildrenManagedEventType && !locationAvailable && locationDetails && (
<p className="pl-1 text-sm leading-none text-red-600">
{t("app_not_connected", { appName: locationDetails.name })}{" "}
<a className="underline" href={`${CAL_URL}/apps/${locationDetails.slug}`}>
{t("connect_now")}
</a>
</p>
)}
{validLocations.length > 0 && !isManagedEventType && !isChildrenManagedEventType && (
<li>
<Button
data-testid="add-location"
StartIcon={Plus}
color="minimal"
onClick={() => setShowEmptyLocationSelect(true)}>
{t("add_location")}
</Button>
</li>
)}
</ul>
<p className="text-default mt-2 text-sm">
<Trans i18nKey="cant_find_the_right_video_app_visit_our_app_store">
Can&apos;t find the right video app? Visit our
<Link className="cursor-pointer text-blue-500 underline" href="/apps/categories/video">
App Store
</Link>
.
</Trans>
</p>
</div> </div>
); );
}; };
@ -542,33 +606,6 @@ export const EventSetupTab = (
/> />
</div> </div>
</div> </div>
{/* We portal this modal so we can submit the form inside. Otherwise we get issues submitting two forms at once */}
<EditLocationDialog
isOpenDialog={showLocationModal}
setShowLocationModal={setShowLocationModal}
saveLocation={saveLocation}
defaultValues={formMethods.getValues("locations")}
selection={
selectedLocation
? selectedLocation.address
? {
value: selectedLocation.value,
label: t(selectedLocation.label),
icon: selectedLocation.icon,
address: selectedLocation.address,
}
: {
value: selectedLocation.value,
label: t(selectedLocation.label),
icon: selectedLocation.icon,
}
: undefined
}
setSelectedLocation={setSelectedLocation}
setEditingLocationType={setEditingLocationType}
teamId={eventType.team?.id}
/>
</div> </div>
</div> </div>
); );

View File

@ -52,7 +52,7 @@ const CheckboxField = forwardRef<HTMLInputElement, Props>(
className="text-primary-600 focus:ring-primary-500 border-default bg-default h-4 w-4 rounded" className="text-primary-600 focus:ring-primary-500 border-default bg-default h-4 w-4 rounded"
/> />
</div> </div>
<span className="ms-3 text-sm">{description}</span> <span className="ms-2 text-sm">{description}</span>
</> </>
)} )}
{informationIconText && <InfoBadge content={informationIconText} />} {informationIconText && <InfoBadge content={informationIconText} />}

View File

@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-empty-function */ /* eslint-disable @typescript-eslint/no-empty-function */
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { isValidPhoneNumber } from "libphonenumber-js";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
@ -299,6 +300,28 @@ const EventTypePage = (props: EventTypeSetupProps) => {
length: z.union([z.string().transform((val) => +val), z.number()]).optional(), length: z.union([z.string().transform((val) => +val), z.number()]).optional(),
offsetStart: z.union([z.string().transform((val) => +val), z.number()]).optional(), offsetStart: z.union([z.string().transform((val) => +val), z.number()]).optional(),
bookingFields: eventTypeBookingFields, bookingFields: eventTypeBookingFields,
locations: z
.array(
z
.object({
type: z.string(),
address: z.string().optional(),
link: z.string().url().optional(),
phone: z
.string()
.refine((val) => isValidPhoneNumber(val))
.optional(),
hostPhoneNumber: z
.string()
.refine((val) => isValidPhoneNumber(val))
.optional(),
displayLocationPublicly: z.boolean().optional(),
credentialId: z.number().optional(),
teamName: z.string().optional(),
})
.passthrough()
)
.optional(),
}) })
// TODO: Add schema for other fields later. // TODO: Add schema for other fields later.
.passthrough() .passthrough()

View File

@ -115,23 +115,13 @@ test.describe("Event Types tests", () => {
const locationData = ["location 1", "location 2", "location 3"]; const locationData = ["location 1", "location 2", "location 3"];
const fillLocation = async (inputText: string) => { await fillLocation(page, locationData[0], 0);
await page.locator("#location-select").click();
await page.locator("text=In Person (Organizer Address)").click();
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(1000);
await page.locator('input[name="locationAddress"]').fill(inputText);
await page.locator("[data-testid=display-location]").check();
await page.locator("[data-testid=update-location]").click();
};
await fillLocation(locationData[0]);
await page.locator("[data-testid=add-location]").click(); await page.locator("[data-testid=add-location]").click();
await fillLocation(locationData[1]); await fillLocation(page, locationData[1], 1);
await page.locator("[data-testid=add-location]").click(); await page.locator("[data-testid=add-location]").click();
await fillLocation(locationData[2]); await fillLocation(page, locationData[2], 2);
await page.locator("[data-testid=update-eventtype]").click(); await page.locator("[data-testid=update-eventtype]").click();
@ -177,6 +167,93 @@ test.describe("Event Types tests", () => {
await expect(page.locator("[data-testid=success-page]")).toBeVisible(); await expect(page.locator("[data-testid=success-page]")).toBeVisible();
await expect(page.locator("text=+19199999999")).toBeVisible(); await expect(page.locator("text=+19199999999")).toBeVisible();
}); });
test("Can add Organzer Phone Number location and book with it", async ({ page }) => {
await gotoFirstEventType(page);
await page.locator("#location-select").click();
await page.locator(`text="Organizer Phone Number"`).click();
const locationInputName = "locations[0].hostPhoneNumber";
await page.locator(`input[name="${locationInputName}"]`).waitFor();
await page.locator(`input[name="${locationInputName}"]`).fill("9199999999");
await saveEventType(page);
await gotoBookingPage(page);
await selectFirstAvailableTimeSlotNextMonth(page);
await bookTimeSlot(page);
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
await expect(page.locator("text=+19199999999")).toBeVisible();
});
test("Can add Cal video location and book with it", async ({ page }) => {
await gotoFirstEventType(page);
await page.locator("#location-select").click();
await page.locator(`text="Cal Video (Global)"`).click();
await saveEventType(page);
await page.getByTestId("toast-success").waitFor();
await gotoBookingPage(page);
await selectFirstAvailableTimeSlotNextMonth(page);
await bookTimeSlot(page);
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
await expect(page.locator("[data-testid=where] ")).toContainText("Cal Video");
});
test("Can add Link Meeting as location and book with it", async ({ page }) => {
await gotoFirstEventType(page);
await page.locator("#location-select").click();
await page.locator(`text="Link meeting"`).click();
const locationInputName = `locations[0].link`;
const testUrl = "https://cal.ai/";
await page.locator(`input[name="${locationInputName}"]`).fill(testUrl);
await saveEventType(page);
await page.getByTestId("toast-success").waitFor();
await gotoBookingPage(page);
await selectFirstAvailableTimeSlotNextMonth(page);
await bookTimeSlot(page);
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
const linkElement = await page.locator("[data-testid=where] > a");
expect(await linkElement.getAttribute("href")).toBe(testUrl);
});
test("Can remove location from multiple locations that are saved", async ({ page }) => {
await gotoFirstEventType(page);
// Add Attendee Phone Number location
await selectAttendeePhoneNumber(page);
// Add Cal Video location
await addAnotherLocation(page, "Cal Video (Global)");
await saveEventType(page);
await page.waitForLoadState("networkidle");
// Remove Attendee Phone Number Location
const removeButtomId = "delete-locations.0.type";
await page.getByTestId(removeButtomId).click();
await saveEventType(page);
await page.waitForLoadState("networkidle");
await gotoBookingPage(page);
await selectFirstAvailableTimeSlotNextMonth(page);
await bookTimeSlot(page);
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
await expect(page.locator("[data-testid=where]")).toHaveText(/Cal Video/);
});
}); });
}); });
}); });
@ -205,3 +282,26 @@ async function gotoBookingPage(page: Page) {
await page.goto(previewLink ?? ""); await page.goto(previewLink ?? "");
} }
/**
* Adds n+1 location to the event type
*/
async function addAnotherLocation(page: Page, locationOptionText: string) {
await page.locator("[data-testid=add-location]").click();
// When adding another location, the dropdown opens automatically. So, we don't need to open it here.
//
await page.locator(`text="${locationOptionText}"`).click();
}
const fillLocation = async (page: Page, inputText: string, index: number) => {
// Except the first location, dropdown automatically opens when adding another location
if (index == 0) {
await page.locator("#location-select").last().click();
}
await page.locator("text=In Person (Organizer Address)").last().click();
const locationInputName = `locations[${index}].address`;
await page.locator(`input[name="${locationInputName}"]`).waitFor();
await page.locator(`input[name="locations[${index}].address"]`).fill(inputText);
await page.locator("[data-testid=display-location]").last().check();
};

View File

@ -1605,6 +1605,7 @@
"options": "Options", "options": "Options",
"enter_option": "Enter Option {{index}}", "enter_option": "Enter Option {{index}}",
"add_an_option": "Add an option", "add_an_option": "Add an option",
"location_already_exists": "This Location already exists. Please select a new location",
"radio": "Radio", "radio": "Radio",
"google_meet_warning": "In order to use Google Meet you must set your destination calendar to a Google Calendar", "google_meet_warning": "In order to use Google Meet you must set your destination calendar to a Google Calendar",
"individual": "Individual", "individual": "Individual",