Compare commits

...
7 Commits
Author SHA1 Message Date
Alex van Andel 0d03006653 v2.3.3
release.yml / release (push) Canceled after 0s
2022-11-29 09:40:40 +00:00
Alex van Andel d44c5fd8b0 Fixes availability to reset to default on save non-team (#5710) 2022-11-29 09:36:05 +00:00
checkly[bot] 74aa366f1e [Checkly] adds "Location Link" code (#5752)
Co-authored-by: checkly[bot] <54914329+checkly[bot]@users.noreply.github.com>
2022-11-28 16:52:24 -07:00
sean-brydon 7ebea57602 Dialog Api improvements (#5714) 2022-11-28 19:14:38 +00:00
26d675e162 Allow clients to choose different duration for a meeting (#5660)
* WIP

* Missing translation

* Default duration clears correctly

* Fixing hydration error

* Applying feedback

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-11-28 18:14:01 +00:00
Jeroen Reumkensandkodiakhq[bot] e16d29e916 Added optional chaining to embed iframe using window object, where objects were potentially undefined. (#5727)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-11-28 09:54:14 -07:00
zomars 0e89fb9f6a Update release.yml 2022-11-28 09:07:30 -07:00
53 changed files with 677 additions and 502 deletions
+1
View File
@@ -11,6 +11,7 @@ jobs:
steps:
- uses: actions/checkout@v3
with:
ref: main # Always checkout main even for tagged releases
fetch-depth: 0
token: ${{ secrets.GH_ACCESS_TOKEN }}
- run: git push origin +main:production
+35
View File
@@ -0,0 +1,35 @@
/**
* This is a Playwright script Checkly generated for you based on your Vercel project.
* To learn more about Browser checks and Playwright visit: https://www.checklyhq.com/docs/browser-checks
*/
// Create a Chromium browser
const { chromium } = require("playwright");
// Checkly supports top level await, but we wrap your code in an async function so you can run it locally too.
async function run() {
const browser = await chromium.launch();
const page = await browser.newPage();
const targetUrl = process.env.ENVIRONMENT_URL || "https://cal.com";
await page.goto(`${targetUrl}/rick/test-location-link`);
await page.waitForSelector('[data-testid="day"][data-disabled="false"]');
await page.click('[data-testid="day"][data-disabled="false"]');
await page.waitForSelector('[data-testid="time"]');
await page.click('[data-testid="time"]');
await page.waitForSelector("#name");
await page.click("#name");
await page.type("#name", "Calcom");
await page.type('[name="email"]', "cal@cal.com");
await page.waitForSelector('[data-testid="confirm-book-button"]');
await page.click('[data-testid="confirm-book-button"]');
await browser.close();
}
run();
+3 -5
View File
@@ -619,7 +619,7 @@ const ChooseEmbedTypesDialogContent = () => {
const { t } = useLocale();
const router = useRouter();
return (
<DialogContent type="creation" useOwnActionButtons size="lg">
<DialogContent type="creation" size="lg">
<div className="mb-4">
<h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title">
{t("how_you_want_add_cal_site")}
@@ -797,7 +797,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
];
return (
<DialogContent size="xl" className="p-0.5" type="creation" useOwnActionButtons>
<DialogContent size="xl" className="p-0.5" type="creation">
<div className="flex">
<div className="flex w-1/3 flex-col bg-gray-50 p-8">
<h3 className="mb-2 flex text-xl font-bold leading-6 text-gray-900" id="modal-title">
@@ -1095,9 +1095,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
{t("copy_code")}
</Button>
) : null}
<DialogClose asChild>
<Button color="secondary">{t("Close")}</Button>
</DialogClose>
<DialogClose />
</div>
</div>
);
+2 -6
View File
@@ -156,12 +156,8 @@ export default function ImageUploader({
</div>
</div>
<div className="mt-5 gap-x-2 sm:mt-4 sm:flex sm:flex-row-reverse">
<DialogClose asChild>
<Button onClick={() => showCroppedImage(croppedAreaPixels)}>{t("save")}</Button>
</DialogClose>
<DialogClose asChild>
<Button color="secondary">{t("cancel")}</Button>
</DialogClose>
<DialogClose onClick={() => showCroppedImage(croppedAreaPixels)}>{t("save")}</DialogClose>
<DialogClose color="secondary">{t("cancel")}</DialogClose>
</div>
</DialogContent>
</Dialog>
@@ -1,9 +1,11 @@
import { SchedulingType } from "@prisma/client";
import { FC, ReactNode } from "react";
import { FC, ReactNode, useEffect } from "react";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Icon } from "@calcom/ui";
import { Icon, Badge } from "@calcom/ui";
import useRouterQuery from "@lib/hooks/useRouterQuery";
import { UserAvatars } from "@components/booking/UserAvatars";
import EventTypeDescriptionSafeHTML from "@components/eventtype/EventTypeDescriptionSafeHTML";
@@ -40,6 +42,18 @@ interface Props {
const BookingDescription: FC<Props> = (props) => {
const { profile, eventType, isBookingPage = false, children } = props;
const { t } = useLocale();
const { duration, setQuery: setDuration } = useRouterQuery("duration");
useEffect(() => {
if (eventType.metadata?.multipleDuration !== undefined) {
if (!duration) {
setDuration(eventType.length);
} else {
if (!eventType.metadata?.multipleDuration.includes(Number(duration))) {
setDuration(eventType.length);
}
}
}
}, []);
return (
<>
<UserAvatars
@@ -90,14 +104,46 @@ const BookingDescription: FC<Props> = (props) => {
<AvailableEventLocations
locations={eventType.locations as AvailabilityPageProps["eventType"]["locations"]}
/>
<p
<div
className={classNames(
"text-sm font-medium",
"flex flex-nowrap text-sm font-medium",
isBookingPage && "dark:text-darkgray-600 text-gray-600"
)}>
<Icon.FiClock className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4" />
{eventType.length} {t("minutes")}
</p>
<Icon.FiClock
className={classNames(
"min-h-4 min-w-4 mr-[10px] ml-[2px] inline-block",
isBookingPage && "mt-[2px]"
)}
/>
{eventType.metadata?.multipleDuration !== undefined ? (
!isBookingPage ? (
<ul className="-mt-1 flex flex-wrap gap-1">
{eventType.metadata.multipleDuration.map((dur, idx) => (
<li key={idx}>
<Badge
variant="gray"
size="lg"
className={classNames(
duration === dur.toString()
? "bg-darkgray-200 text-darkgray-900 dark:bg-darkmodebrand dark:!text-darkmodebrandcontrast"
: "hover:bg-darkgray-200 dark:hover:bg-darkmodebrand hover:text-darkgray-900 dark:hover:text-darkmodebrandcontrast dark:bg-darkgray-200 bg-gray-200 text-gray-900 dark:text-white",
"cursor-pointer"
)}
onClick={() => {
setDuration(dur);
}}>
{dur} {t("minute_timeUnit")}
</Badge>
</li>
))}
</ul>
) : (
`${duration} ${t("minutes")}`
)
) : (
`${eventType.length} ${t("minutes")}`
)}
</div>
{children}
</div>
</>
@@ -237,9 +237,7 @@ function BookingListItem(booking: BookingItemProps) {
/>
<DialogFooter>
<DialogClose>
<Button color="secondary">{t("cancel")}</Button>
</DialogClose>
<DialogClose />
<Button
disabled={mutation.isLoading}
@@ -27,12 +27,14 @@ import notEmpty from "@calcom/lib/notEmpty";
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { trpc } from "@calcom/trpc/react";
import { Icon, DatePicker } from "@calcom/ui";
import { timeZone as localStorageTimeZone } from "@lib/clock";
// import { timeZone } from "@lib/clock";
import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally";
import useRouterQuery from "@lib/hooks/useRouterQuery";
import { isBrandingHidden } from "@lib/isBrandingHidden";
import Gates, { Gate, GateState } from "@components/Gates";
@@ -70,6 +72,7 @@ const useSlots = ({
endTime,
usernameList,
timeZone,
duration,
}: {
eventTypeId: number;
eventTypeSlug: string;
@@ -77,6 +80,7 @@ const useSlots = ({
endTime?: Dayjs;
usernameList: string[];
timeZone?: string;
duration?: string;
}) => {
const { data, isLoading, isPaused } = trpc.viewer.public.slots.getSchedule.useQuery(
{
@@ -86,6 +90,7 @@ const useSlots = ({
startTime: startTime?.toISOString() || "",
endTime: endTime?.toISOString() || "",
timeZone,
duration,
},
{
enabled: !!startTime && !!endTime,
@@ -126,6 +131,7 @@ const SlotPicker = ({
}) => {
const [selectedDate, setSelectedDate] = useState<Dayjs>();
const [browsingDate, setBrowsingDate] = useState<Dayjs>();
const { duration } = useRouterQuery("duration");
const { date, setQuery: setDate } = useRouterQuery("date");
const { month, setQuery: setMonth } = useRouterQuery("month");
const router = useRouter();
@@ -151,7 +157,7 @@ const SlotPicker = ({
setSelectedDate(dayjs.tz(date, timeZone));
}
}
}, [router.isReady, month, date, timeZone]);
}, [router.isReady, month, date, duration, timeZone]);
const { i18n, isLocaleReady } = useLocale();
const { slots: _1 } = useSlots({
@@ -161,6 +167,7 @@ const SlotPicker = ({
startTime: selectedDate?.startOf("day"),
endTime: selectedDate?.endOf("day"),
timeZone,
duration,
});
const { slots: _2, isLoading } = useSlots({
eventTypeId: eventType.id,
@@ -169,6 +176,7 @@ const SlotPicker = ({
startTime: browsingDate?.startOf("month"),
endTime: browsingDate?.endOf("month"),
timeZone,
duration,
});
const slots = useMemo(() => ({ ..._2, ..._1 }), [_1, _2]);
@@ -230,13 +238,13 @@ function TimezoneDropdown({
return (
<Popover.Root open={isTimeOptionsOpen} onOpenChange={setIsTimeOptionsOpen}>
<Popover.Trigger className="min-w-32 dark:text-darkgray-600 radix-state-open:bg-gray-200 dark:radix-state-open:bg-darkgray-200 group relative mb-2 -ml-2 inline-block rounded-md px-2 py-2 text-left text-gray-600">
<p className="text-sm font-medium">
<Icon.FiGlobe className="mr-[10px] ml-[2px] -mt-[2px] inline-block h-4 w-4" />
<p className="flex items-center text-sm font-medium">
<Icon.FiGlobe className="min-h-4 min-w-4 mr-[10px] ml-[2px] -mt-[2px] inline-block" />
{timeZone}
{isTimeOptionsOpen ? (
<Icon.FiChevronUp className="ml-1 inline-block h-4 w-4" />
<Icon.FiChevronUp className="min-h-4 min-w-4 ml-1 inline-block" />
) : (
<Icon.FiChevronDown className="ml-1 inline-block h-4 w-4" />
<Icon.FiChevronDown className="min-h-4 min-w-4 ml-1 inline-block" />
)}
</p>
</Popover.Trigger>
@@ -258,23 +266,6 @@ const dateQuerySchema = z.object({
timeZone: z.string().optional().default(""),
});
const useRouterQuery = <T extends string>(name: T) => {
const router = useRouter();
const existingQueryParams = router.asPath.split("?")[1];
const urlParams = new URLSearchParams(existingQueryParams);
const query = Object.fromEntries(urlParams);
const setQuery = (newValue: string | number | null | undefined) => {
router.replace({ query: { ...router.query, [name]: newValue } }, undefined, { shallow: true });
router.replace({ query: { ...router.query, ...query, [name]: newValue } }, undefined, { shallow: true });
};
return { [name]: query[name], setQuery } as {
[K in T]: string | undefined;
} & { setQuery: typeof setQuery };
};
export type Props = AvailabilityTeamPageProps | AvailabilityPageProps | DynamicAvailabilityPageProps;
const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
@@ -43,6 +43,7 @@ import { asStringOrNull } from "@lib/asStringOrNull";
import { timeZone } from "@lib/clock";
import { ensureArray } from "@lib/ensureArray";
import useMeQuery from "@lib/hooks/useMeQuery";
import useRouterQuery from "@lib/hooks/useRouterQuery";
import createBooking from "@lib/mutations/bookings/create-booking";
import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking";
import { parseDate, parseRecurringDates } from "@lib/parseDate";
@@ -85,9 +86,7 @@ const BookingPage = ({
...restProps
}: BookingPageProps) => {
const { t, i18n } = useLocale();
// Get user so we can determine 12/24 hour format preferences
const query = useMeQuery();
const user = query.data;
const { duration: queryDuration } = useRouterQuery("duration");
const isEmbed = useIsEmbed(restProps.isEmbed);
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
@@ -104,6 +103,12 @@ const BookingPage = ({
);
const stripeAppData = getStripeAppData(eventType);
// Define duration now that we support multiple duration eventTypes
let duration = eventType.length;
if (queryDuration && !isNaN(Number(queryDuration))) {
duration = Number(queryDuration);
}
useEffect(() => {
if (top !== window) {
//page_view will be collected automatically by _middleware.ts
@@ -318,7 +323,7 @@ const BookingPage = ({
const recurringBookings = recurringDates.map((recurringDate) => ({
...booking,
start: dayjs(recurringDate).format(),
end: dayjs(recurringDate).add(eventType.length, "minute").format(),
end: dayjs(recurringDate).add(duration, "minute").format(),
eventTypeId: eventType.id,
eventTypeSlug: eventType.slug,
recurringEventId,
@@ -351,7 +356,7 @@ const BookingPage = ({
mutation.mutate({
...booking,
start: dayjs(date).format(),
end: dayjs(date).add(eventType.length, "minute").format(),
end: dayjs(date).add(duration, "minute").format(),
eventTypeId: eventType.id,
eventTypeSlug: eventType.slug,
timeZone: timeZone(),
@@ -839,7 +844,7 @@ const BookingPage = ({
) : (
<textarea
{...bookingForm.register("notes")}
required={!!eventType.metadata.additionalNotesRequired}
required={!!eventType.metadata?.additionalNotesRequired}
id="notes"
name="notes"
rows={3}
@@ -212,7 +212,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
return (
<Dialog open={isOpenDialog}>
<DialogContent useOwnActionButtons>
<DialogContent>
<div className="flex flex-row space-x-3">
<div className="bg-secondary-100 mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10">
<Icon.FiMapPin className="text-primary-600 h-6 w-6" />
+46 -39
View File
@@ -2,7 +2,17 @@ import { Dispatch, SetStateAction, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button, Dialog, DialogContent, Icon, showToast, TextArea } from "@calcom/ui";
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
Icon,
showToast,
TextArea,
} from "@calcom/ui";
interface IRescheduleDialog {
isOpenDialog: boolean;
@@ -30,44 +40,41 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
return (
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
<DialogContent
Icon={Icon.FiClock}
useOwnActionButtons
title={t("send_reschedule_request")}
description={t("reschedule_modal_description")}>
<div>
<p className="text-sm font-medium text-black">
{t("reason_for_reschedule_request")}
<span className="font-normal text-gray-500"> (Optional)</span>
</p>
<TextArea
data-testid="reschedule_reason"
name={t("reschedule_reason")}
value={rescheduleReason}
onChange={(e) => setRescheduleReason(e.target.value)}
className="mt-2"
/>
</div>
<div className="flex justify-end space-x-2">
<Button
onClick={() => {
setRescheduleReason("");
setIsOpenDialog(false);
}}
color="secondary">
{t("cancel")}
</Button>
<Button
data-testid="send_request"
disabled={isLoading}
onClick={() => {
rescheduleApi({
bookingId,
rescheduleReason,
});
}}>
{t("send_reschedule_request")}
</Button>
<DialogContent>
<div className="flex flex-row space-x-3">
<div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
<Icon.FiClock className="m-auto h-6 w-6" />
</div>
<div className="pt-1">
<DialogHeader title={t("send_reschedule_request")} />
<p className="-mt-8 text-sm text-gray-500">{t("reschedule_modal_description")}</p>
<p className="mt-6 mb-2 text-sm font-bold text-black">
{t("reason_for_reschedule_request")}
<span className="font-normal text-gray-500"> (Optional)</span>
</p>
<TextArea
data-testid="reschedule_reason"
name={t("reschedule_reason")}
value={rescheduleReason}
onChange={(e) => setRescheduleReason(e.target.value)}
className="mb-5 sm:mb-6"
/>
<DialogFooter>
<DialogClose />
<Button
data-testid="send_request"
disabled={isLoading}
onClick={() => {
rescheduleApi({
bookingId,
rescheduleReason,
});
}}>
{t("send_reschedule_request")}
</Button>
</DialogFooter>
</div>
</div>
</DialogContent>
</Dialog>
@@ -264,9 +264,7 @@ export default function CreateEventTypeButton(props: CreateEventTypeBtnProps) {
<Button type="submit" loading={createMutation.isLoading}>
{t("continue")}
</Button>
<DialogClose asChild>
<Button color="secondary">{t("cancel")}</Button>
</DialogClose>
<DialogClose />
</div>
</Form>
</DialogContent>
@@ -211,7 +211,6 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
<DialogContent
type="creation"
Icon={Icon.FiMapPin}
useOwnActionButtons
title={t("edit_location")}
description={!booking ? t("this_input_will_shown_booking_this_event") : undefined}>
{booking && (
@@ -15,7 +15,9 @@ import {
Checkbox,
CustomInputItem,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
Icon,
Label,
SettingsToggle,
@@ -352,11 +354,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
<DialogContent
title={t("custom_event_name")}
description={t("custom_event_name_description")}
type="creation"
actionText="Create"
// Set event name back to what it was on close
actionOnClose={() => formMethods.setValue("eventName", eventType.eventName ?? "")}
actionOnClick={() => setShowEventNameTip(false)}>
type="creation">
<TextField
label={t("event_name")}
type="text"
@@ -370,6 +368,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
<p>{`{HOST/ATTENDEE} = ${t("dynamically_display_attendee_or_organizer")}`}</p>
<p>{`{LOCATION} = ${t("event_location")}`}</p>
</div>
<DialogFooter>
<Button color="primary" onClick={() => setShowEventNameTip(false)}>
{t("create")}
</Button>
<DialogClose onClick={() => formMethods.setValue("eventName", eventType.eventName ?? "")} />
</DialogFooter>
</DialogContent>
</Dialog>
)}
@@ -383,7 +387,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
type="creation"
Icon={Icon.FiPlus}
title={t("add_new_custom_input_field")}
useOwnActionButtons
description={t("this_input_will_shown_booking_this_event")}>
<CustomInputTypeForm
selectedCustomInput={selectedCustomInput}
+115 -12
View File
@@ -4,12 +4,13 @@ import { isValidPhoneNumber } from "libphonenumber-js";
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
import { useState } from "react";
import { Controller, useForm, useFormContext } from "react-hook-form";
import { MultiValue } from "react-select";
import { z } from "zod";
import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations";
import { CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Icon, Label, Select, Skeleton, TextField } from "@calcom/ui";
import { Button, Icon, Label, Select, Skeleton, TextField, SettingsToggle } from "@calcom/ui";
import { slugify } from "@lib/slugify";
@@ -30,6 +31,34 @@ export const EventSetupTab = (
const [showLocationModal, setShowLocationModal] = useState(false);
const [editingLocationType, setEditingLocationType] = useState<string>("");
const [selectedLocation, setSelectedLocation] = useState<OptionTypeBase | undefined>(undefined);
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata.multipleDuration);
const multipleDurationOptions = [
{ value: 5, label: t("multiple_duration_mins", { count: 5 }) },
{ value: 10, label: t("multiple_duration_mins", { count: 10 }) },
{ value: 15, label: t("multiple_duration_mins", { count: 15 }) },
{ value: 20, label: t("multiple_duration_mins", { count: 20 }) },
{ value: 25, label: t("multiple_duration_mins", { count: 25 }) },
{ value: 30, label: t("multiple_duration_mins", { count: 30 }) },
{ value: 45, label: t("multiple_duration_mins", { count: 45 }) },
{ value: 50, label: t("multiple_duration_mins", { count: 50 }) },
{ value: 60, label: t("multiple_duration_mins", { count: 60 }) },
{ value: 75, label: t("multiple_duration_mins", { count: 75 }) },
{ value: 80, label: t("multiple_duration_mins", { count: 80 }) },
{ value: 90, label: t("multiple_duration_mins", { count: 90 }) },
{ value: 120, label: t("multiple_duration_mins", { count: 120 }) },
{ value: 180, label: t("multiple_duration_mins", { count: 180 }) },
];
const [selectedMultipleDuration, setSelectedMultipleDuration] = useState<
MultiValue<{
value: number;
label: string;
}>
>(multipleDurationOptions.filter((mdOpt) => multipleDuration?.includes(mdOpt.value)));
const [defaultDuration, setDefaultDuration] = useState(
selectedMultipleDuration.find((opt) => opt.value === eventType.length) ?? null
);
const openLocationModal = (type: EventLocationType["type"]) => {
setSelectedLocation(locationOptions.find((option) => option.value === type));
@@ -219,17 +248,91 @@ export const EventSetupTab = (
setValueAs: (v) => slugify(v),
})}
/>
<TextField
required
name="length"
type="number"
label={t("duration")}
addOnSuffix={<>{t("minutes")}</>}
defaultValue={eventType.length ?? 15}
onChange={(e) => {
formMethods.setValue("length", Number(e.target.value));
}}
/>
{multipleDuration ? (
<div className="space-y-4">
<div>
<Skeleton as={Label} loadingClassName="w-16">
{t("available_durations")}
</Skeleton>
<Select
isMulti
defaultValue={selectedMultipleDuration}
name="metadata.multipleDuration"
isSearchable={false}
className="h-auto !min-h-[36px] text-sm"
options={multipleDurationOptions}
onChange={(options) => {
const values = options
.map((opt) => opt.value)
.sort(function (a, b) {
return a - b;
});
setMultipleDuration(values);
setSelectedMultipleDuration(options);
if (!options.find((opt) => opt.value === defaultDuration?.value)) {
if (options.length > 0) {
setDefaultDuration(options[0]);
} else {
setDefaultDuration(null);
}
}
if (options.length === 1 && defaultDuration === null) {
setDefaultDuration(options[0]);
}
formMethods.setValue("metadata.multipleDuration", values);
}}
/>
</div>
<div>
<Skeleton as={Label} loadingClassName="w-16">
{t("default_duration")}
</Skeleton>
<Select
value={defaultDuration}
isSearchable={false}
name="length"
className="text-sm"
noOptionsMessage={() => t("default_duration_no_options")}
options={selectedMultipleDuration}
onChange={(option) => {
setDefaultDuration(
selectedMultipleDuration.find((opt) => opt.value === option?.value) ?? null
);
formMethods.setValue("length", Number(option?.value));
}}
/>
</div>
</div>
) : (
<TextField
required
name="length"
type="number"
label={t("duration")}
addOnSuffix={<>{t("minutes")}</>}
defaultValue={eventType.length ?? 15}
onChange={(e) => {
formMethods.setValue("length", Number(e.target.value));
}}
/>
)}
<div className="!mt-4 [&_label]:my-1 [&_label]:font-normal">
<SettingsToggle
title={t("allow_booker_to_select_duration")}
checked={multipleDuration !== undefined}
onCheckedChange={() => {
if (multipleDuration !== undefined) {
setMultipleDuration(undefined);
formMethods.setValue("metadata.multipleDuration", undefined);
formMethods.setValue("length", eventType.length);
} else {
setMultipleDuration([]);
formMethods.setValue("metadata.multipleDuration", []);
formMethods.setValue("length", 0);
}
}}
/>
</div>
<div>
<Skeleton as={Label} loadingClassName="w-16">
{t("location")}
@@ -72,11 +72,7 @@ const DisableTwoFactorAuthModal = ({
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
title={t("disable_2fa")}
description={t("disable_2fa_recommendation")}
type="creation"
useOwnActionButtons>
<DialogContent title={t("disable_2fa")} description={t("disable_2fa_recommendation")} type="creation">
<Form form={form} handleSubmit={handleDisable}>
<div className="mb-4">
<PasswordField
@@ -130,11 +130,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
title={t("enable_2fa")}
description={setupDescriptions[step]}
type="creation"
useOwnActionButtons>
<DialogContent title={t("enable_2fa")} description={setupDescriptions[step]} type="creation">
<WithStep step={SetupStep.ConfirmPassword} current={step}>
<form onSubmit={handleSetup}>
<div className="mb-4">
@@ -347,10 +347,8 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
{t("save")}
</Button>
)}
<DialogClose asChild>
<Button color="secondary" onClick={() => setOpenDialogSaveUsername(false)}>
{t("cancel")}
</Button>
<DialogClose color="secondary" onClick={() => setOpenDialogSaveUsername(false)}>
{t("cancel")}
</DialogClose>
</div>
</DialogContent>
@@ -194,10 +194,8 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
{t("save")}
</Button>
<DialogClose asChild>
<Button color="secondary" onClick={() => setOpenDialogSaveUsername(false)}>
{t("cancel")}
</Button>
<DialogClose color="secondary" onClick={() => setOpenDialogSaveUsername(false)}>
{t("cancel")}
</DialogClose>
</div>
</DialogContent>
+18
View File
@@ -0,0 +1,18 @@
import { useRouter } from "next/router";
export default function useRouterQuery<T extends string>(name: T) {
const router = useRouter();
const existingQueryParams = router.asPath.split("?")[1];
const urlParams = new URLSearchParams(existingQueryParams);
const query = Object.fromEntries(urlParams);
const setQuery = (newValue: string | number | null | undefined) => {
router.replace({ query: { ...router.query, [name]: newValue } }, undefined, { shallow: true });
router.replace({ query: { ...router.query, ...query, [name]: newValue } }, undefined, { shallow: true });
};
return { [name]: query[name], setQuery } as {
[K in T]: string | undefined;
} & { setQuery: typeof setQuery };
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "2.3.2",
"version": "2.3.3",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
+1 -1
View File
@@ -224,7 +224,7 @@ async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
const locations = eventType.locations ? (eventType.locations as LocationObject[]) : [];
const eventTypeObject = Object.assign({}, eventType, {
metadata: (eventType.metadata || {}) as JSONObject,
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
locations: privacyFilteredLocations(locations),
users: users.map((user) => {
+2 -1
View File
@@ -12,6 +12,7 @@ import {
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { bookEventTypeSelect } from "@calcom/prisma";
import prisma from "@calcom/prisma";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
import getBooking, { GetBookingType } from "@lib/getBooking";
@@ -113,7 +114,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
const eventType = {
...eventTypeRaw,
metadata: (eventTypeRaw.metadata || {}) as JSONObject,
metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata || {}),
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
};
+2 -1
View File
@@ -4,6 +4,7 @@ import { JSONObject } from "superjson/dist/types";
import { parseRecurringEvent } from "@calcom/lib";
import prisma from "@calcom/prisma";
import { bookEventTypeSelect } from "@calcom/prisma/selects";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
import { inferSSRProps } from "@lib/types/inferSSRProps";
@@ -69,7 +70,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
if (!eventTypeRaw) return { notFound: true };
const eventType = {
...eventTypeRaw,
metadata: (eventTypeRaw.metadata || {}) as JSONObject,
metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata || {}),
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
};
+27 -12
View File
@@ -150,6 +150,21 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
endDate: new Date(eventType.periodEndDate || Date.now()),
});
const metadata = eventType.metadata;
// fallback to !!eventType.schedule when 'useHostSchedulesForTeamEvent' is undefined
if (!!team) {
metadata.config = {
...metadata.config,
useHostSchedulesForTeamEvent:
typeof eventType.metadata.config?.useHostSchedulesForTeamEvent !== "undefined"
? eventType.metadata.config?.useHostSchedulesForTeamEvent === true
: !!eventType.schedule,
};
} else {
// Make sure non-team events NEVER have this config key;
delete metadata.config?.useHostSchedulesForTeamEvent;
}
const formMethods = useForm<FormValues>({
defaultValues: {
title: eventType.title,
@@ -172,17 +187,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
findDurationType(eventType.minimumBookingNotice),
eventType.minimumBookingNotice
),
// fallback to !!eventType.schedule when 'useHostSchedulesForTeamEvent' is undefined
metadata: {
...eventType.metadata,
config: {
...eventType.metadata.config,
useHostSchedulesForTeamEvent:
typeof eventType.metadata.config?.useHostSchedulesForTeamEvent !== "undefined"
? eventType.metadata.config?.useHostSchedulesForTeamEvent === true
: !!eventType.schedule,
},
},
metadata,
},
});
@@ -266,7 +271,17 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
if (bookingLimits) {
const isValid = validateBookingLimitOrder(bookingLimits);
if (!isValid) throw new Error("Booking limits must be in accending order. [day,week,month,year]");
if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
}
if (metadata?.multipleDuration !== undefined) {
if (metadata?.multipleDuration.length < 1) {
throw new Error(t("event_setup_multiple_duration_error"));
} else {
if (!metadata?.multipleDuration?.includes(input.length)) {
throw new Error(t("event_setup_multiple_duration_default_error"));
}
}
}
updateMutation.mutate({
@@ -79,7 +79,7 @@ const ApiKeysView = () => {
</LicenseRequired>
<Dialog open={apiKeyModal} onOpenChange={setApiKeyModal}>
<DialogContent type="creation" useOwnActionButtons>
<DialogContent type="creation">
<ApiKeyDialogForm handleClose={() => setApiKeyModal(false)} defaultValues={apiKeyToEdit} />
</DialogContent>
</Dialog>
@@ -5,7 +5,9 @@ import { trpc } from "@calcom/trpc/react";
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
Dropdown,
DropdownMenuContent,
DropdownMenuItem,
@@ -111,10 +113,14 @@ const ConferencingLayout = () => {
title={t("Remove app")}
description={t("are_you_sure_you_want_to_remove_this_app")}
type="confirmation"
actionText={t("yes_remove_app")}
Icon={Icon.FiAlertCircle}
actionOnClick={() => deleteAppMutation.mutate({ id: deleteCredentialId })}
/>
Icon={Icon.FiAlertCircle}>
<DialogFooter>
<Button color="primary" onClick={() => deleteAppMutation.mutate({ id: deleteCredentialId })}>
{t("yes_remove_app")}
</Button>
<DialogClose />
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
+19 -10
View File
@@ -14,7 +14,9 @@ import {
Avatar,
Button,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogTrigger,
Form,
getSettingsLayout as getLayout,
@@ -298,13 +300,7 @@ const ProfileView = () => {
title={t("delete_account_modal_title")}
description={t("confirm_delete_account_modal")}
type="creation"
actionText={t("delete_my_account")}
actionProps={{
// @ts-expect-error data attributes aren't typed
"data-testid": "delete-account-confirm",
}}
Icon={Icon.FiAlertTriangle}
actionOnClick={(e) => e && onConfirmButton(e)}>
Icon={Icon.FiAlertTriangle}>
<>
<p className="mb-7">{t("delete_account_confirmation_message")}</p>
{isCALIdentityProviver && (
@@ -326,6 +322,15 @@ const ProfileView = () => {
)}
{hasDeleteErrors && <Alert severity="error" title={deleteErrorMessage} />}
<DialogFooter>
<Button
color="primary"
data-testid="delete-account-confirm"
onClick={(e) => onConfirmButton(e)}>
{t("delete_my_account")}
</Button>
<DialogClose />
</DialogFooter>
</>
</DialogContent>
</Dialog>
@@ -337,9 +342,7 @@ const ProfileView = () => {
title={t("confirm_password")}
description={t("confirm_password_change_email")}
type="creation"
actionText={t("confirm")}
Icon={Icon.FiAlertTriangle}
actionOnClick={(e) => e && onConfirmPassword(e)}>
Icon={Icon.FiAlertTriangle}>
<>
<PasswordField
data-testid="password"
@@ -352,6 +355,12 @@ const ProfileView = () => {
/>
{confirmPasswordErrorMessage && <Alert severity="error" title={confirmPasswordErrorMessage} />}
<DialogFooter>
<Button color="primary" onClick={(e) => onConfirmPassword(e)}>
{t("confirm")}
</Button>
<DialogClose />
</DialogFooter>
</>
</DialogContent>
</Dialog>
+132 -112
View File
@@ -189,6 +189,7 @@ export default function Success(props: SuccessProps) {
const isEmbed = useIsEmbed();
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
const [calculatedDuration, setCalculatedDuration] = useState<number | undefined>(undefined);
function setIsCancellationMode(value: boolean) {
if (value) router.query.cancel = "true";
@@ -231,7 +232,7 @@ export default function Success(props: SuccessProps) {
sdkActionManager.fire("bookingSuccessful", {
eventType,
date: date.toString(),
duration: eventType.length,
duration: calculatedDuration,
organizer: {
name: users[0].name || "Nameless",
email: users[0].email || "Email-less",
@@ -245,6 +246,12 @@ export default function Success(props: SuccessProps) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [eventType, needsConfirmation]);
useEffect(() => {
setCalculatedDuration(
dayjs(props.bookingInfo.endTime).diff(dayjs(props.bookingInfo.startTime), "minutes")
);
}, []);
function eventLink(): string {
const optional: { location?: string } = {};
if (location) {
@@ -263,7 +270,9 @@ export default function Success(props: SuccessProps) {
title: eventName,
description: props.eventType.description ? props.eventType.description : undefined,
/** formatted to required type of description ^ */
duration: { minutes: props.eventType.length },
duration: {
minutes: calculatedDuration,
},
...optional,
});
@@ -389,6 +398,7 @@ export default function Success(props: SuccessProps) {
<div className="col-span-2 mb-6 last:mb-0">
<RecurringBookings
eventType={props.eventType}
duration={calculatedDuration}
recurringBookings={props.recurringBookings}
allRemainingBookings={allRemainingBookings}
date={date}
@@ -513,114 +523,118 @@ export default function Success(props: SuccessProps) {
/>
</>
))}
{userIsOwner && !needsConfirmation && !isCancellationMode && !isCancelled && (
<>
<hr className="border-bookinglightest dark:border-darkgray-300 mt-8" />
<div className="text-bookingdark align-center flex flex-row justify-center pt-8">
<span className="flex self-center font-medium text-gray-700 ltr:mr-2 rtl:ml-2 dark:text-gray-50">
{t("add_to_calendar")}
</span>
<div className="justify-left mt-1 flex text-left sm:mt-0">
<Link
href={
`https://calendar.google.com/calendar/r/eventedit?dates=${date
.utc()
.format("YYYYMMDDTHHmmss[Z]")}/${date
.add(props.eventType.length, "minute")
.utc()
.format("YYYYMMDDTHHmmss[Z]")}&text=${eventName}&details=${
props.eventType.description
}` +
(typeof location === "string"
? "&location=" + encodeURIComponent(location)
: "") +
(props.eventType.recurringEvent
? "&recur=" +
encodeURIComponent(new RRule(props.eventType.recurringEvent).toString())
: "")
}>
<a className="mr-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white">
<svg
className="-mt-1.5 inline-block h-4 w-4"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<title>Google</title>
<path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z" />
</svg>
</a>
</Link>
<Link
href={
encodeURI(
"https://outlook.live.com/calendar/0/deeplink/compose?body=" +
props.eventType.description +
"&enddt=" +
date.add(props.eventType.length, "minute").utc().format() +
"&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=" +
date.utc().format() +
"&subject=" +
eventName
) + (location ? "&location=" + location : "")
}>
<a
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white"
target="_blank">
<svg
className="mr-1 -mt-1.5 inline-block h-4 w-4"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<title>Microsoft Outlook</title>
<path d="M7.88 12.04q0 .45-.11.87-.1.41-.33.74-.22.33-.58.52-.37.2-.87.2t-.85-.2q-.35-.21-.57-.55-.22-.33-.33-.75-.1-.42-.1-.86t.1-.87q.1-.43.34-.76.22-.34.59-.54.36-.2.87-.2t.86.2q.35.21.57.55.22.34.31.77.1.43.1.88zM24 12v9.38q0 .46-.33.8-.33.32-.8.32H7.13q-.46 0-.8-.33-.32-.33-.32-.8V18H1q-.41 0-.7-.3-.3-.29-.3-.7V7q0-.41.3-.7Q.58 6 1 6h6.5V2.55q0-.44.3-.75.3-.3.75-.3h12.9q.44 0 .75.3.3.3.3.75V10.85l1.24.72h.01q.1.07.18.18.07.12.07.25zm-6-8.25v3h3v-3zm0 4.5v3h3v-3zm0 4.5v1.83l3.05-1.83zm-5.25-9v3h3.75v-3zm0 4.5v3h3.75v-3zm0 4.5v2.03l2.41 1.5 1.34-.8v-2.73zM9 3.75V6h2l.13.01.12.04v-2.3zM5.98 15.98q.9 0 1.6-.3.7-.32 1.19-.86.48-.55.73-1.28.25-.74.25-1.61 0-.83-.25-1.55-.24-.71-.71-1.24t-1.15-.83q-.68-.3-1.55-.3-.92 0-1.64.3-.71.3-1.2.85-.5.54-.75 1.3-.25.74-.25 1.63 0 .85.26 1.56.26.72.74 1.23.48.52 1.17.81.69.3 1.56.3zM7.5 21h12.39L12 16.08V17q0 .41-.3.7-.29.3-.7.3H7.5zm15-.13v-7.24l-5.9 3.54Z" />
</svg>
</a>
</Link>
<Link
href={
encodeURI(
"https://outlook.office.com/calendar/0/deeplink/compose?body=" +
props.eventType.description +
"&enddt=" +
date.add(props.eventType.length, "minute").utc().format() +
"&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=" +
date.utc().format() +
"&subject=" +
eventName
) + (location ? "&location=" + location : "")
}>
<a
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white"
target="_blank">
<svg
className="mr-1 -mt-1.5 inline-block h-4 w-4"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<title>Microsoft Office</title>
<path d="M21.53 4.306v15.363q0 .807-.472 1.433-.472.627-1.253.85l-6.888 1.974q-.136.037-.29.055-.156.019-.293.019-.396 0-.72-.105-.321-.106-.656-.292l-4.505-2.544q-.248-.137-.391-.366-.143-.23-.143-.515 0-.434.304-.738.304-.305.739-.305h5.831V4.964l-4.38 1.563q-.533.187-.856.658-.322.472-.322 1.03v8.078q0 .496-.248.912-.25.416-.683.651l-2.072 1.13q-.286.148-.571.148-.497 0-.844-.347-.348-.347-.348-.844V6.563q0-.62.33-1.19.328-.571.874-.881L11.07.285q.248-.136.534-.21.285-.075.57-.075.211 0 .38.031.166.031.364.093l6.888 1.899q.384.11.7.329.317.217.547.52.23.305.353.67.125.367.125.764zm-1.588 15.363V4.306q0-.273-.16-.478-.163-.204-.423-.28l-3.388-.93q-.397-.111-.794-.23-.397-.117-.794-.216v19.68l4.976-1.427q.26-.074.422-.28.161-.204.161-.477z" />
</svg>
</a>
</Link>
<Link href={"data:text/calendar," + eventLink()}>
<a
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white"
download={props.eventType.title + ".ics"}>
<svg
version="1.1"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1000 1000"
className="mr-1 -mt-1.5 inline-block h-4 w-4">
<title>{t("other")}</title>
<path d="M971.3,154.9c0-34.7-28.2-62.9-62.9-62.9H611.7c-1.3,0-2.6,0.1-3.9,0.2V10L28.7,87.3v823.4L607.8,990v-84.6c1.3,0.1,2.6,0.2,3.9,0.2h296.7c34.7,0,62.9-28.2,62.9-62.9V154.9z M607.8,636.1h44.6v-50.6h-44.6v-21.9h44.6v-50.6h-44.6v-92h277.9v230.2c0,3.8-3.1,7-7,7H607.8V636.1z M117.9,644.7l-50.6-2.4V397.5l50.6-2.2V644.7z M288.6,607.3c17.6,0.6,37.3-2.8,49.1-7.2l9.1,48c-11,5.1-35.6,9.9-66.9,8.3c-85.4-4.3-127.5-60.7-127.5-132.6c0-86.2,57.8-136.7,133.2-140.1c30.3-1.3,53.7,4,64.3,9.2l-12.2,48.9c-12.1-4.9-28.8-9.2-49.5-8.6c-45.3,1.2-79.5,30.1-79.5,87.4C208.8,572.2,237.8,605.7,288.6,607.3z M455.5,665.2c-32.4-1.6-63.7-11.3-79.1-20.5l12.6-50.7c16.8,9.1,42.9,18.5,70.4,19.4c30.1,1,46.3-10.7,46.3-29.3c0-17.8-14-28.1-48.8-40.6c-46.9-16.4-76.8-41.7-76.8-81.5c0-46.6,39.3-84.1,106.8-87.1c33.3-1.5,58.3,4.2,76.5,11.2l-15.4,53.3c-12.1-5.3-33.5-12.8-62.3-12c-28.3,0.8-41.9,13.6-41.9,28.1c0,17.8,16.1,25.5,53.6,39c52.9,18.5,78.4,45.3,78.4,86.4C575.6,629.7,536.2,669.2,455.5,665.2z M935.3,842.7c0,14.9-12.1,27-27,27H611.7c-1.3,0-2.6-0.2-3.9-0.4V686.2h270.9c19.2,0,34.9-15.6,34.9-34.9V398.4c0-19.2-15.6-34.9-34.9-34.9h-47.1v-32.3H808v32.3h-44.8v-32.3h-22.7v32.3h-43.3v-32.3h-22.7v32.3H628v-32.3h-20.2v-203c1.31.2,2.6-0.4,3.9-0.4h296.7c14.9,0,27,12.1,27,27L935.3,842.7L935.3,842.7z" />
</svg>
</a>
</Link>
{userIsOwner &&
!needsConfirmation &&
!isCancellationMode &&
!isCancelled &&
calculatedDuration && (
<>
<hr className="border-bookinglightest dark:border-darkgray-300 mt-8" />
<div className="text-bookingdark align-center flex flex-row justify-center pt-8">
<span className="flex self-center font-medium text-gray-700 ltr:mr-2 rtl:ml-2 dark:text-gray-50">
{t("add_to_calendar")}
</span>
<div className="justify-left mt-1 flex text-left sm:mt-0">
<Link
href={
`https://calendar.google.com/calendar/r/eventedit?dates=${date
.utc()
.format("YYYYMMDDTHHmmss[Z]")}/${date
.add(calculatedDuration, "minute")
.utc()
.format("YYYYMMDDTHHmmss[Z]")}&text=${eventName}&details=${
props.eventType.description
}` +
(typeof location === "string"
? "&location=" + encodeURIComponent(location)
: "") +
(props.eventType.recurringEvent
? "&recur=" +
encodeURIComponent(new RRule(props.eventType.recurringEvent).toString())
: "")
}>
<a className="mr-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white">
<svg
className="-mt-1.5 inline-block h-4 w-4"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<title>Google</title>
<path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z" />
</svg>
</a>
</Link>
<Link
href={
encodeURI(
"https://outlook.live.com/calendar/0/deeplink/compose?body=" +
props.eventType.description +
"&enddt=" +
date.add(calculatedDuration, "minute").utc().format() +
"&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=" +
date.utc().format() +
"&subject=" +
eventName
) + (location ? "&location=" + location : "")
}>
<a
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white"
target="_blank">
<svg
className="mr-1 -mt-1.5 inline-block h-4 w-4"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<title>Microsoft Outlook</title>
<path d="M7.88 12.04q0 .45-.11.87-.1.41-.33.74-.22.33-.58.52-.37.2-.87.2t-.85-.2q-.35-.21-.57-.55-.22-.33-.33-.75-.1-.42-.1-.86t.1-.87q.1-.43.34-.76.22-.34.59-.54.36-.2.87-.2t.86.2q.35.21.57.55.22.34.31.77.1.43.1.88zM24 12v9.38q0 .46-.33.8-.33.32-.8.32H7.13q-.46 0-.8-.33-.32-.33-.32-.8V18H1q-.41 0-.7-.3-.3-.29-.3-.7V7q0-.41.3-.7Q.58 6 1 6h6.5V2.55q0-.44.3-.75.3-.3.75-.3h12.9q.44 0 .75.3.3.3.3.75V10.85l1.24.72h.01q.1.07.18.18.07.12.07.25zm-6-8.25v3h3v-3zm0 4.5v3h3v-3zm0 4.5v1.83l3.05-1.83zm-5.25-9v3h3.75v-3zm0 4.5v3h3.75v-3zm0 4.5v2.03l2.41 1.5 1.34-.8v-2.73zM9 3.75V6h2l.13.01.12.04v-2.3zM5.98 15.98q.9 0 1.6-.3.7-.32 1.19-.86.48-.55.73-1.28.25-.74.25-1.61 0-.83-.25-1.55-.24-.71-.71-1.24t-1.15-.83q-.68-.3-1.55-.3-.92 0-1.64.3-.71.3-1.2.85-.5.54-.75 1.3-.25.74-.25 1.63 0 .85.26 1.56.26.72.74 1.23.48.52 1.17.81.69.3 1.56.3zM7.5 21h12.39L12 16.08V17q0 .41-.3.7-.29.3-.7.3H7.5zm15-.13v-7.24l-5.9 3.54Z" />
</svg>
</a>
</Link>
<Link
href={
encodeURI(
"https://outlook.office.com/calendar/0/deeplink/compose?body=" +
props.eventType.description +
"&enddt=" +
date.add(calculatedDuration, "minute").utc().format() +
"&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=" +
date.utc().format() +
"&subject=" +
eventName
) + (location ? "&location=" + location : "")
}>
<a
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white"
target="_blank">
<svg
className="mr-1 -mt-1.5 inline-block h-4 w-4"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<title>Microsoft Office</title>
<path d="M21.53 4.306v15.363q0 .807-.472 1.433-.472.627-1.253.85l-6.888 1.974q-.136.037-.29.055-.156.019-.293.019-.396 0-.72-.105-.321-.106-.656-.292l-4.505-2.544q-.248-.137-.391-.366-.143-.23-.143-.515 0-.434.304-.738.304-.305.739-.305h5.831V4.964l-4.38 1.563q-.533.187-.856.658-.322.472-.322 1.03v8.078q0 .496-.248.912-.25.416-.683.651l-2.072 1.13q-.286.148-.571.148-.497 0-.844-.347-.348-.347-.348-.844V6.563q0-.62.33-1.19.328-.571.874-.881L11.07.285q.248-.136.534-.21.285-.075.57-.075.211 0 .38.031.166.031.364.093l6.888 1.899q.384.11.7.329.317.217.547.52.23.305.353.67.125.367.125.764zm-1.588 15.363V4.306q0-.273-.16-.478-.163-.204-.423-.28l-3.388-.93q-.397-.111-.794-.23-.397-.117-.794-.216v19.68l4.976-1.427q.26-.074.422-.28.161-.204.161-.477z" />
</svg>
</a>
</Link>
<Link href={"data:text/calendar," + eventLink()}>
<a
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white"
download={props.eventType.title + ".ics"}>
<svg
version="1.1"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1000 1000"
className="mr-1 -mt-1.5 inline-block h-4 w-4">
<title>{t("other")}</title>
<path d="M971.3,154.9c0-34.7-28.2-62.9-62.9-62.9H611.7c-1.3,0-2.6,0.1-3.9,0.2V10L28.7,87.3v823.4L607.8,990v-84.6c1.3,0.1,2.6,0.2,3.9,0.2h296.7c34.7,0,62.9-28.2,62.9-62.9V154.9z M607.8,636.1h44.6v-50.6h-44.6v-21.9h44.6v-50.6h-44.6v-92h277.9v230.2c0,3.8-3.1,7-7,7H607.8V636.1z M117.9,644.7l-50.6-2.4V397.5l50.6-2.2V644.7z M288.6,607.3c17.6,0.6,37.3-2.8,49.1-7.2l9.1,48c-11,5.1-35.6,9.9-66.9,8.3c-85.4-4.3-127.5-60.7-127.5-132.6c0-86.2,57.8-136.7,133.2-140.1c30.3-1.3,53.7,4,64.3,9.2l-12.2,48.9c-12.1-4.9-28.8-9.2-49.5-8.6c-45.3,1.2-79.5,30.1-79.5,87.4C208.8,572.2,237.8,605.7,288.6,607.3z M455.5,665.2c-32.4-1.6-63.7-11.3-79.1-20.5l12.6-50.7c16.8,9.1,42.9,18.5,70.4,19.4c30.1,1,46.3-10.7,46.3-29.3c0-17.8-14-28.1-48.8-40.6c-46.9-16.4-76.8-41.7-76.8-81.5c0-46.6,39.3-84.1,106.8-87.1c33.3-1.5,58.3,4.2,76.5,11.2l-15.4,53.3c-12.1-5.3-33.5-12.8-62.3-12c-28.3,0.8-41.9,13.6-41.9,28.1c0,17.8,16.1,25.5,53.6,39c52.9,18.5,78.4,45.3,78.4,86.4C575.6,629.7,536.2,669.2,455.5,665.2z M935.3,842.7c0,14.9-12.1,27-27,27H611.7c-1.3,0-2.6-0.2-3.9-0.4V686.2h270.9c19.2,0,34.9-15.6,34.9-34.9V398.4c0-19.2-15.6-34.9-34.9-34.9h-47.1v-32.3H808v32.3h-44.8v-32.3h-22.7v32.3h-43.3v-32.3h-22.7v32.3H628v-32.3h-20.2v-203c1.31.2,2.6-0.4,3.9-0.4h296.7c14.9,0,27,12.1,27,27L935.3,842.7L935.3,842.7z" />
</svg>
</a>
</Link>
</div>
</div>
</div>
</>
)}
</>
)}
{session === null && !(userIsOwner || props.hideBranding) && (
<>
<hr className="border-bookinglightest dark:border-darkgray-300 mt-8" />
@@ -669,6 +683,7 @@ type RecurringBookingsProps = {
eventType: SuccessProps["eventType"];
recurringBookings: SuccessProps["recurringBookings"];
date: dayjs.Dayjs;
duration: number | undefined;
is24h: boolean;
allRemainingBookings: boolean;
isCancelled: boolean;
@@ -677,6 +692,7 @@ type RecurringBookingsProps = {
export function RecurringBookings({
eventType,
recurringBookings,
duration,
date,
allRemainingBookings,
is24h,
@@ -689,6 +705,8 @@ export function RecurringBookings({
? recurringBookings.sort((a: ConfigType, b: ConfigType) => (dayjs(a).isAfter(dayjs(b)) ? 1 : -1))
: null;
if (!duration) return null;
if (recurringBookingsSorted && allRemainingBookings) {
return (
<>
@@ -707,7 +725,7 @@ export function RecurringBookings({
{dayjs.tz(dateStr, timeZone()).format("MMMM DD, YYYY")}
<br />
{formatTime(dateStr, is24h ? 24 : 12, timeZone())} -{" "}
{formatTime(dayjs(dateStr).add(eventType.length, "m"), is24h ? 24 : 12, timeZone())}{" "}
{formatTime(dayjs(dateStr).add(duration, "m"), is24h ? 24 : 12, timeZone())}{" "}
<span className="text-bookinglight">({timeZone()})</span>
</div>
))}
@@ -725,7 +743,7 @@ export function RecurringBookings({
{dayjs.tz(dateStr, timeZone()).format("MMMM DD, YYYY")}
<br />
{formatTime(dateStr, is24h ? 24 : 12, timeZone())} -{" "}
{formatTime(dayjs(dateStr).add(eventType.length, "m"), is24h ? 24 : 12, timeZone())}{" "}
{formatTime(dayjs(dateStr).add(duration, "m"), is24h ? 24 : 12, timeZone())}{" "}
<span className="text-bookinglight">({timeZone()})</span>
</div>
))}
@@ -741,7 +759,7 @@ export function RecurringBookings({
{dayjs.tz(date, timeZone()).format("MMMM DD, YYYY")}
<br />
{formatTime(date, is24h ? 24 : 12, timeZone())} -{" "}
{formatTime(dayjs(date).add(eventType.length, "m"), is24h ? 24 : 12, timeZone())}{" "}
{formatTime(dayjs(date).add(duration, "m"), is24h ? 24 : 12, timeZone())}{" "}
<span className="text-bookinglight">({timeZone()})</span>
</div>
);
@@ -865,6 +883,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
smsReminderNumber: true,
recurringEventId: true,
startTime: true,
endTime: true,
location: true,
status: true,
cancellationReason: true,
@@ -901,6 +920,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
// @NOTE: had to do this because Server side cant return [Object objects]
// probably fixable with json.stringify -> json.parse
bookingInfo["startTime"] = (bookingInfo?.startTime as Date)?.toISOString() as unknown as Date;
bookingInfo["endTime"] = (bookingInfo?.endTime as Date)?.toISOString() as unknown as Date;
const eventTypeRaw = !bookingInfo.eventTypeId
? getDefaultEvent(eventTypeSlug || "")
+2 -1
View File
@@ -4,6 +4,7 @@ import { JSONObject } from "superjson/dist/types";
import { LocationObject, privacyFilteredLocations } from "@calcom/app-store/locations";
import { parseRecurringEvent } from "@calcom/lib";
import prisma from "@calcom/prisma";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
import getBooking, { GetBookingType } from "@lib/getBooking";
@@ -92,7 +93,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
const eventTypeObject = [eventType].map((e) => {
return {
...e,
metadata: (eventType.metadata || {}) as JSONObject,
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
periodStartDate: e.periodStartDate?.toString() ?? null,
periodEndDate: e.periodEndDate?.toString() ?? null,
users: eventType.users.map((u) => ({
@@ -558,6 +558,10 @@
"collective": "Collective",
"collective_description": "Schedule meetings when all selected team members are available.",
"duration": "Duration",
"available_durations": "Available durations",
"default_duration": "Default duration",
"default_duration_no_options": "Please choose available durations first",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"minutes": "Minutes",
"round_robin": "Round Robin",
"round_robin_description": "Cycle meetings between multiple team members.",
@@ -901,6 +905,7 @@
"user_impersonation_heading": "User Impersonation",
"user_impersonation_description": "Allows our support team to temporarily sign in as you to help us quickly resolve any issues you report to us.",
"team_impersonation_description": "Allows your team members to temporarily sign in as you.",
"allow_booker_to_select_duration": "Allow booker to select duration",
"impersonate_user_tip": "All uses of this feature is audited.",
"impersonating_user_warning": "Impersonating username \"{{user}}\".",
"impersonating_stop_instructions": "<0>Click Here to stop</0>.",
@@ -1105,6 +1110,9 @@
"event_limit_tab_description": "How often you can be booked",
"event_advanced_tab_description": "Calendar settings & more...",
"event_advanced_tab_title": "Advanced",
"event_setup_multiple_duration_error": "Event Setup: Multiple durations requires at least 1 option.",
"event_setup_multiple_duration_default_error": "Event Setup: Please select a valid default duration.",
"event_setup_booking_limits_error": "Booking limits must be in accending order. [day,week,month,year]",
"select_which_cal": "Select which calendar to add bookings to",
"custom_event_name": "Custom event name",
"custom_event_name_description": "Create customised event names to display on calendar event",
@@ -120,9 +120,7 @@ function NewFormDialog({ appUrl }: { appUrl: string }) {
<Button loading={mutation.isLoading} data-testid="add-form" type="submit">
{t("continue")}
</Button>
<DialogClose asChild>
<Button color="secondary">{t("cancel")}</Button>
</DialogClose>
<DialogClose />
</div>
</Form>
</DialogContent>
@@ -379,16 +379,14 @@ function SingleForm({ form, appUrl, Page }: SingleFormComponentProps) {
)}
</div>
<DialogFooter>
<DialogClose asChild>
<Button
color="secondary"
onClick={() => {
setIsTestPreviewOpen(false);
setDecidedAction(null);
setResponse({});
}}>
{t("close")}
</Button>
<DialogClose
color="secondary"
onClick={() => {
setIsTestPreviewOpen(false);
setDecidedAction(null);
setResponse({});
}}>
{t("close")}
</DialogClose>
<Button type="submit" data-testid="test-routing">
{t("test_routing")}
@@ -187,13 +187,12 @@ export const SearchDialog = (props: ISearchDialog) => {
)}
<DialogFooter>
<DialogClose
color="minimal"
tabIndex={-1}
onClick={() => {
props.setIsOpenDialog(false);
}}
asChild>
<Button type="button" color="minimal" tabIndex={-1}>
{t("cancel")}
</Button>
}}>
{t("cancel")}
</DialogClose>
<Button
@@ -15,13 +15,13 @@ export function AccountDialog(props: DialogProps) {
<DialogFooter>
<DialogClose
type="button"
color="secondary"
tabIndex={-1}
onClick={() => {
props.onOpenChange?.(false);
}}
asChild>
<Button type="button" color="secondary" tabIndex={-1}>
Cancel
</Button>
}}>
Cancel
</DialogClose>
<Button type="button" onClick={() => mutation.mutate("")}>
@@ -47,7 +47,7 @@ let isSafariBrowser = false;
const isBrowser = typeof window !== "undefined";
if (isBrowser) {
window.CalEmbed = window.CalEmbed || {};
window.CalEmbed = window?.CalEmbed || {};
window.CalEmbed.embedStore = embedStore;
const ua = navigator.userAgent.toLowerCase();
isSafariBrowser = ua.includes("safari") && !ua.includes("chrome");
@@ -201,7 +201,7 @@ function getNamespace() {
return embedStore.namespace;
}
if (isBrowser) {
const namespace = window.getEmbedNamespace();
const namespace = window?.getEmbedNamespace?.() || null;
embedStore.namespace = namespace;
return namespace;
}
@@ -228,7 +228,7 @@ export const useIsEmbed = (embedSsr?: boolean) => {
"Looks like you have iframed cal.com but not using Embed Snippet. Directly using an iframe isn't recommended."
);
}
setIsEmbed(window.isEmbed());
setIsEmbed(window?.isEmbed?.() || false);
}, []);
return isEmbed;
};
@@ -254,7 +254,7 @@ export const methods = {
// In case where parent gives instructions before CalComPlan is set.
// This is easily possible as React takes time to initialize and render components where this variable is set.
if (!window.CalComPlan) {
if (!window?.CalComPlan) {
return requestAnimationFrame(() => {
style(uiConfig);
});
@@ -364,10 +364,10 @@ function keepParentInformedAboutDimensionChanges() {
}
if (isBrowser) {
log("Embed SDK loaded", { isEmbed: window.isEmbed() });
log("Embed SDK loaded", { isEmbed: window?.isEmbed?.() || false });
const url = new URL(document.URL);
embedStore.theme = (window.getEmbedTheme() || "auto") as UiConfig["theme"];
if (url.searchParams.get("prerender") !== "true" && window.isEmbed()) {
embedStore.theme = (window?.getEmbedTheme?.() || "auto") as UiConfig["theme"];
if (url.searchParams.get("prerender") !== "true" && window?.isEmbed?.()) {
log("Initializing embed-iframe");
// HACK
const pageStatus = window.CalComPageStatus;
@@ -174,7 +174,7 @@ export default function SAMLConfiguration({ teamId }: { teamId: number | null })
{/* Add/Update SAML Connection */}
<Dialog open={configModal} onOpenChange={setConfigModal}>
<DialogContent type="creation" useOwnActionButtons>
<DialogContent type="creation">
<ConfigDialogForm handleClose={() => setConfigModal(false)} teamId={teamId} />
</DialogContent>
</Dialog>
@@ -67,7 +67,7 @@ export default function MemberChangeRoleModal(props: {
}
return (
<Dialog open={props.isOpen} onOpenChange={props.onExit}>
<DialogContent type="creation" useOwnActionButtons size="md">
<DialogContent type="creation" size="md">
<>
<div className="mb-4 sm:flex sm:items-start">
<div className="text-center sm:text-left">
@@ -66,7 +66,6 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps)
}}>
<DialogContent
type="creation"
useOwnActionButtons
title={t("invite_new_member")}
description={
IS_TEAM_BILLING_ENABLED ? (
@@ -13,6 +13,7 @@ import {
ConfirmationDialogContent,
Dialog,
DialogContent,
DialogFooter,
DialogTrigger,
Dropdown,
DropdownItem,
@@ -258,11 +259,11 @@ export default function MemberListItem(props: Props) {
)}
{showTeamAvailabilityModal && (
<Dialog open={showTeamAvailabilityModal} onOpenChange={() => setShowTeamAvailabilityModal(false)}>
<DialogContent type="creation" useOwnActionButtons size="md">
<DialogContent type="creation" size="md">
<TeamAvailabilityModal team={props.team} member={props.member} />
<div className="flex justify-end border-t pt-5">
<DialogFooter>
<Button onClick={() => setShowTeamAvailabilityModal(false)}>{t("done")}</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
)}
@@ -102,7 +102,7 @@ export const AddActionDialog = (props: IAddActionDialog) => {
return (
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
<DialogContent type="creation" useOwnActionButtons={true} title={t("add_action")}>
<DialogContent type="creation" title={t("add_action")}>
<div className="space-x-3 ">
<div className="pt-1">
<Form
@@ -188,21 +188,17 @@ export const AddActionDialog = (props: IAddActionDialog) => {
</div>
)}
<DialogFooter>
<DialogClose asChild>
<Button
color="secondary"
onClick={() => {
setIsOpenDialog(false);
form.unregister("sendTo");
form.unregister("action");
form.unregister("numberRequired");
setIsPhoneNumberNeeded(false);
setIsEmailAddressNeeded(false);
setIsSenderIdNeeded(false);
}}>
{t("cancel")}
</Button>
</DialogClose>
<DialogClose
onClick={() => {
setIsOpenDialog(false);
form.unregister("sendTo");
form.unregister("action");
form.unregister("numberRequired");
setIsPhoneNumberNeeded(false);
setIsEmailAddressNeeded(false);
setIsSenderIdNeeded(false);
}}
/>
<Button type="submit">{t("add")}</Button>
</DialogFooter>
</Form>
@@ -603,7 +603,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
</ConfirmationDialogContent>
</Dialog>
<Dialog open={isAdditionalInputsDialogOpen} onOpenChange={setIsAdditionalInputsDialogOpen}>
<DialogContent useOwnActionButtons type="creation" className="sm:max-w-[610px] md:h-[570px]">
<DialogContent type="creation" className="sm:max-w-[610px] md:h-[570px]">
<div className="-m-3 h-[430px] overflow-x-hidden overflow-y-scroll sm:m-0">
<h1 className="w-full text-xl font-semibold ">{t("how_additional_inputs_as_variables")}</h1>
<div className="mb-7 mt-7 rounded-md bg-gray-50 p-3 sm:p-4">
@@ -649,11 +649,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
</div>
</div>
<div className="flex flex-row-reverse">
<DialogClose asChild>
<Button color="primary" type="button">
{t("close")}
</Button>
</DialogClose>
<DialogClose color="primary" />
</div>
</DialogContent>
</Dialog>
@@ -4,7 +4,17 @@ import { useForm } from "react-hook-form";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
import { trpc } from "@calcom/trpc/react";
import { Button, Dialog, DialogClose, DialogContent, DialogTrigger, Form, Icon, showToast } from "@calcom/ui";
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogTrigger,
Form,
Icon,
showToast,
} from "@calcom/ui";
export function NewScheduleButton({ name = "new-schedule" }: { name?: string }) {
const router = useRouter();
@@ -78,14 +88,12 @@ export function NewScheduleButton({ name = "new-schedule" }: { name?: string })
/>
</div>
</div>
<div className="mt-8 flex flex-row-reverse gap-x-2">
<DialogFooter>
<Button type="submit" loading={createMutation.isLoading}>
{t("continue")}
</Button>
<DialogClose asChild>
<Button color="secondary">{t("cancel")}</Button>
</DialogClose>
</div>
<DialogClose />
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
+8
View File
@@ -249,6 +249,14 @@ async function main() {
slug: "60min",
length: 60,
},
{
title: "Multiple duration",
slug: "multiple-duration",
length: 75,
metadata: {
multipleDuration: [30, 75, 90],
},
},
{
title: "paid",
slug: "paid",
+1
View File
@@ -29,6 +29,7 @@ export const EventTypeMetaDataSchema = z
.object({
smartContractAddress: z.string().optional(),
blockchainId: z.number().optional(),
multipleDuration: z.number().array().optional(),
giphyThankYouPage: z.string().optional(),
apps: z.object(appDataSchemas).partial().optional(),
additionalNotesRequired: z.boolean().optional(),
@@ -34,6 +34,11 @@ const getScheduleSchema = z
// or list of users (for dynamic events)
usernameList: z.array(z.string()).optional(),
debug: z.boolean().optional(),
// to handle event types with multiple duration options
duration: z
.string()
.optional()
.transform((val) => val && parseInt(val)),
})
.refine(
(data) => !!data.eventTypeId || !!data.usernameList,
@@ -277,10 +282,10 @@ export async function getSchedule(input: z.infer<typeof getScheduleSchema>, ctx:
// get slots retrieves the available times for a given day
const timeSlots = getTimeSlots({
inviteeDate: currentCheckedTime,
eventLength: eventType.length,
eventLength: input.duration || eventType.length,
workingHours,
minimumBookingNotice: eventType.minimumBookingNotice,
frequency: eventType.slotInterval || eventType.length,
frequency: eventType.slotInterval || input.duration || eventType.length,
});
const endGetSlots = performance.now();
-86
View File
@@ -1,86 +0,0 @@
/**
* @deprecated file
* All new changes should be made to the V2 file in
* `/packages/ui/v2/core/ConfirmationDialogContent.tsx`
*/
import * as DialogPrimitive from "@radix-ui/react-dialog";
import React, { PropsWithChildren, ReactNode } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, DialogClose, DialogContent, Icon } from ".";
export type ConfirmationDialogContentProps = {
confirmBtn?: ReactNode;
confirmBtnText?: string;
cancelBtnText?: string;
isLoading?: boolean;
loadingText?: string;
onConfirm?: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
title: string;
variety?: "danger" | "warning" | "success";
};
/**
* @deprecated file
* All new changes should be made to the V2 file in
* `/packages/ui/v2/core/ConfirmationDialogContent.tsx`
*/
export default function ConfirmationDialogContent(props: PropsWithChildren<ConfirmationDialogContentProps>) {
const { t } = useLocale();
const {
title,
variety,
confirmBtn = null,
confirmBtnText = t("confirm"),
cancelBtnText = t("cancel"),
loadingText = t("loading"),
isLoading = false,
onConfirm,
children,
} = props;
return (
<DialogContent>
<div className="flex">
{variety && (
<div className="mt-0.5 ltr:mr-3">
{variety === "danger" && (
<div className="mx-auto rounded-full bg-red-100 p-2 text-center">
<Icon.FiAlertCircle className="h-5 w-5 text-red-600" />
</div>
)}
{variety === "warning" && (
<div className="mx-auto rounded-full bg-orange-100 p-2 text-center">
<Icon.FiAlertCircle className="h-5 w-5 text-orange-600" />
</div>
)}
{variety === "success" && (
<div className="mx-auto rounded-full bg-green-100 p-2 text-center">
<Icon.FiCheck className="h-5 w-5 text-green-600" />
</div>
)}
</div>
)}
<div>
<DialogPrimitive.Title className="font-cal text-xl text-gray-900">{title}</DialogPrimitive.Title>
<DialogPrimitive.Description className="text-sm text-neutral-500">
{children}
</DialogPrimitive.Description>
</div>
</div>
<div className="mt-5 flex flex-row-reverse gap-x-2 sm:mt-8">
<DialogClose disabled={isLoading} onClick={onConfirm} asChild>
{confirmBtn || (
<Button data-testid="confirm-button" color="primary" loading={isLoading}>
{isLoading ? loadingText : confirmBtnText}
</Button>
)}
</DialogClose>
<DialogClose disabled={isLoading} asChild>
<Button color="secondary">{cancelBtnText}</Button>
</DialogClose>
</div>
</DialogContent>
);
}
+10 -6
View File
@@ -6,6 +6,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Dialog, DialogContent, showToast } from ".";
import { DialogClose, DialogFooter } from "./Dialog";
export default function TimezoneChangeDialog() {
const { t } = useLocale();
@@ -58,20 +59,23 @@ export default function TimezoneChangeDialog() {
if (data?.user.impersonatedByUID) return null;
const ONE_DAY = 60 * 60 * 24; // 1 day in seconds (60 seconds * 60 minutes * 24 hours)
const THREE_MONTHS = ONE_DAY * 90; // 90 days in seconds (90 days * 1 day in seconds)
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent
title={t("update_timezone_question")}
description={t("update_timezone_description", { formattedCurrentTz })}
type="creation"
actionText={t("update_timezone")}
actionOnClick={() => updateTimezone()}
closeText={t("dont_update")}
onInteractOutside={() => onCancel(86400, false) /* 1 day expire */}
actionOnClose={() => onCancel(7776000, true) /* 3 months expire */}>
{/* todo: save this in db and auto-update when timezone changes (be able to disable??? if yes, /settings)
onInteractOutside={() => onCancel(ONE_DAY, false) /* 1 day expire */}>
{/* todo: save this in db and auto-update when timezone changes (be able to disable??? if yes, /settings)
<Checkbox description="Always update timezone" />
*/}
<DialogFooter>
<DialogClose onClick={() => onCancel(THREE_MONTHS, true)}>{t("dont_update")}</DialogClose>
<DialogClose onClick={() => updateTimezone()}>{t("update_timezone")}</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
@@ -33,7 +33,7 @@ export default function ConfirmationDialogContent(props: PropsWithChildren<Confi
} = props;
return (
<DialogContent type="creation" useOwnActionButtons={true}>
<DialogContent type="creation">
<div className="flex">
{variety && (
<div className="mt-0.5 ltr:mr-3">
@@ -62,16 +62,14 @@ export default function ConfirmationDialogContent(props: PropsWithChildren<Confi
</div>
</div>
<div className="mt-5 flex flex-row-reverse gap-x-2 sm:mt-8">
<DialogClose disabled={isLoading} onClick={onConfirm} asChild>
{confirmBtn || (
<Button color="primary" loading={isLoading}>
{isLoading ? loadingText : confirmBtnText}
</Button>
)}
</DialogClose>
<DialogClose disabled={isLoading} asChild>
<Button color="secondary">{cancelBtnText}</Button>
</DialogClose>
{confirmBtn ? (
confirmBtn
) : (
<DialogClose color="primary" loading={isLoading} onClick={(e) => onConfirm && onConfirm(e)}>
{isLoading ? loadingText : confirmBtnText}
</DialogClose>
)}
<DialogClose disabled={isLoading}>{cancelBtnText}</DialogClose>
</div>
</DialogContent>
);
+25 -39
View File
@@ -1,12 +1,12 @@
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { useRouter } from "next/router";
import React, { ReactNode, useState } from "react";
import React, { forwardRef, HTMLProps, ReactNode, useState } from "react";
import { Icon } from "react-feather";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "../../components/button";
import { Button, ButtonProps } from "../../components/button";
export type DialogProps = React.ComponentProps<typeof DialogPrimitive["Root"]> & {
name?: string;
@@ -70,20 +70,11 @@ type DialogContentProps = React.ComponentProps<typeof DialogPrimitive["Content"]
description?: string | JSX.Element | undefined;
closeText?: string;
actionDisabled?: boolean;
actionText?: string;
Icon?: Icon;
// If this is set it allows you to overide the action buttons. Usefull if you need to use formcontext
useOwnActionButtons?: boolean;
actionOnClick?: (e: Event | React.MouseEvent<HTMLElement, MouseEvent>) => void;
actionOnClose?: () => void;
actionProps?: React.ComponentProps<typeof Button>;
};
export const DialogContent = React.forwardRef<HTMLDivElement, DialogContentProps>(
(
{ children, title, Icon, actionProps, useOwnActionButtons, type = "creation", ...props },
forwardedRef
) => {
({ children, title, Icon, type = "creation", ...props }, forwardedRef) => {
const { t } = useLocale();
return (
@@ -125,31 +116,6 @@ export const DialogContent = React.forwardRef<HTMLDivElement, DialogContentProps
</div>
</div>
)}
{!useOwnActionButtons && (
<DialogFooter>
<div className="mt-2 flex space-x-2">
<DialogClose asChild>
{/* This will require the i18n string passed in */}
<Button color="minimal" onClick={props.actionOnClose}>
{props.closeText ?? t("close")}
</Button>
</DialogClose>
{props.actionOnClick ? (
<Button
color="primary"
disabled={props.actionDisabled}
onClick={props.actionOnClick}
{...actionProps}>
{props.actionText}
</Button>
) : (
<Button color="primary" type="submit" disabled={props.actionDisabled} {...actionProps}>
{props.actionText}
</Button>
)}
</div>
</DialogFooter>
)}
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
);
@@ -175,7 +141,7 @@ export function DialogHeader(props: DialogHeaderProps) {
export function DialogFooter(props: { children: ReactNode }) {
return (
<div>
<div className="mt-5 flex justify-end space-x-2 rtl:space-x-reverse">{props.children}</div>
<div className="mt-7 flex justify-end space-x-2 rtl:space-x-reverse ">{props.children}</div>
</div>
);
}
@@ -183,4 +149,24 @@ export function DialogFooter(props: { children: ReactNode }) {
DialogContent.displayName = "DialogContent";
export const DialogTrigger = DialogPrimitive.Trigger;
export const DialogClose = DialogPrimitive.Close;
// export const DialogClose = DialogPrimitive.Close;
export function DialogClose(
props: {
dialogCloseProps?: React.ComponentProps<typeof DialogPrimitive["Close"]>;
children?: ReactNode;
onClick?: (e: React.MouseEvent<HTMLElement, MouseEvent>) => void;
disabled?: boolean;
color?: ButtonProps["color"];
} & React.ComponentProps<typeof Button>
) {
const { t } = useLocale();
return (
<DialogPrimitive.Close asChild {...props.dialogCloseProps}>
{/* This will require the i18n string passed in */}
<Button color={props.color || "minimal"} {...props}>
{props.children ? props.children : t("Close")}
</Button>
</DialogPrimitive.Close>
);
}
+3 -7
View File
@@ -163,7 +163,7 @@ export default function ImageUploader({
{buttonMsg}
</Button>
</DialogTrigger>
<DialogContent useOwnActionButtons>
<DialogContent>
<div className="mb-4 sm:flex sm:items-start">
<div className="mt-3 text-center sm:mt-0 sm:text-left">
<h3 className="font-cal text-lg leading-6 text-gray-900" id="modal-title">
@@ -201,12 +201,8 @@ export default function ImageUploader({
</div>
</div>
<div className="mt-5 flex flex-row-reverse gap-x-2 sm:mt-4">
<DialogClose asChild>
<Button onClick={() => showCroppedImage(croppedAreaPixels)}>{t("save")}</Button>
</DialogClose>
<DialogClose asChild>
<Button color="secondary">{t("cancel")}</Button>
</DialogClose>
<DialogClose onClick={() => showCroppedImage(croppedAreaPixels)}>{t("save")}</DialogClose>
<DialogClose color="secondary">{t("cancel")}</DialogClose>
</div>
</DialogContent>
</Dialog>
@@ -205,7 +205,6 @@ export default function CreateEventTypeButton(props: CreateEventTypeBtnProps) {
<DialogContent
type="creation"
className="overflow-y-auto"
useOwnActionButtons
title={teamId ? t("add_new_team_event_type") : t("add_new_event_type")}
description={t("new_event_type_to_book_description")}>
<Form
@@ -315,9 +314,7 @@ export default function CreateEventTypeButton(props: CreateEventTypeBtnProps) {
<Button type="submit" loading={createMutation.isLoading}>
{t("continue")}
</Button>
<DialogClose asChild>
<Button color="secondary">{t("cancel")}</Button>
</DialogClose>
<DialogClose />
</div>
</Form>
</DialogContent>
@@ -41,11 +41,21 @@ export const EventTypeDescription = ({ eventType, className }: EventTypeDescript
</p>
)}
<ul className="mt-2 flex flex-wrap space-x-2 sm:flex-nowrap">
<li>
<Badge variant="gray" size="lg" StartIcon={Icon.FiClock}>
{eventType.length}m
</Badge>
</li>
{eventType.metadata?.multipleDuration ? (
eventType.metadata.multipleDuration.map((dur, idx) => (
<li key={idx}>
<Badge variant="gray" size="lg" StartIcon={Icon.FiClock}>
{dur}m
</Badge>
</li>
))
) : (
<li>
<Badge variant="gray" size="lg" StartIcon={Icon.FiClock}>
{eventType.length}m
</Badge>
</li>
)}
{eventType.schedulingType ? (
<li>
<Badge variant="gray" size="lg" StartIcon={Icon.FiUser}>
@@ -3,7 +3,17 @@ import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button, ButtonProps, Dialog, DialogContent, DialogTrigger, Icon, showToast } from "../../..";
import {
Button,
ButtonProps,
Dialog,
DialogContent,
DialogTrigger,
Icon,
showToast,
DialogFooter,
DialogClose,
} from "../../..";
export default function DisconnectIntegration({
credentialId,
@@ -52,10 +62,14 @@ export default function DisconnectIntegration({
title={t("remove_app")}
description={t("are_you_sure_you_want_to_remove_this_app")}
type="confirmation"
actionText={t("yes_remove_app")}
Icon={Icon.FiAlertCircle}
actionOnClick={() => mutation.mutate({ id: credentialId })}
/>
Icon={Icon.FiAlertCircle}>
<DialogFooter>
<DialogClose onClick={() => setModalOpen(false)} />
<DialogClose color="primary" onClick={() => mutation.mutate({ id: credentialId })}>
{t("yes_remove_app")}
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);