feat: add option to hide duration selector in booking page for multiple durations (#27033)
* feat: add option to hide duration selector in booking page for multiple durations - Add 'hideDurationSelectorInBookingPage' field to event type metadata schema - Add checkbox under Default Duration select in event type settings - Modify Duration component to hide selector when setting is enabled - URL params can still set duration when selector is hidden - Add i18n translation for the new checkbox label - Add e2e tests for the new functionality Co-Authored-By: ali@cal.com <ali@cal.com> * fix: rename checkbox label to 'Hide duration selector' Co-Authored-By: ali@cal.com <ali@cal.com> * fix: replace text locator with data-testid in E2E test Replace `text=Multiple duration` locator with `page.getByTestId("event-types").locator('a[title="Multiple duration"]')` to follow E2E test best practices. Co-Authored-By: unknown <> * fix: add data-testid to hide duration selector checkbox for E2E tests Co-Authored-By: ali@cal.com <ali@cal.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: ali@cal.com <ali@cal.com> Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
This commit is contained in:
co-authored by
ali@cal.com <ali@cal.com>
ali@cal.com <ali@cal.com>
unknown <>
ali@cal.com <ali@cal.com>
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
ali@cal.com <ali@cal.com>
Syed Ali Shahbaz
parent
4162b5be55
commit
cb567a91c5
@@ -93,6 +93,13 @@ export const EventDuration = ({
|
||||
return <>{getDurationFormatted(event.length, t)}</>;
|
||||
|
||||
const durations = event?.metadata?.multipleDuration || [15, 30, 60, 90];
|
||||
const hideDurationSelector = event?.metadata?.hideDurationSelectorInBookingPage;
|
||||
|
||||
// When duration selector is hidden, show only the selected/default duration as text
|
||||
// URL params can still set the duration, but the user cannot change it via UI
|
||||
if (hideDurationSelector) {
|
||||
return <>{getDurationFormatted(selectedDuration || event.length, t)}</>;
|
||||
}
|
||||
|
||||
return selectedDuration ? (
|
||||
<div className="border-default relative mr-5 flex flex-row items-center justify-between rounded-md border">
|
||||
|
||||
@@ -15,7 +15,7 @@ import { slugify } from "@calcom/lib/slugify";
|
||||
import turndown from "@calcom/lib/turndownService";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
import { Editor } from "@calcom/ui/components/editor";
|
||||
import { Label, Select, SettingsToggle, TextAreaField, TextField } from "@calcom/ui/components/form";
|
||||
import { CheckboxField, Label, Select, SettingsToggle, TextAreaField, TextField } from "@calcom/ui/components/form";
|
||||
import { Skeleton } from "@calcom/ui/components/skeleton";
|
||||
import Locations from "@calcom/web/modules/event-types/components/locations/Locations";
|
||||
import { useState } from "react";
|
||||
@@ -276,6 +276,21 @@ export const EventSetupTab = (
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<Controller
|
||||
name="metadata.hideDurationSelectorInBookingPage"
|
||||
control={formMethods.control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<CheckboxField
|
||||
data-testid="hide-duration-selector-checkbox"
|
||||
checked={value ?? false}
|
||||
onChange={(e) => onChange(e.target.checked)}
|
||||
description={t("hide_duration_selector_in_booking_page")}
|
||||
disabled={lengthLockedProps.disabled}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<TextField
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import { test } from "./lib/fixtures";
|
||||
import { bookTimeSlot, selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils";
|
||||
|
||||
test.afterEach(({ users }) => users.deleteAll());
|
||||
|
||||
test.describe("Hide duration selector in booking page", () => {
|
||||
test("duration selector is hidden when setting is enabled but URL params still work", async ({
|
||||
page,
|
||||
users,
|
||||
}) => {
|
||||
const user = await users.create();
|
||||
await user.apiLogin();
|
||||
|
||||
// Get the multiple duration event type
|
||||
const eventType = user.eventTypes.find((e) => e.slug === "multiple-duration");
|
||||
expect(eventType).toBeDefined();
|
||||
|
||||
await test.step("enable hide duration selector setting", async () => {
|
||||
await page.goto("/event-types");
|
||||
await page.waitForSelector('[data-testid="event-types"]');
|
||||
await page.getByTestId("event-types").locator('a[title="Multiple duration"]').click();
|
||||
await page.waitForSelector('[data-testid="event-title"]');
|
||||
|
||||
// Find and check the hide duration selector checkbox
|
||||
const checkbox = page.getByTestId("hide-duration-selector-checkbox");
|
||||
await checkbox.check();
|
||||
|
||||
// Save the event type
|
||||
await page.locator('[data-testid="update-eventtype"]').click();
|
||||
await page.waitForResponse("/api/trpc/eventTypesHeavy/update?batch=1");
|
||||
});
|
||||
|
||||
await test.step("verify duration selector is hidden on booking page", async () => {
|
||||
await page.goto(`/${user.username}/multiple-duration`);
|
||||
|
||||
// Wait for the page to load
|
||||
await page.locator('[data-testid="day"][data-disabled="false"]').nth(0).waitFor({ state: "visible" });
|
||||
|
||||
// The duration selector buttons should NOT be visible
|
||||
await expect(page.getByTestId("multiple-choice-30mins")).not.toBeVisible();
|
||||
await expect(page.getByTestId("multiple-choice-60mins")).not.toBeVisible();
|
||||
await expect(page.getByTestId("multiple-choice-90mins")).not.toBeVisible();
|
||||
});
|
||||
|
||||
await test.step("verify URL param can still set duration when selector is hidden", async () => {
|
||||
// Navigate with duration param
|
||||
await page.goto(`/${user.username}/multiple-duration?duration=60`);
|
||||
|
||||
// Wait for the page to load
|
||||
await page.locator('[data-testid="day"][data-disabled="false"]').nth(0).waitFor({ state: "visible" });
|
||||
|
||||
// The duration selector should still be hidden
|
||||
await expect(page.getByTestId("multiple-choice-60mins")).not.toBeVisible();
|
||||
|
||||
// Select a time slot and book
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
|
||||
await page.fill('[name="name"]', "Test User");
|
||||
await page.fill('[name="email"]', "test@example.com");
|
||||
await bookTimeSlot(page);
|
||||
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
|
||||
// Verify the booking was made with the correct duration (60 mins = 1 hour)
|
||||
// The booking title or details should reflect the 60 min duration
|
||||
});
|
||||
});
|
||||
|
||||
test("duration selector is visible when setting is disabled", async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
await user.apiLogin();
|
||||
|
||||
// The default multiple-duration event type should have the selector visible
|
||||
await page.goto(`/${user.username}/multiple-duration`);
|
||||
|
||||
// Wait for the page to load
|
||||
await page.locator('[data-testid="day"][data-disabled="false"]').nth(0).waitFor({ state: "visible" });
|
||||
|
||||
// The duration selector buttons should be visible
|
||||
await expect(page.getByTestId("multiple-choice-30mins")).toBeVisible();
|
||||
await expect(page.getByTestId("multiple-choice-60mins")).toBeVisible();
|
||||
await expect(page.getByTestId("multiple-choice-90mins")).toBeVisible();
|
||||
});
|
||||
|
||||
test("default duration is shown when selector is hidden and no URL param", async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
await user.apiLogin();
|
||||
|
||||
// Get the multiple duration event type
|
||||
const eventType = user.eventTypes.find((e) => e.slug === "multiple-duration");
|
||||
expect(eventType).toBeDefined();
|
||||
|
||||
await test.step("enable hide duration selector setting", async () => {
|
||||
await page.goto("/event-types");
|
||||
await page.waitForSelector('[data-testid="event-types"]');
|
||||
await page.getByTestId("event-types").locator('a[title="Multiple duration"]').click();
|
||||
await page.waitForSelector('[data-testid="event-title"]');
|
||||
|
||||
// Find and check the hide duration selector checkbox
|
||||
const checkbox = page.getByTestId("hide-duration-selector-checkbox");
|
||||
await checkbox.check();
|
||||
|
||||
// Save the event type
|
||||
await page.locator('[data-testid="update-eventtype"]').click();
|
||||
await page.waitForResponse("/api/trpc/eventTypesHeavy/update?batch=1");
|
||||
});
|
||||
|
||||
await test.step("verify default duration is displayed", async () => {
|
||||
await page.goto(`/${user.username}/multiple-duration`);
|
||||
|
||||
// Wait for the page to load
|
||||
await page.locator('[data-testid="day"][data-disabled="false"]').nth(0).waitFor({ state: "visible" });
|
||||
|
||||
// The duration text should be visible (default is 30 mins)
|
||||
// Since the selector is hidden, we should see the duration as text
|
||||
await expect(page.locator('[data-testid="event-meta"]')).toContainText("30");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -881,6 +881,7 @@
|
||||
"available_durations": "Available durations",
|
||||
"default_duration": "Default duration",
|
||||
"default_duration_no_options": "Please choose available durations first",
|
||||
"hide_duration_selector_in_booking_page": "Hide duration selector",
|
||||
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
|
||||
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
|
||||
"multiple_duration_timeUnit_short": "{{count}}$t({{unit}}_short)",
|
||||
|
||||
@@ -218,6 +218,7 @@ const _eventTypeMetaDataSchemaWithoutApps = z.object({
|
||||
smartContractAddress: z.string().optional(),
|
||||
blockchainId: z.number().optional(),
|
||||
multipleDuration: z.number().array().optional(),
|
||||
hideDurationSelectorInBookingPage: z.boolean().optional(),
|
||||
giphyThankYouPage: z.string().optional(),
|
||||
additionalNotesRequired: z.boolean().optional(),
|
||||
disableSuccessPage: z.boolean().optional(),
|
||||
|
||||
Reference in New Issue
Block a user