chore: UX Fixes (#26643)
* Copy changes
* Move search bar inline with new button
* Get rid of no more results message
* Change hidden badge to (hidden)
* Remove Cal.ai badge from sidebar
* Add dropdown to create button when there is multiple options
* Fix delete dialog
* Saved filters updates
* More string fixes
* Switch members table to use names
* Fix member spacing
* Fix routing form identifier field
* Fix routing forms stuff
* Only show SMS hint on SMS options
* Make workflow delete button minimal
* Fix padding on workflow steps
* Remove min width on workflow title
* Fix delete workflow PR
* Fix org profile buttons
* Fix org profile screen partially scrolled down
* Improve logos & banner uploads
* Personal profile fixes
* Fix settings general view stuff
* Sentence case consistency
* Fix stuff I broke
* Fix fab
* Fix hidden translation string
* Fix text fields
* Make button small for solo users too
* fix: update E2E tests to match sentence case labels in routing forms
* fix: update tests to match sentence case label changes
- insights.e2e.ts: chart titles (14 strings)
- event-types.e2e.ts: Organizer phone number location
- EditLocationDialog.test.tsx: phone number labels
* fix: address Cubic AI review feedback (confidence 9+)
- Replace hardcoded text-gray-500 with text-muted in TextField.tsx hint section
- Replace text locator with data-testid in E2E test for location select
Co-Authored-By: unknown <>
* fix: update E2E tests for sentence case label changes
- Use data-testid selectors for location options (more reliable than text)
- Update field identifiers in routing-forms tests to match new labels
- Fix Long text selector in manage-booking-questions test
* fix: replace text locator with data-testid in manage-booking-questions E2E test
Replace fragile text="Long text" locator with resilient
page.getByTestId("select-option-textarea") selector per E2E best practices.
Addresses Cubic AI review feedback (confidence 9/10).
Co-Authored-By: unknown <>
* fix: use .last() for multiple location select items
---------
Co-authored-by: Pedro Castro <pedro@cal.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
unknown <>
Pedro Castro
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
32d97fbea4
commit
7c66f33de2
@@ -18,14 +18,21 @@ import {
|
||||
useBookerStoreContext,
|
||||
} from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||
import { useInitializeBookerStore } from "@calcom/features/bookings/Booker/store";
|
||||
import { useEvent, useScheduleForEvent } from "@calcom/features/bookings/Booker/utils/event";
|
||||
import {
|
||||
useEvent,
|
||||
useScheduleForEvent,
|
||||
} from "@calcom/features/bookings/Booker/utils/event";
|
||||
import DatePicker from "@calcom/features/calendars/DatePicker";
|
||||
import { Dialog } from "@calcom/features/components/controlled-dialog";
|
||||
import { TimezoneSelect } from "@calcom/features/components/timezone-select";
|
||||
import type { Slot } from "@calcom/features/schedules/lib/use-schedule/types";
|
||||
import { useNonEmptyScheduleDays } from "@calcom/features/schedules/lib/use-schedule/useNonEmptyScheduleDays";
|
||||
import { useSlotsForDate } from "@calcom/features/schedules/lib/use-schedule/useSlotsForDate";
|
||||
import { APP_NAME, DEFAULT_LIGHT_BRAND_COLOR, DEFAULT_DARK_BRAND_COLOR } from "@calcom/lib/constants";
|
||||
import {
|
||||
APP_NAME,
|
||||
DEFAULT_LIGHT_BRAND_COLOR,
|
||||
DEFAULT_DARK_BRAND_COLOR,
|
||||
} from "@calcom/lib/constants";
|
||||
import { weekdayToWeekIndex } from "@calcom/lib/dayjs";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -33,7 +40,11 @@ import { BookerLayouts } from "@calcom/prisma/zod-utils";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { DialogContent, DialogFooter, DialogClose } from "@calcom/ui/components/dialog";
|
||||
import {
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
} from "@calcom/ui/components/dialog";
|
||||
import { Select, ColorPicker } from "@calcom/ui/components/form";
|
||||
import { Label } from "@calcom/ui/components/form";
|
||||
import { TextField } from "@calcom/ui/components/form";
|
||||
@@ -49,14 +60,25 @@ import { EmbedTheme } from "@calcom/features/embed/lib/constants";
|
||||
import { getDimension } from "@calcom/features/embed/lib/getDimension";
|
||||
import { useEmbedDialogCtx } from "@calcom/features/embed/lib/hooks/useEmbedDialogCtx";
|
||||
import { useEmbedParams } from "@calcom/features/embed/lib/hooks/useEmbedParams";
|
||||
import type { EmbedTabs, EmbedType, EmbedTypes, PreviewState, EmbedConfig } from "@calcom/features/embed/types";
|
||||
import type {
|
||||
EmbedTabs,
|
||||
EmbedType,
|
||||
EmbedTypes,
|
||||
PreviewState,
|
||||
EmbedConfig,
|
||||
} from "@calcom/features/embed/types";
|
||||
|
||||
type EventType = RouterOutputs["viewer"]["eventTypes"]["get"]["eventType"] | undefined;
|
||||
type EventType =
|
||||
| RouterOutputs["viewer"]["eventTypes"]["get"]["eventType"]
|
||||
| undefined;
|
||||
type EmbedDialogProps = {
|
||||
types: EmbedTypes;
|
||||
tabs: EmbedTabs;
|
||||
eventTypeHideOptionDisabled: boolean;
|
||||
defaultBrandColor: { brandColor: string | null; darkBrandColor: string | null } | null;
|
||||
defaultBrandColor: {
|
||||
brandColor: string | null;
|
||||
darkBrandColor: string | null;
|
||||
} | null;
|
||||
noQueryParamMode?: boolean;
|
||||
};
|
||||
|
||||
@@ -91,7 +113,11 @@ function chooseTimezone({
|
||||
userSettingsTimezone: string | undefined;
|
||||
}) {
|
||||
// We prefer user's timezone configured in settings at the moment - Might be a better idea to prefer timezoneFromTimePreferences over user settings as the user might be in different timezone
|
||||
return timezoneFromBookerStore ?? userSettingsTimezone ?? timezoneFromTimePreferences;
|
||||
return (
|
||||
timezoneFromBookerStore ??
|
||||
userSettingsTimezone ??
|
||||
timezoneFromTimePreferences
|
||||
);
|
||||
}
|
||||
|
||||
function useRouterHelpers() {
|
||||
@@ -141,7 +167,10 @@ function useEmbedGoto(noQueryParamMode = false) {
|
||||
}));
|
||||
} else {
|
||||
const validQueryParams = Object.fromEntries(
|
||||
Object.entries(props).filter(([_, value]) => value !== null) as [string, string][]
|
||||
Object.entries(props).filter(([_, value]) => value !== null) as [
|
||||
string,
|
||||
string
|
||||
][]
|
||||
);
|
||||
goto(validQueryParams);
|
||||
}
|
||||
@@ -203,7 +232,9 @@ const ChooseEmbedTypesDialogContent = ({
|
||||
{t("how_you_want_add_cal_site", { appName: APP_NAME })}
|
||||
</h3>
|
||||
<div>
|
||||
<p className="text-subtle text-sm">{t("choose_ways_put_cal_site", { appName: APP_NAME })}</p>
|
||||
<p className="text-subtle text-sm">
|
||||
{t("choose_ways_put_cal_site", { appName: APP_NAME })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="items-start stack-y-2 md:flex md:stack-y-0">
|
||||
@@ -214,17 +245,23 @@ const ChooseEmbedTypesDialogContent = ({
|
||||
data-testid={embed.type}
|
||||
onClick={() => {
|
||||
if (embed.type === "headless") {
|
||||
window.open("https://cal.com/help/routing/headless-routing", "_blank");
|
||||
window.open(
|
||||
"https://cal.com/help/routing/headless-routing",
|
||||
"_blank"
|
||||
);
|
||||
} else {
|
||||
gotoState({
|
||||
embedType: embed.type as EmbedType,
|
||||
});
|
||||
}
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<div className="bg-default order-0 box-border flex-none rounded-md border border-solid transition dark:bg-transparent dark:invert">
|
||||
{embed.illustration}
|
||||
</div>
|
||||
<div className="text-emphasis mt-4 font-semibold">{embed.title}</div>
|
||||
<div className="text-emphasis mt-4 font-semibold">
|
||||
{embed.title}
|
||||
</div>
|
||||
<p className="text-subtle mt-2 text-sm">{embed.subtitle}</p>
|
||||
</button>
|
||||
))}
|
||||
@@ -251,7 +288,8 @@ const EmailEmbed = ({
|
||||
userSettingsTimezone?: string;
|
||||
}) => {
|
||||
const { t, i18n } = useLocale();
|
||||
const { timezoneFromBookerStore, timezoneFromTimePreferences } = useBookerTime();
|
||||
const { timezoneFromBookerStore, timezoneFromTimePreferences } =
|
||||
useBookerTime();
|
||||
const timezone = chooseTimezone({
|
||||
timezoneFromBookerStore,
|
||||
timezoneFromTimePreferences,
|
||||
@@ -279,17 +317,22 @@ const EmailEmbed = ({
|
||||
(state) => [state.month, state.selectedDate, state.selectedDatesAndTimes],
|
||||
shallow
|
||||
);
|
||||
const [setSelectedDate, setMonth, setSelectedDatesAndTimes, setSelectedTimeslot, setTimezone] =
|
||||
useBookerStoreContext(
|
||||
(state) => [
|
||||
state.setSelectedDate,
|
||||
state.setMonth,
|
||||
state.setSelectedDatesAndTimes,
|
||||
state.setSelectedTimeslot,
|
||||
state.setTimezone,
|
||||
],
|
||||
shallow
|
||||
);
|
||||
const [
|
||||
setSelectedDate,
|
||||
setMonth,
|
||||
setSelectedDatesAndTimes,
|
||||
setSelectedTimeslot,
|
||||
setTimezone,
|
||||
] = useBookerStoreContext(
|
||||
(state) => [
|
||||
state.setSelectedDate,
|
||||
state.setMonth,
|
||||
state.setSelectedDatesAndTimes,
|
||||
state.setSelectedTimeslot,
|
||||
state.setTimezone,
|
||||
],
|
||||
shallow
|
||||
);
|
||||
const event = useEvent();
|
||||
const schedule = useScheduleForEvent({
|
||||
orgSlug,
|
||||
@@ -306,8 +349,10 @@ const EmailEmbed = ({
|
||||
return null;
|
||||
}
|
||||
if (selectedDatesAndTimes && selectedDatesAndTimes[eventType.slug]) {
|
||||
const selectedDatesAndTimesForEvent = selectedDatesAndTimes[eventType.slug];
|
||||
const selectedSlots = selectedDatesAndTimesForEvent[selectedDate as string] ?? [];
|
||||
const selectedDatesAndTimesForEvent =
|
||||
selectedDatesAndTimes[eventType.slug];
|
||||
const selectedSlots =
|
||||
selectedDatesAndTimesForEvent[selectedDate as string] ?? [];
|
||||
if (selectedSlots?.includes(time)) {
|
||||
// Checks whether a user has removed all their timeSlots and thus removes it from the selectedDatesAndTimesForEvent state
|
||||
if (selectedSlots?.length > 1) {
|
||||
@@ -315,13 +360,17 @@ const EmailEmbed = ({
|
||||
...selectedDatesAndTimes,
|
||||
[eventType.slug]: {
|
||||
...selectedDatesAndTimesForEvent,
|
||||
[selectedDate as string]: selectedSlots?.filter((slot: string) => slot !== time),
|
||||
[selectedDate as string]: selectedSlots?.filter(
|
||||
(slot: string) => slot !== time
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
setSelectedDatesAndTimes(updatedDatesAndTimes);
|
||||
} else {
|
||||
const updatedDatesAndTimesForEvent = { ...selectedDatesAndTimesForEvent };
|
||||
const updatedDatesAndTimesForEvent = {
|
||||
...selectedDatesAndTimesForEvent,
|
||||
};
|
||||
delete updatedDatesAndTimesForEvent[selectedDate as string];
|
||||
setSelectedTimeslot(null);
|
||||
setSelectedDatesAndTimes({
|
||||
@@ -342,7 +391,9 @@ const EmailEmbed = ({
|
||||
|
||||
setSelectedDatesAndTimes(updatedDatesAndTimes);
|
||||
} else if (!selectedDatesAndTimes) {
|
||||
setSelectedDatesAndTimes({ [eventType.slug]: { [selectedDate as string]: [time] } });
|
||||
setSelectedDatesAndTimes({
|
||||
[eventType.slug]: { [selectedDate as string]: [time] },
|
||||
});
|
||||
} else {
|
||||
setSelectedDatesAndTimes({
|
||||
...selectedDatesAndTimes,
|
||||
@@ -377,7 +428,9 @@ const EmailEmbed = ({
|
||||
<DatePicker
|
||||
isLoading={schedule.isPending}
|
||||
onChange={(date: Dayjs | null) => {
|
||||
setSelectedDate({ date: date === null ? date : date.format("YYYY-MM-DD") });
|
||||
setSelectedDate({
|
||||
date: date === null ? date : date.format("YYYY-MM-DD"),
|
||||
});
|
||||
}}
|
||||
onMonthChange={(date: Dayjs) => {
|
||||
setMonth(date.format("YYYY-MM"));
|
||||
@@ -387,7 +440,9 @@ const EmailEmbed = ({
|
||||
locale={i18n.language}
|
||||
browsingDate={month ? dayjs(month) : undefined}
|
||||
selected={dayjs(selectedDate)}
|
||||
weekStart={weekdayToWeekIndex(event?.data?.subsetOfUsers?.[0]?.weekStart)}
|
||||
weekStart={weekdayToWeekIndex(
|
||||
event?.data?.subsetOfUsers?.[0]?.weekStart
|
||||
)}
|
||||
eventSlug={eventType?.slug}
|
||||
/>
|
||||
</CollapsibleContent>
|
||||
@@ -405,7 +460,9 @@ const EmailEmbed = ({
|
||||
selectedDatesAndTimes &&
|
||||
selectedDatesAndTimes[eventType.slug] &&
|
||||
selectedDatesAndTimes[eventType.slug][selectedDate as string]
|
||||
? selectedDatesAndTimes[eventType.slug][selectedDate as string]
|
||||
? selectedDatesAndTimes[eventType.slug][
|
||||
selectedDate as string
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
handleSlotClick={handleSlotClick}
|
||||
@@ -423,7 +480,9 @@ const EmailEmbed = ({
|
||||
<div className="text-default mb-[9px] text-sm">{t("duration")}</div>
|
||||
{durationsOptions.length > 0 ? (
|
||||
<Select<{ label: string; value: number }>
|
||||
value={durationsOptions.find((option) => option.value === selectedDuration)}
|
||||
value={durationsOptions.find(
|
||||
(option) => option.value === selectedDuration
|
||||
)}
|
||||
options={durationsOptions}
|
||||
onChange={(option) => {
|
||||
setSelectedDuration(option?.value);
|
||||
@@ -445,7 +504,11 @@ const EmailEmbed = ({
|
||||
<Collapsible open>
|
||||
<CollapsibleContent>
|
||||
<div className="text-default mb-[9px] text-sm">{t("timezone")}</div>
|
||||
<TimezoneSelect id="timezone" value={timezone} onChange={({ value }) => setTimezone(value)} />
|
||||
<TimezoneSelect
|
||||
id="timezone"
|
||||
value={timezone}
|
||||
onChange={({ value }) => setTimezone(value)}
|
||||
/>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
@@ -474,7 +537,8 @@ const EmailEmbedPreview = ({
|
||||
userSettingsTimezone?: string;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const { timeFormat, timezoneFromBookerStore, timezoneFromTimePreferences } = useBookerTime();
|
||||
const { timeFormat, timezoneFromBookerStore, timezoneFromTimePreferences } =
|
||||
useBookerTime();
|
||||
const timezone = chooseTimezone({
|
||||
timezoneFromBookerStore,
|
||||
timezoneFromTimePreferences,
|
||||
@@ -498,7 +562,8 @@ const EmailEmbedPreview = ({
|
||||
overflowY: "auto",
|
||||
backgroundColor: "white",
|
||||
}}
|
||||
ref={emailContentRef}>
|
||||
ref={emailContentRef}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontStyle: "normal",
|
||||
@@ -507,7 +572,8 @@ const EmailEmbedPreview = ({
|
||||
lineHeight: "19px",
|
||||
marginTop: "15px",
|
||||
marginBottom: "15px",
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<b style={{ color: "black" }}> {eventType.title}</b>
|
||||
</div>
|
||||
<div
|
||||
@@ -517,8 +583,10 @@ const EmailEmbedPreview = ({
|
||||
fontSize: "14px",
|
||||
lineHeight: "17px",
|
||||
color: "#333333",
|
||||
}}>
|
||||
{t("duration")}: <b style={{ color: "black" }}>{selectedDuration} mins</b>
|
||||
}}
|
||||
>
|
||||
{t("duration")}:{" "}
|
||||
<b style={{ color: "black" }}>{selectedDuration} mins</b>
|
||||
</div>
|
||||
<div>
|
||||
<b style={{ color: "black" }}>
|
||||
@@ -529,7 +597,8 @@ const EmailEmbedPreview = ({
|
||||
fontSize: "14px",
|
||||
lineHeight: "17px",
|
||||
color: "#333333",
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
{t("timezone")}: <b style={{ color: "black" }}>{timezone}</b>
|
||||
</span>
|
||||
</b>
|
||||
@@ -553,10 +622,13 @@ const EmailEmbedPreview = ({
|
||||
textAlign: "left",
|
||||
borderCollapse: "collapse",
|
||||
borderSpacing: "0px",
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ textAlign: "left", marginTop: "16px" }}>
|
||||
<td
|
||||
style={{ textAlign: "left", marginTop: "16px" }}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
@@ -564,7 +636,8 @@ const EmailEmbedPreview = ({
|
||||
paddingBottom: "8px",
|
||||
color: "rgb(26, 26, 26)",
|
||||
fontWeight: "bold",
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
{selectedDate}
|
||||
|
||||
</span>
|
||||
@@ -572,15 +645,24 @@ const EmailEmbedPreview = ({
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<table style={{ borderCollapse: "separate", borderSpacing: "0px 4px" }}>
|
||||
<table
|
||||
style={{
|
||||
borderCollapse: "separate",
|
||||
borderSpacing: "0px 4px",
|
||||
}}
|
||||
>
|
||||
<tbody>
|
||||
<tr style={{ height: "25px" }}>
|
||||
{sortedTimes?.length > 0 &&
|
||||
sortedTimes.map((time) => {
|
||||
// If teamId is present on eventType and is not null, it means it is a team event.
|
||||
// So we add 'team/' to the url.
|
||||
const bookingURL = `${eventType.bookerUrl}/${
|
||||
eventType.teamId !== null ? "team/" : ""
|
||||
const bookingURL = `${
|
||||
eventType.bookerUrl
|
||||
}/${
|
||||
eventType.teamId !== null
|
||||
? "team/"
|
||||
: ""
|
||||
}${username}/${
|
||||
eventType.slug
|
||||
}?duration=${selectedDuration}&date=${key}&month=${month}&slot=${time}&cal.tz=${timezone}`;
|
||||
@@ -596,34 +678,45 @@ const EmailEmbedPreview = ({
|
||||
height: "24px",
|
||||
border: "1px solid #111827",
|
||||
borderRadius: "3px",
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<table style={{ height: "21px" }}>
|
||||
<tbody>
|
||||
<tr style={{ height: "21px" }}>
|
||||
<td style={{ width: "7px" }} />
|
||||
<td
|
||||
style={{ width: "7px" }}
|
||||
/>
|
||||
<td
|
||||
style={{
|
||||
width: "50px",
|
||||
textAlign: "center",
|
||||
marginRight: "1px",
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<a
|
||||
href={bookingURL}
|
||||
className="spot"
|
||||
style={{
|
||||
fontFamily: '"Proxima Nova", sans-serif',
|
||||
fontFamily:
|
||||
'"Proxima Nova", sans-serif',
|
||||
textDecoration: "none",
|
||||
textAlign: "center",
|
||||
color: "#111827",
|
||||
fontSize: "12px",
|
||||
lineHeight: "16px",
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<b
|
||||
style={{
|
||||
fontWeight: "normal",
|
||||
textDecoration: "none",
|
||||
}}>
|
||||
{dayjs.utc(time).tz(timezone).format(timeFormat)}
|
||||
textDecoration:
|
||||
"none",
|
||||
}}
|
||||
>
|
||||
{dayjs
|
||||
.utc(time)
|
||||
.tz(timezone)
|
||||
.format(timeFormat)}
|
||||
|
||||
</b>
|
||||
</a>
|
||||
@@ -652,7 +745,8 @@ const EmailEmbedPreview = ({
|
||||
textDecoration: "none",
|
||||
cursor: "pointer",
|
||||
color: "black",
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
{t("see_all_available_times")}
|
||||
</a>
|
||||
</div>
|
||||
@@ -664,7 +758,8 @@ const EmailEmbedPreview = ({
|
||||
borderTop: "1px solid #CCCCCC",
|
||||
marginTop: "8px",
|
||||
paddingTop: "8px",
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<span>{t("powered_by")}</span>{" "}
|
||||
<b style={{ color: "black" }}>
|
||||
<span> Cal.com</span>
|
||||
@@ -695,7 +790,8 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
const { t } = useLocale();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const pathname = usePathname();
|
||||
const { resetState, gotoState, gotoEmbedTypeSelectionState } = useEmbedGoto(noQueryParamMode);
|
||||
const { resetState, gotoState, gotoEmbedTypeSelectionState } =
|
||||
useEmbedGoto(noQueryParamMode);
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
const dialogContentRef = useRef<HTMLDivElement>(null);
|
||||
const emailContentRef = useRef<HTMLDivElement>(null);
|
||||
@@ -712,7 +808,10 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
const calLink = decodeURIComponent(embedUrl);
|
||||
const { data: eventTypeData } = trpc.viewer.eventTypes.get.useQuery(
|
||||
{ id: parsedEventId },
|
||||
{ enabled: !Number.isNaN(parsedEventId) && embedType === "email", refetchOnWindowFocus: false }
|
||||
{
|
||||
enabled: !Number.isNaN(parsedEventId) && embedType === "email",
|
||||
refetchOnWindowFocus: false,
|
||||
}
|
||||
);
|
||||
const { data: userSettings } = trpc.viewer.me.get.useQuery();
|
||||
|
||||
@@ -743,7 +842,10 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
}),
|
||||
};
|
||||
});
|
||||
const embedCodeRefs: Record<(typeof tabs)[0]["name"], RefObject<HTMLTextAreaElement>> = {};
|
||||
const embedCodeRefs: Record<
|
||||
(typeof tabs)[0]["name"],
|
||||
RefObject<HTMLTextAreaElement>
|
||||
> = {};
|
||||
tabs
|
||||
.filter((tab) => tab.type === "code")
|
||||
.forEach((codeTab) => {
|
||||
@@ -752,10 +854,14 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
|
||||
const refOfEmbedCodesRefs = useRef(embedCodeRefs);
|
||||
const embed = types.find((embed) => embed.type === embedType);
|
||||
const [selectedDuration, setSelectedDuration] = useState(eventTypeData?.eventType.length);
|
||||
const [selectedDuration, setSelectedDuration] = useState(
|
||||
eventTypeData?.eventType.length
|
||||
);
|
||||
|
||||
const [isEmbedCustomizationOpen, setIsEmbedCustomizationOpen] = useState(true);
|
||||
const [isBookingCustomizationOpen, setIsBookingCustomizationOpen] = useState(true);
|
||||
const [isEmbedCustomizationOpen, setIsEmbedCustomizationOpen] =
|
||||
useState(true);
|
||||
const [isBookingCustomizationOpen, setIsBookingCustomizationOpen] =
|
||||
useState(true);
|
||||
const defaultConfig = {
|
||||
layout: BookerLayouts.MONTH_VIEW,
|
||||
};
|
||||
@@ -832,7 +938,13 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
);
|
||||
};
|
||||
|
||||
const inlineEmbedDimensionUpdate = ({ width, height }: { width: string; height: string }) => {
|
||||
const inlineEmbedDimensionUpdate = ({
|
||||
width,
|
||||
height,
|
||||
}: {
|
||||
width: string;
|
||||
height: string;
|
||||
}) => {
|
||||
iframeRef.current?.contentWindow?.postMessage(
|
||||
{
|
||||
mode: "cal:preview",
|
||||
@@ -924,18 +1036,22 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
enableOverflow
|
||||
ref={dialogContentRef}
|
||||
className="rounded-lg p-0.5 sm:max-w-7xl!"
|
||||
type="creation">
|
||||
type="creation"
|
||||
>
|
||||
<div className="flex">
|
||||
<div className="bg-cal-muted flex h-[95vh] w-1/3 flex-col overflow-y-auto p-8">
|
||||
<h3
|
||||
className="text-emphasis mb-2.5 flex items-center text-xl font-semibold leading-5"
|
||||
id="modal-title">
|
||||
id="modal-title"
|
||||
>
|
||||
<button className="h-6 w-6" onClick={gotoEmbedTypeSelectionState}>
|
||||
<Icon name="arrow-left" className="mr-4 w-4" />
|
||||
</button>
|
||||
{embed.title}
|
||||
</h3>
|
||||
<h4 className="text-subtle mb-6 text-sm font-normal">{embed.subtitle}</h4>
|
||||
<h4 className="text-subtle mb-6 text-sm font-normal">
|
||||
{embed.subtitle}
|
||||
</h4>
|
||||
{eventTypeData?.eventType && embedType === "email" ? (
|
||||
<EmailEmbed
|
||||
eventType={eventTypeData?.eventType}
|
||||
@@ -948,68 +1064,82 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col">
|
||||
<div className={classNames("font-medium", embedType === "element-click" ? "hidden" : "")}>
|
||||
<div
|
||||
className={classNames(
|
||||
"font-medium",
|
||||
embedType === "element-click" ? "hidden" : ""
|
||||
)}
|
||||
>
|
||||
<Collapsible
|
||||
open={isEmbedCustomizationOpen}
|
||||
onOpenChange={() => setIsEmbedCustomizationOpen((val) => !val)}>
|
||||
onOpenChange={() =>
|
||||
setIsEmbedCustomizationOpen((val) => !val)
|
||||
}
|
||||
>
|
||||
<CollapsibleContent className="text-sm">
|
||||
{/* Conditionally render Window Sizing only if inline embed AND NOT React Atom */}
|
||||
{embedType === "inline" && embedParams.embedTabName !== EmbedTabName.ATOM_REACT && (
|
||||
<div>
|
||||
{/*TODO: Add Auto/Fixed toggle from Figma */}
|
||||
<div className="text-default mb-[9px] text-sm">Window sizing</div>
|
||||
<div className="justify-left mb-6 flex items-center font-normal! ">
|
||||
<div className="mr-[9px]">
|
||||
{embedType === "inline" &&
|
||||
embedParams.embedTabName !== EmbedTabName.ATOM_REACT && (
|
||||
<div>
|
||||
{/*TODO: Add Auto/Fixed toggle from Figma */}
|
||||
<div className="text-default mb-[9px] text-sm">
|
||||
Window sizing
|
||||
</div>
|
||||
<div className="justify-left mb-6 flex items-center font-normal! ">
|
||||
<div className="mr-[9px]">
|
||||
<TextField
|
||||
labelProps={{ className: "hidden" }}
|
||||
className="focus:ring-offset-0"
|
||||
required
|
||||
value={previewState.inline.width}
|
||||
onChange={(e) => {
|
||||
setPreviewState((previewState) => {
|
||||
const width = e.target.value || "100%";
|
||||
|
||||
return {
|
||||
...previewState,
|
||||
inline: {
|
||||
...previewState.inline,
|
||||
width,
|
||||
},
|
||||
};
|
||||
});
|
||||
}}
|
||||
addOnLeading={<>W</>}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TextField
|
||||
labelProps={{ className: "hidden" }}
|
||||
className="focus:ring-offset-0"
|
||||
value={previewState.inline.height}
|
||||
required
|
||||
value={previewState.inline.width}
|
||||
onChange={(e) => {
|
||||
setPreviewState((previewState) => {
|
||||
const width = e.target.value || "100%";
|
||||
const height = e.target.value || "100%";
|
||||
|
||||
setPreviewState((previewState) => {
|
||||
return {
|
||||
...previewState,
|
||||
inline: {
|
||||
...previewState.inline,
|
||||
width,
|
||||
height,
|
||||
},
|
||||
};
|
||||
});
|
||||
}}
|
||||
addOnLeading={<>W</>}
|
||||
addOnLeading={<>H</>}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TextField
|
||||
labelProps={{ className: "hidden" }}
|
||||
className="focus:ring-offset-0"
|
||||
value={previewState.inline.height}
|
||||
required
|
||||
onChange={(e) => {
|
||||
const height = e.target.value || "100%";
|
||||
|
||||
setPreviewState((previewState) => {
|
||||
return {
|
||||
...previewState,
|
||||
inline: {
|
||||
...previewState.inline,
|
||||
height,
|
||||
},
|
||||
};
|
||||
});
|
||||
}}
|
||||
addOnLeading={<>H</>}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
<div
|
||||
className={classNames(
|
||||
"items-center justify-between",
|
||||
embedType === "floating-popup" ? "text-emphasis" : "hidden"
|
||||
)}>
|
||||
embedType === "floating-popup"
|
||||
? "text-emphasis"
|
||||
: "hidden"
|
||||
)}
|
||||
>
|
||||
<div className="mb-2 text-sm">Button text</div>
|
||||
{/* Default Values should come from preview iframe */}
|
||||
<TextField
|
||||
@@ -1035,7 +1165,8 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
embedType === "floating-popup"
|
||||
? "text-emphasis space-x-2 rtl:space-x-reverse"
|
||||
: "hidden"
|
||||
)}>
|
||||
)}
|
||||
>
|
||||
<Switch
|
||||
defaultChecked={true}
|
||||
onCheckedChange={(checked) => {
|
||||
@@ -1050,13 +1181,18 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<div className="text-default my-2 text-sm">Display calendar icon</div>
|
||||
<div className="text-default my-2 text-sm">
|
||||
Display calendar icon
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
"mt-4 items-center justify-between",
|
||||
embedType === "floating-popup" ? "text-emphasis" : "hidden"
|
||||
)}>
|
||||
embedType === "floating-popup"
|
||||
? "text-emphasis"
|
||||
: "hidden"
|
||||
)}
|
||||
>
|
||||
<div className="mb-2">Position of button</div>
|
||||
<Select
|
||||
onChange={(position) => {
|
||||
@@ -1075,7 +1211,12 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-col xl:flex-row xl:justify-between">
|
||||
<div className={classNames("mt-4", embedType === "floating-popup" ? "" : "hidden")}>
|
||||
<div
|
||||
className={classNames(
|
||||
"mt-4",
|
||||
embedType === "floating-popup" ? "" : "hidden"
|
||||
)}
|
||||
>
|
||||
<div className="whitespace-nowrap">Button color</div>
|
||||
<div className="mt-2 w-40 xl:mt-0 xl:w-full">
|
||||
<ColorPicker
|
||||
@@ -1097,7 +1238,12 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classNames("mt-4", embedType === "floating-popup" ? "" : "hidden")}>
|
||||
<div
|
||||
className={classNames(
|
||||
"mt-4",
|
||||
embedType === "floating-popup" ? "" : "hidden"
|
||||
)}
|
||||
>
|
||||
<div className="whitespace-nowrap">Text color</div>
|
||||
<div className="mb-6 mt-2 w-40 xl:mt-0 xl:w-full">
|
||||
<ColorPicker
|
||||
@@ -1126,13 +1272,16 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
<div className="font-medium">
|
||||
<Collapsible
|
||||
open={isBookingCustomizationOpen}
|
||||
onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}>
|
||||
onOpenChange={() =>
|
||||
setIsBookingCustomizationOpen((val) => !val)
|
||||
}
|
||||
>
|
||||
<CollapsibleContent>
|
||||
<div className="text-sm">
|
||||
{/* Conditionally render EmbedTheme only if NOT React Atom */}
|
||||
{embedParams.embedTabName !== EmbedTabName.ATOM_REACT && (
|
||||
<Label className="mb-6">
|
||||
<div className="mb-2">EmbedTheme</div>
|
||||
<div className="mb-2">Embed theme</div>
|
||||
<Select
|
||||
className="w-full"
|
||||
defaultValue={ThemeOptions[0]}
|
||||
@@ -1146,7 +1295,9 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
}
|
||||
setPreviewState((previewState) => {
|
||||
// Ensure theme is updated in config for all embed types
|
||||
const newConfig = (currentConfig?: EmbedConfig) => ({
|
||||
const newConfig = (
|
||||
currentConfig?: EmbedConfig
|
||||
) => ({
|
||||
...(currentConfig ?? {}),
|
||||
theme: option.value,
|
||||
});
|
||||
@@ -1154,15 +1305,21 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
...previewState,
|
||||
inline: {
|
||||
...previewState.inline,
|
||||
config: newConfig(previewState.inline.config),
|
||||
config: newConfig(
|
||||
previewState.inline.config
|
||||
),
|
||||
},
|
||||
floatingPopup: {
|
||||
...previewState.floatingPopup,
|
||||
config: newConfig(previewState.floatingPopup.config),
|
||||
config: newConfig(
|
||||
previewState.floatingPopup.config
|
||||
),
|
||||
},
|
||||
elementClick: {
|
||||
...previewState.elementClick,
|
||||
config: newConfig(previewState.elementClick.config),
|
||||
config: newConfig(
|
||||
previewState.elementClick.config
|
||||
),
|
||||
},
|
||||
// Keep updating top-level theme for preview iframe
|
||||
theme: option.value,
|
||||
@@ -1188,7 +1345,9 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<div className="text-default text-sm">{t("hide_eventtype_details")}</div>
|
||||
<div className="text-default text-sm">
|
||||
{t("hide_eventtype_details")}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{/* Conditionally render Brand Colors only if NOT React Atom */}
|
||||
@@ -1207,11 +1366,14 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
<div className="w-full">
|
||||
<ColorPicker
|
||||
popoverAlign="start"
|
||||
container={dialogContentRef?.current ?? undefined}
|
||||
container={
|
||||
dialogContentRef?.current ?? undefined
|
||||
}
|
||||
defaultValue={paletteDefaultValue(palette.name)}
|
||||
onChange={(color) => {
|
||||
addToPalette({
|
||||
[palette.name as keyof (typeof previewState)["palette"]]: color,
|
||||
[palette.name as keyof (typeof previewState)["palette"]]:
|
||||
color,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
@@ -1229,7 +1391,9 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
}
|
||||
setPreviewState((previewState) => {
|
||||
// Ensure layout is updated in config for all embed types
|
||||
const newConfig = (currentConfig?: EmbedConfig) => ({
|
||||
const newConfig = (
|
||||
currentConfig?: EmbedConfig
|
||||
) => ({
|
||||
...(currentConfig ?? {}),
|
||||
layout: option.value,
|
||||
});
|
||||
@@ -1241,11 +1405,15 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
},
|
||||
floatingPopup: {
|
||||
...previewState.floatingPopup,
|
||||
config: newConfig(previewState.floatingPopup.config),
|
||||
config: newConfig(
|
||||
previewState.floatingPopup.config
|
||||
),
|
||||
},
|
||||
elementClick: {
|
||||
...previewState.elementClick,
|
||||
config: newConfig(previewState.elementClick.config),
|
||||
config: newConfig(
|
||||
previewState.elementClick.config
|
||||
),
|
||||
},
|
||||
// Keep updating top-level layout for preview iframe
|
||||
layout: option.value,
|
||||
@@ -1281,8 +1449,11 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
<div
|
||||
key={tab.href}
|
||||
className={classNames(
|
||||
embedParams.embedTabName === tab.href.split("=")[1] ? "flex-1" : "hidden"
|
||||
)}>
|
||||
embedParams.embedTabName === tab.href.split("=")[1]
|
||||
? "flex-1"
|
||||
: "hidden"
|
||||
)}
|
||||
>
|
||||
{tab.type === "code" && (
|
||||
<tab.Component
|
||||
namespace={namespace}
|
||||
@@ -1293,16 +1464,27 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={embedParams.embedTabName === "embed-preview" ? "mt-2 block" : "hidden"}
|
||||
className={
|
||||
embedParams.embedTabName === "embed-preview"
|
||||
? "mt-2 block"
|
||||
: "hidden"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (embedType === "email" && (tab.name !== "Preview" || !eventTypeData?.eventType)) return;
|
||||
if (
|
||||
embedType === "email" &&
|
||||
(tab.name !== "Preview" || !eventTypeData?.eventType)
|
||||
)
|
||||
return;
|
||||
|
||||
return (
|
||||
<div key={tab.href} className={classNames("flex grow flex-col")}>
|
||||
<div
|
||||
key={tab.href}
|
||||
className={classNames("flex grow flex-col")}
|
||||
>
|
||||
<div className="flex h-[55vh] grow flex-col">
|
||||
<EmailEmbedPreview
|
||||
selectedDuration={selectedDuration}
|
||||
@@ -1314,12 +1496,20 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
month={month as string}
|
||||
selectedDateAndTime={
|
||||
selectedDatesAndTimes
|
||||
? selectedDatesAndTimes[eventTypeData?.eventType.slug as string]
|
||||
? selectedDatesAndTimes[
|
||||
eventTypeData?.eventType.slug as string
|
||||
]
|
||||
: {}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className={embedParams.embedTabName === "embed-preview" ? "mt-2 block" : "hidden"} />
|
||||
<div
|
||||
className={
|
||||
embedParams.embedTabName === "embed-preview"
|
||||
? "mt-2 block"
|
||||
: "hidden"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -1336,7 +1526,10 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter className="mt-10 flex-row-reverse gap-x-2" showDivider>
|
||||
<DialogFooter
|
||||
className="mt-10 flex-row-reverse gap-x-2"
|
||||
showDivider
|
||||
>
|
||||
<DialogClose />
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -1349,14 +1542,16 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
(tab) => tab.href === `embedTabName=${currentTabHref}`
|
||||
)?.name;
|
||||
if (!currentTabName) return;
|
||||
const currentTabCodeEl = refOfEmbedCodesRefs.current[currentTabName].current;
|
||||
const currentTabCodeEl =
|
||||
refOfEmbedCodesRefs.current[currentTabName].current;
|
||||
if (!currentTabCodeEl) {
|
||||
return;
|
||||
}
|
||||
navigator.clipboard.writeText(currentTabCodeEl.value);
|
||||
showToast(t("code_copied"), "success");
|
||||
}
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
{embedType === "email" ? t("copy") : t("copy_code")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
@@ -1395,9 +1590,13 @@ export const EmbedDialog = ({
|
||||
// Must not set name when noQueryParam mode as required by Dialog component
|
||||
name: "embed",
|
||||
clearQueryParamsOnClose: queryParamsForDialog,
|
||||
})}>
|
||||
})}
|
||||
>
|
||||
{!embedParams.embedType ? (
|
||||
<ChooseEmbedTypesDialogContent types={types} noQueryParamMode={noQueryParamMode} />
|
||||
<ChooseEmbedTypesDialogContent
|
||||
types={types}
|
||||
noQueryParamMode={noQueryParamMode}
|
||||
/>
|
||||
) : (
|
||||
<EmbedTypeCodeAndPreviewDialogContent
|
||||
embedType={embedParams.embedType as EmbedType}
|
||||
@@ -1455,7 +1654,8 @@ export const EmbedButton = <T extends React.ElementType = typeof Button>({
|
||||
data-test-embed-url={embedUrl}
|
||||
data-testid="embed"
|
||||
type="button"
|
||||
onClick={openEmbedModal}>
|
||||
onClick={openEmbedModal}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user