Recurring event fixes (#3030)

* Incorporates recurringEvent into calEvent

* Recurrent event fixes

* Update event.ts

* type fixes

* Update zod-utils.ts

* Adding more recurringEvent to calEvent and other fixes

* Moving away from heavy 3rd party enum

* Fixing test

* Renaming as suggested and fixes

Co-authored-by: Leo Giovanetti <hello@leog.me>
This commit is contained in:
Omar López 2022-06-09 18:32:34 -06:00 committed by GitHub
parent 8cd420173c
commit b11398f551
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
55 changed files with 315 additions and 333 deletions

View File

@ -13,11 +13,12 @@ import dayjs from "dayjs";
import { useRouter } from "next/router";
import { useState } from "react";
import { useMutation } from "react-query";
import { Frequency as RRuleFrequency } from "rrule";
import { parseRecurringEvent } from "@calcom/lib";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { Frequency } from "@calcom/prisma/zod-utils";
import Button from "@calcom/ui/Button";
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
import { Tooltip } from "@calcom/ui/Tooltip";
@ -196,7 +197,7 @@ function BookingListItem(booking: BookingItemProps) {
[recurringStrings] = parseRecurringDates(
{
startDate: booking.startTime,
recurringEvent: booking.eventType.recurringEvent,
recurringEvent: parseRecurringEvent(booking.eventType.recurringEvent),
recurringCount: booking.recurringCount,
},
i18n
@ -307,14 +308,10 @@ function BookingListItem(booking: BookingItemProps) {
<RefreshIcon className="mr-1 -mt-1 inline-block h-4 w-4 text-gray-400" />
{`${t("every_for_freq", {
freq: t(
`${RRuleFrequency[booking.eventType.recurringEvent.freq]
.toString()
.toLowerCase()}`
`${Frequency[booking.eventType.recurringEvent.freq].toString().toLowerCase()}`
),
})} ${booking.recurringCount} ${t(
`${RRuleFrequency[booking.eventType.recurringEvent.freq]
.toString()
.toLowerCase()}`,
`${Frequency[booking.eventType.recurringEvent.freq].toString().toLowerCase()}`,
{ count: booking.recurringCount }
)}`}
</p>

View File

@ -21,7 +21,6 @@ import { TFunction } from "next-i18next";
import { useRouter } from "next/router";
import { useCallback, useEffect, useMemo, useState } from "react";
import { FormattedNumber, IntlProvider } from "react-intl";
import { Frequency as RRuleFrequency } from "rrule";
import { AppStoreLocationType, LocationObject, LocationType } from "@calcom/app-store/locations";
import {
@ -35,6 +34,7 @@ import classNames from "@calcom/lib/classNames";
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { localStorage } from "@calcom/lib/webstorage";
import { Frequency } from "@calcom/prisma/zod-utils";
import { asStringOrNull } from "@lib/asStringOrNull";
import { timeZone } from "@lib/clock";
@ -302,7 +302,7 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
<p className="mb-1 -ml-2 inline px-2 py-1">
{t("every_for_freq", {
freq: t(
`${RRuleFrequency[eventType.recurringEvent.freq].toString().toLowerCase()}`
`${Frequency[eventType.recurringEvent.freq].toString().toLowerCase()}`
),
})}
</p>
@ -317,12 +317,9 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
}}
/>
<p className="inline text-gray-600 dark:text-white">
{t(
`${RRuleFrequency[eventType.recurringEvent.freq].toString().toLowerCase()}`,
{
count: recurringEventCount,
}
)}
{t(`${Frequency[eventType.recurringEvent.freq].toString().toLowerCase()}`, {
count: recurringEventCount,
})}
</p>
</div>
)}
@ -344,7 +341,7 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
<p className="mb-1 -ml-2 inline px-2 py-1">
{t("every_for_freq", {
freq: t(
`${RRuleFrequency[eventType.recurringEvent.freq].toString().toLowerCase()}`
`${Frequency[eventType.recurringEvent.freq].toString().toLowerCase()}`
),
})}
</p>
@ -359,12 +356,9 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
}}
/>
<p className="inline text-gray-600 dark:text-white">
{t(
`${RRuleFrequency[eventType.recurringEvent.freq].toString().toLowerCase()}`,
{
count: recurringEventCount,
}
)}
{t(`${Frequency[eventType.recurringEvent.freq].toString().toLowerCase()}`, {
count: recurringEventCount,
})}
</p>
</div>
)}
@ -471,9 +465,7 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
<RefreshIcon className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4 text-gray-400" />
<p className="mb-1 -ml-2 inline px-2 py-1">
{t("every_for_freq", {
freq: t(
`${RRuleFrequency[eventType.recurringEvent.freq].toString().toLowerCase()}`
),
freq: t(`${Frequency[eventType.recurringEvent.freq].toString().toLowerCase()}`),
})}
</p>
<input
@ -487,7 +479,7 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
}}
/>
<p className="inline text-gray-600 dark:text-white">
{t(`${RRuleFrequency[eventType.recurringEvent.freq].toString().toLowerCase()}`, {
{t(`${Frequency[eventType.recurringEvent.freq].toString().toLowerCase()}`, {
count: recurringEventCount,
})}
</p>

View File

@ -20,7 +20,6 @@ import { Controller, useForm, useWatch } from "react-hook-form";
import { FormattedNumber, IntlProvider } from "react-intl";
import { ReactMultiEmail } from "react-multi-email";
import { useMutation } from "react-query";
import { Frequency as RRuleFrequency } from "rrule";
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
@ -32,6 +31,7 @@ import {
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
import { Frequency } from "@calcom/prisma/zod-utils";
import { createPaymentLink } from "@calcom/stripe/client";
import { Button } from "@calcom/ui/Button";
import { Tooltip } from "@calcom/ui/Tooltip";
@ -518,9 +518,9 @@ const BookingPage = ({
<RefreshIcon className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4 text-gray-400" />
<p className="mb-1 -ml-2 inline px-2 py-1">
{`${t("every_for_freq", {
freq: t(`${RRuleFrequency[eventType.recurringEvent.freq].toString().toLowerCase()}`),
freq: t(`${Frequency[eventType.recurringEvent.freq].toString().toLowerCase()}`),
})} ${recurringEventCount} ${t(
`${RRuleFrequency[eventType.recurringEvent.freq].toString().toLowerCase()}`,
`${Frequency[eventType.recurringEvent.freq].toString().toLowerCase()}`,
{ count: parseInt(recurringEventCount.toString()) }
)}`}
</p>
@ -529,12 +529,12 @@ const BookingPage = ({
<div className="text-bookinghighlight mb-4 flex">
<CalendarIcon className="mr-[10px] ml-[2px] inline-block h-4 w-4" />
<div className="-mt-1">
{(rescheduleUid || !eventType.recurringEvent.freq) &&
{(rescheduleUid || !eventType.recurringEvent?.freq) &&
parseDate(dayjs(date).tz(timeZone()), i18n)}
{!rescheduleUid &&
eventType.recurringEvent.freq &&
eventType.recurringEvent?.freq &&
recurringStrings.slice(0, 5).map((aDate, key) => <p key={key}>{aDate}</p>)}
{!rescheduleUid && eventType.recurringEvent.freq && recurringStrings.length > 5 && (
{!rescheduleUid && eventType.recurringEvent?.freq && recurringStrings.length > 5 && (
<div className="flex">
<Tooltip
content={recurringStrings.slice(5).map((aDate, key) => (

View File

@ -10,9 +10,9 @@ import { Prisma, SchedulingType } from "@prisma/client";
import { useMemo } from "react";
import { FormattedNumber, IntlProvider } from "react-intl";
import { parseRecurringEvent } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { baseEventTypeSelect } from "@calcom/prisma/selects";
import { RecurringEvent } from "@calcom/types/Calendar";
import classNames from "@lib/classNames";
@ -30,8 +30,8 @@ export type EventTypeDescriptionProps = {
export const EventTypeDescription = ({ eventType, className }: EventTypeDescriptionProps) => {
const { t } = useLocale();
const recurringEvent: RecurringEvent = useMemo(
() => (eventType.recurringEvent as RecurringEvent) || [],
const recurringEvent = useMemo(
() => parseRecurringEvent(eventType.recurringEvent),
[eventType.recurringEvent]
);

View File

@ -1,17 +1,16 @@
import { Collapsible, CollapsibleContent } from "@radix-ui/react-collapsible";
import type { FormValues } from "pages/event-types/[type]";
import { useState } from "react";
import { UseFormReturn } from "react-hook-form";
import { Frequency as RRuleFrequency } from "rrule";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Frequency } from "@calcom/prisma/zod-utils";
import { RecurringEvent } from "@calcom/types/Calendar";
import { Alert } from "@calcom/ui/Alert";
import Select from "@components/ui/form/Select";
type RecurringEventControllerProps = {
recurringEvent: RecurringEvent;
recurringEvent: RecurringEvent | null;
formMethods: UseFormReturn<FormValues>;
paymentEnabled: boolean;
onRecurringEventDefined: (value: boolean) => void;
@ -24,20 +23,13 @@ export default function RecurringEventController({
onRecurringEventDefined,
}: RecurringEventControllerProps) {
const { t } = useLocale();
const [recurringEventDefined, setRecurringEventDefined] = useState(recurringEvent?.count !== undefined);
const [recurringEventInterval, setRecurringEventInterval] = useState(recurringEvent?.interval || 1);
const [recurringEventFrequency, setRecurringEventFrequency] = useState(
recurringEvent?.freq || RRuleFrequency.WEEKLY
);
const [recurringEventCount, setRecurringEventCount] = useState(recurringEvent?.count || 12);
const [recurringEventState, setRecurringEventState] = useState<RecurringEvent | null>(recurringEvent);
/* Just yearly-0, monthly-1 and weekly-2 */
const recurringEventFreqOptions = Object.entries(RRuleFrequency)
const recurringEventFreqOptions = Object.entries(Frequency)
.filter(([key, value]) => isNaN(Number(key)) && Number(value) < 3)
.map(([key, value]) => ({
label: t(`${key.toString().toLowerCase()}`, { count: recurringEventInterval }),
label: t(`${key.toString().toLowerCase()}`, { count: recurringEventState?.interval }),
value: value.toString(),
}));
@ -55,27 +47,23 @@ export default function RecurringEventController({
<div className="flex h-5 items-center">
<input
onChange={(event) => {
setRecurringEventDefined(event?.target.checked);
onRecurringEventDefined(event?.target.checked);
if (!event?.target.checked) {
formMethods.setValue("recurringEvent", {});
formMethods.setValue("recurringEvent", null);
setRecurringEventState(null);
} else {
formMethods.setValue(
"recurringEvent",
recurringEventDefined
? recurringEvent
: {
interval: 1,
count: 12,
freq: RRuleFrequency.WEEKLY,
}
);
const newVal = recurringEvent || {
interval: 1,
count: 12,
freq: Frequency.WEEKLY,
};
formMethods.setValue("recurringEvent", newVal);
setRecurringEventState(newVal);
}
recurringEvent = formMethods.getValues("recurringEvent");
}}
type="checkbox"
className="text-primary-600 h-4 w-4 rounded border-gray-300"
defaultChecked={recurringEventDefined}
defaultChecked={recurringEventState !== null}
data-testid="recurring-event-check"
id="recurringEvent"
/>
@ -86,11 +74,8 @@ export default function RecurringEventController({
</label>
</div>
</div>
<Collapsible
open={recurringEventDefined}
data-testid="recurring-event-collapsible"
onOpenChange={() => setRecurringEventDefined(!recurringEventDefined)}>
<CollapsibleContent className="mt-4 text-sm">
{recurringEventState && (
<div data-testid="recurring-event-collapsible" className="mt-4 text-sm">
<div className="flex items-center">
<p className="mr-2 text-neutral-900">{t("repeats_every")}</p>
<input
@ -98,24 +83,28 @@ export default function RecurringEventController({
min="1"
max="20"
className="block w-16 rounded-sm border-gray-300 shadow-sm [appearance:textfield] ltr:mr-2 rtl:ml-2 sm:text-sm"
defaultValue={recurringEvent?.interval || 1}
defaultValue={recurringEventState.interval}
onChange={(event) => {
setRecurringEventInterval(parseInt(event?.target.value));
recurringEvent.interval = parseInt(event?.target.value);
formMethods.setValue("recurringEvent", recurringEvent);
const newVal = {
...recurringEventState,
interval: parseInt(event?.target.value),
};
formMethods.setValue("recurringEvent", newVal);
setRecurringEventState(newVal);
}}
/>
<Select
options={recurringEventFreqOptions}
value={recurringEventFreqOptions[recurringEventFrequency]}
value={recurringEventFreqOptions[recurringEventState.freq]}
isSearchable={false}
className="w-18 block min-w-0 rounded-sm sm:text-sm"
onChange={(e) => {
if (e?.value) {
setRecurringEventFrequency(parseInt(e?.value));
recurringEvent.freq = parseInt(e?.value);
formMethods.setValue("recurringEvent", recurringEvent);
}
onChange={(event) => {
const newVal = {
...recurringEventState,
freq: parseInt(event?.value || `${Frequency.WEEKLY}`),
};
formMethods.setValue("recurringEvent", newVal);
setRecurringEventState(newVal);
}}
/>
</div>
@ -126,21 +115,24 @@ export default function RecurringEventController({
min="1"
max="20"
className="block w-16 rounded-sm border-gray-300 shadow-sm [appearance:textfield] ltr:mr-2 rtl:ml-2 sm:text-sm"
defaultValue={recurringEvent?.count || 12}
defaultValue={recurringEventState.count}
onChange={(event) => {
setRecurringEventCount(parseInt(event?.target.value));
recurringEvent.count = parseInt(event?.target.value);
formMethods.setValue("recurringEvent", recurringEvent);
const newVal = {
...recurringEventState,
count: parseInt(event?.target.value),
};
formMethods.setValue("recurringEvent", newVal);
setRecurringEventState(newVal);
}}
/>
<p className="mr-2 text-neutral-900">
{t(`${RRuleFrequency[recurringEventFrequency].toString().toLowerCase()}`, {
count: recurringEventCount,
{t(`${Frequency[recurringEventState.freq].toString().toLowerCase()}`, {
count: recurringEventState.count,
})}
</p>
</div>
</CollapsibleContent>
</Collapsible>
</div>
)}
</>
)}
</div>

View File

@ -5,11 +5,11 @@ import Stripe from "stripe";
import EventManager from "@calcom/core/EventManager";
import { sendScheduledEmails } from "@calcom/emails";
import { isPrismaObjOrUndefined } from "@calcom/lib";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import stripe from "@calcom/stripe/server";
import { CalendarEvent, RecurringEvent } from "@calcom/types/Calendar";
import { CalendarEvent } from "@calcom/types/Calendar";
import { IS_PRODUCTION } from "@lib/config/constants";
import { HttpError as HttpCode } from "@lib/core/http/error";
@ -80,10 +80,6 @@ async function handlePaymentSuccess(event: Stripe.Event) {
});
}
const eventType = {
recurringEvent: (eventTypeRaw?.recurringEvent || {}) as RecurringEvent,
};
const { user } = booking;
if (!user) throw new Error("No user found");
@ -119,6 +115,7 @@ async function handlePaymentSuccess(event: Stripe.Event) {
attendees: attendeesList,
uid: booking.uid,
destinationCalendar: booking.destinationCalendar || user.destinationCalendar,
recurringEvent: parseRecurringEvent(eventTypeRaw?.recurringEvent),
};
if (booking.location) evt.location = booking.location;
@ -153,7 +150,7 @@ async function handlePaymentSuccess(event: Stripe.Event) {
await prisma.$transaction([paymentUpdate, bookingUpdate]);
await sendScheduledEmails({ ...evt }, eventType.recurringEvent);
await sendScheduledEmails({ ...evt });
throw new HttpCode({
statusCode: 200,

View File

@ -25,11 +25,11 @@ export const parseRecurringDates = (
startDate,
recurringEvent,
recurringCount,
}: { startDate: string | null | Dayjs; recurringEvent: RecurringEvent; recurringCount: number },
}: { startDate: string | null | Dayjs; recurringEvent: RecurringEvent | null; recurringCount: number },
i18n: I18n
): [string[], Date[]] => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { count, ...restRecurringEvent } = recurringEvent;
const { count, ...restRecurringEvent } = recurringEvent || {};
const rule = new RRule({
...restRecurringEvent,
count: recurringCount,

View File

@ -1,12 +1,11 @@
import { Prisma } from "@prisma/client";
import { UserPlan } from "@prisma/client";
import { Prisma, UserPlan } from "@prisma/client";
import { GetServerSidePropsContext } from "next";
import { JSONObject } from "superjson/dist/types";
import { locationHiddenFilter, LocationObject } from "@calcom/app-store/locations";
import { parseRecurringEvent } from "@calcom/lib";
import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defaultEvents";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RecurringEvent } from "@calcom/types/Calendar";
import { asStringOrNull } from "@lib/asStringOrNull";
import { getWorkingHours } from "@lib/availability";
@ -262,7 +261,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
metadata: (eventType.metadata || {}) as JSONObject,
periodStartDate: eventType.periodStartDate?.toString() ?? null,
periodEndDate: eventType.periodEndDate?.toString() ?? null,
recurringEvent: (eventType.recurringEvent || {}) as RecurringEvent,
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
locations: locationHiddenFilter(locations),
});

View File

@ -5,6 +5,7 @@ import { GetServerSidePropsContext } from "next";
import { JSONObject } from "superjson/dist/types";
import { getLocationLabels } from "@calcom/app-store/utils";
import { parseRecurringEvent } from "@calcom/lib";
import {
getDefaultEvent,
getDynamicEventName,
@ -12,9 +13,8 @@ import {
getUsernameList,
} from "@calcom/lib/defaultEvents";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RecurringEvent } from "@calcom/types/Calendar";
import { asStringOrThrow, asStringOrNull } from "@lib/asStringOrNull";
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
import getBooking, { GetBookingType } from "@lib/getBooking";
import prisma from "@lib/prisma";
import { inferSSRProps } from "@lib/types/inferSSRProps";
@ -162,7 +162,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
const eventType = {
...eventTypeRaw,
metadata: (eventTypeRaw.metadata || {}) as JSONObject,
recurringEvent: (eventTypeRaw.recurringEvent || {}) as RecurringEvent,
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
isWeb3Active:
web3Credentials && web3Credentials.key
? (((web3Credentials.key as JSONObject).isWeb3Active || false) as boolean)

View File

@ -4,10 +4,10 @@ import { z } from "zod";
import EventManager from "@calcom/core/EventManager";
import { sendDeclinedEmails, sendScheduledEmails } from "@calcom/emails";
import { isPrismaObjOrUndefined } from "@calcom/lib";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
import type { AdditionalInformation, CalendarEvent, RecurringEvent } from "@calcom/types/Calendar";
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
import { refund } from "@ee/lib/stripe/server";
import { getSession } from "@lib/auth";
@ -179,8 +179,7 @@ async function patchHandler(req: NextApiRequest) {
destinationCalendar: booking?.destinationCalendar || currentUser.destinationCalendar,
};
const recurringEvent = booking.eventType?.recurringEvent as RecurringEvent;
const recurringEvent = parseRecurringEvent(booking.eventType?.recurringEvent);
if (recurringEventId && recurringEvent) {
const groupedRecurringBookings = await prisma.booking.groupBy({
where: {
@ -192,6 +191,8 @@ async function patchHandler(req: NextApiRequest) {
// Overriding the recurring event configuration count to be the actual number of events booked for
// the recurring event (equal or less than recurring event configuration count)
recurringEvent.count = groupedRecurringBookings[0]._count;
// count changed, parsing again to get the new value in
evt.recurringEvent = parseRecurringEvent(recurringEvent);
}
if (confirmed) {
@ -217,10 +218,7 @@ async function patchHandler(req: NextApiRequest) {
metadata.entryPoints = results[0].createdEvent?.entryPoints;
}
try {
await sendScheduledEmails(
{ ...evt, additionalInformation: metadata },
recurringEventId ? recurringEvent : {} // Send email with recurring event info only on recurring event context
);
await sendScheduledEmails({ ...evt, additionalInformation: metadata });
} catch (error) {
log.error(error);
}
@ -267,23 +265,16 @@ async function patchHandler(req: NextApiRequest) {
evt.rejectionReason = rejectionReason;
if (recurringEventId) {
// The booking to reject is a recurring event and comes from /booking/upcoming, proceeding to mark all related
// bookings as rejected. Prisma updateMany does not support relations, so doing this in two steps for now.
const unconfirmedRecurringBookings = await prisma.booking.findMany({
// bookings as rejected.
await prisma.booking.updateMany({
where: {
recurringEventId,
status: BookingStatus.PENDING,
},
});
unconfirmedRecurringBookings.map(async (recurringBooking) => {
await prisma.booking.update({
where: {
id: recurringBooking.id,
},
data: {
status: BookingStatus.REJECTED,
rejectionReason,
},
});
data: {
status: BookingStatus.REJECTED,
rejectionReason,
},
});
} else {
await refund(booking, evt); // No payment integration for recurring events for v1
@ -298,7 +289,7 @@ async function patchHandler(req: NextApiRequest) {
});
}
await sendDeclinedEmails(evt, recurringEventId ? recurringEvent : {}); // Send email with recurring event info only on recurring event context
await sendDeclinedEmails(evt);
}
req.statusCode = 204;

View File

@ -17,12 +17,12 @@ import {
sendRescheduledEmails,
sendScheduledEmails,
} from "@calcom/emails";
import { isPrismaObjOrUndefined } from "@calcom/lib";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defaultEvents";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import logger from "@calcom/lib/logger";
import type { BufferedBusyTime } from "@calcom/types/BufferedBusyTime";
import type { AdditionalInformation, CalendarEvent, RecurringEvent } from "@calcom/types/Calendar";
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
import type { EventResult, PartialReference } from "@calcom/types/EventManager";
import { handlePayment } from "@ee/lib/stripe/server";
@ -196,7 +196,7 @@ const getEventTypesFromDB = async (eventTypeId: number) => {
return {
...eventType,
recurringEvent: (eventType.recurringEvent || undefined) as RecurringEvent,
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
};
};
@ -606,10 +606,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
let isAvailableToBeBooked = true;
try {
if (eventType.recurringEvent) {
const allBookingDates = new rrule({
dtstart: new Date(reqBody.start),
...eventType.recurringEvent,
}).all();
const recurringEvent = parseRecurringEvent(eventType.recurringEvent);
const allBookingDates = new rrule({ dtstart: new Date(reqBody.start), ...recurringEvent }).all();
// Go through each date for the recurring event and check if each one's availability
isAvailableToBeBooked = allBookingDates
.map((aDate) => isAvailable(bufferedBusyTimes, aDate, eventType.length)) // <-- array of booleans
@ -721,15 +719,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
if (noEmail !== true) {
await sendRescheduledEmails(
{
...evt,
additionalInformation: metadata,
additionalNotes, // Resets back to the additionalNote input and not the override value
cancellationReason: reqBody.rescheduleReason,
},
reqBody.recurringEventId ? (eventType.recurringEvent as RecurringEvent) : {}
);
await sendRescheduledEmails({
...evt,
additionalInformation: metadata,
additionalNotes, // Resets back to the additionalNote input and not the override value
cancellationReason: reqBody.rescheduleReason,
});
}
}
// If it's not a reschedule, doesn't require confirmation and there's no price,
@ -761,29 +756,19 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
metadata.entryPoints = results[0].createdEvent?.entryPoints;
}
if (noEmail !== true) {
await sendScheduledEmails(
{
...evt,
additionalInformation: metadata,
additionalNotes,
customInputs,
},
reqBody.recurringEventId ? (eventType.recurringEvent as RecurringEvent) : {}
);
await sendScheduledEmails({
...evt,
additionalInformation: metadata,
additionalNotes,
customInputs,
});
}
}
}
if (eventType.requiresConfirmation && !rescheduleUid && noEmail !== true) {
await sendOrganizerRequestEmail(
{ ...evt, additionalNotes },
reqBody.recurringEventId ? (eventType.recurringEvent as RecurringEvent) : {}
);
await sendAttendeeRequestEmail(
{ ...evt, additionalNotes },
attendeesList[0],
reqBody.recurringEventId ? (eventType.recurringEvent as RecurringEvent) : {}
);
await sendOrganizerRequestEmail({ ...evt, additionalNotes });
await sendAttendeeRequestEmail({ ...evt, additionalNotes }, attendeesList[0]);
}
if (

View File

@ -7,7 +7,7 @@ import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApi
import { getCalendar } from "@calcom/core/CalendarManager";
import { deleteMeeting } from "@calcom/core/videoClient";
import { sendCancelledEmails } from "@calcom/emails";
import { isPrismaObjOrUndefined } from "@calcom/lib";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { refund } from "@ee/lib/stripe/server";
@ -58,6 +58,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
paid: true,
eventType: {
select: {
recurringEvent: true,
title: true,
},
},
@ -122,6 +123,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
attendees: attendeesList,
uid: bookingToDelete?.uid,
recurringEvent: parseRecurringEvent(bookingToDelete.eventType?.recurringEvent),
location: bookingToDelete?.location,
destinationCalendar: bookingToDelete?.destinationCalendar || bookingToDelete?.user.destinationCalendar,
cancellationReason: cancellationReason,
@ -175,6 +177,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
});
// Avoiding taking care of recurrence for now as Payments are not supported with Recurring Events at the moment
if (bookingToDelete && bookingToDelete.paid) {
const evt: CalendarEvent = {
type: bookingToDelete?.eventType?.title as string,

View File

@ -3,7 +3,7 @@ import dayjs from "dayjs";
import type { NextApiRequest, NextApiResponse } from "next";
import { sendOrganizerRequestReminderEmail } from "@calcom/emails";
import { isPrismaObjOrUndefined } from "@calcom/lib";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import type { CalendarEvent } from "@calcom/types/Calendar";
@ -45,6 +45,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
destinationCalendar: true,
},
},
eventType: {
select: {
recurringEvent: true,
},
},
uid: true,
destinationCalendar: true,
},
@ -102,6 +107,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
attendees: attendeesList,
uid: booking.uid,
recurringEvent: parseRecurringEvent(booking.eventType?.recurringEvent),
destinationCalendar: booking.destinationCalendar || user.destinationCalendar,
};

View File

@ -39,6 +39,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
metadata: {},
cancellationReason: "It got late",
paymentInfo: { id: "pi_12312", link: "https://cal.com", reason: "no reason" },
recurringEvent: null,
};
req.statusCode = 200;
@ -49,7 +50,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
renderEmail("OrganizerRequestReminderEmail", {
attendee: evt.attendees[0],
calEvent: evt,
recurringEvent: {},
})
);
res.end();

View File

@ -2,7 +2,7 @@ import { Prisma } from "@prisma/client";
import { GetServerSidePropsContext } from "next";
import { JSONObject } from "superjson/dist/types";
import { RecurringEvent } from "@calcom/types/Calendar";
import { parseRecurringEvent } from "@calcom/lib";
import { asStringOrNull } from "@lib/asStringOrNull";
import { getWorkingHours } from "@lib/availability";
@ -142,7 +142,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
const [user] = users;
const eventTypeObject = Object.assign({}, hashedLink.eventType, {
metadata: {} as JSONObject,
recurringEvent: (eventTypeSelect.recurringEvent || {}) as RecurringEvent,
recurringEvent: parseRecurringEvent(hashedLink.eventType.recurringEvent),
periodStartDate: hashedLink.eventType.periodStartDate?.toString() ?? null,
periodEndDate: hashedLink.eventType.periodEndDate?.toString() ?? null,
slug,

View File

@ -6,10 +6,10 @@ import { GetServerSidePropsContext } from "next";
import { JSONObject } from "superjson/dist/types";
import { getLocationLabels } from "@calcom/app-store/utils";
import { parseRecurringEvent } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RecurringEvent } from "@calcom/types/Calendar";
import { asStringOrThrow, asStringOrNull } from "@lib/asStringOrNull";
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
import prisma from "@lib/prisma";
import { inferSSRProps } from "@lib/types/inferSSRProps";
@ -128,7 +128,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
const eventType = {
...eventTypeRaw,
metadata: (eventTypeRaw.metadata || {}) as JSONObject,
recurringEvent: (eventTypeRaw.recurringEvent || {}) as RecurringEvent,
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
isWeb3Active:
web3Credentials && web3Credentials.key
? (((web3Credentials.key as JSONObject).isWeb3Active || false) as boolean)
@ -159,7 +159,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
recurringEventCountQuery &&
(parseInt(recurringEventCountQuery) <= eventTypeObject.recurringEvent.count
? recurringEventCountQuery
: eventType.recurringEvent.count)) ||
: eventType.recurringEvent?.count)) ||
null;
return {

View File

@ -34,9 +34,11 @@ import { z } from "zod";
import { SelectGifInput } from "@calcom/app-store/giphy/components";
import getApps, { getLocationOptions } from "@calcom/app-store/utils";
import { parseRecurringEvent } from "@calcom/lib";
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import prisma from "@calcom/prisma";
import { StripeData } from "@calcom/stripe/server";
import { RecurringEvent } from "@calcom/types/Calendar";
import { Alert } from "@calcom/ui/Alert";
@ -52,7 +54,6 @@ import { getSession } from "@lib/auth";
import { HttpError } from "@lib/core/http/error";
import { isSuccessRedirectAvailable } from "@lib/isSuccessRedirectAvailable";
import { LocationObject, LocationType } from "@lib/location";
import prisma from "@lib/prisma";
import { slugify } from "@lib/slugify";
import { trpc } from "@lib/trpc";
import { inferSSRProps } from "@lib/types/inferSSRProps";
@ -112,7 +113,7 @@ export type FormValues = {
description: string;
disableGuests: boolean;
requiresConfirmation: boolean;
recurringEvent: RecurringEvent;
recurringEvent: RecurringEvent | null;
schedulingType: SchedulingType | null;
price: number;
currency: string;
@ -477,7 +478,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
const formMethods = useForm<FormValues>({
defaultValues: {
locations: eventType.locations || [],
recurringEvent: eventType.recurringEvent || {},
recurringEvent: eventType.recurringEvent || null,
schedule: eventType.schedule?.id,
periodDates: {
startDate: periodDates.startDate,
@ -2249,7 +2250,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
const { locations, metadata, ...restEventType } = rawEventType;
const eventType = {
...restEventType,
recurringEvent: (restEventType.recurringEvent || {}) as RecurringEvent,
recurringEvent: parseRecurringEvent(restEventType.recurringEvent),
locations: locations as unknown as LocationObject[],
metadata: (metadata || {}) as JSONObject,
isWeb3Active:

View File

@ -23,11 +23,11 @@ import {
useIsBackgroundTransparent,
useIsEmbed,
} from "@calcom/embed-core/embed-iframe";
import { parseRecurringEvent } from "@calcom/lib";
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { localStorage } from "@calcom/lib/webstorage";
import { Prisma } from "@calcom/prisma/client";
import { RecurringEvent } from "@calcom/types/Calendar";
import Button from "@calcom/ui/Button";
import { EmailInput } from "@calcom/ui/form/fields";
@ -813,7 +813,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
const eventType = {
...eventTypeRaw,
recurringEvent: (eventTypeRaw.recurringEvent || {}) as RecurringEvent,
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
};
const profile = {

View File

@ -2,7 +2,7 @@ import { UserPlan } from "@prisma/client";
import { GetServerSidePropsContext } from "next";
import { JSONObject } from "superjson/dist/types";
import { RecurringEvent } from "@calcom/types/Calendar";
import { parseRecurringEvent } from "@calcom/lib";
import { asStringOrNull } from "@lib/asStringOrNull";
import { getWorkingHours } from "@lib/availability";
@ -115,7 +115,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
metadata: (eventType.metadata || {}) as JSONObject,
periodStartDate: eventType.periodStartDate?.toString() ?? null,
periodEndDate: eventType.periodEndDate?.toString() ?? null,
recurringEvent: (eventType.recurringEvent || {}) as RecurringEvent,
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
locations: locationHiddenFilter(locations),
});

View File

@ -2,8 +2,8 @@ import { GetServerSidePropsContext } from "next";
import { JSONObject } from "superjson/dist/types";
import { getLocationLabels } from "@calcom/app-store/utils";
import { parseRecurringEvent } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RecurringEvent } from "@calcom/types/Calendar";
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
import getBooking, { GetBookingType } from "@lib/getBooking";
@ -75,7 +75,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
const eventType = {
...eventTypeRaw,
recurringEvent: (eventTypeRaw.recurringEvent || {}) as RecurringEvent,
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
};
const eventTypeObject = [eventType].map((e) => {

View File

@ -68,9 +68,9 @@ test.describe("Event Types tests", () => {
});
await page.click("[data-testid=show-advanced-settings]");
await expect(page.locator("[data-testid=recurring-event-collapsible] > *")).not.toBeVisible();
await expect(page.locator("[data-testid=recurring-event-collapsible]")).not.toBeVisible();
await page.click("[data-testid=recurring-event-check]");
isCreated = page.locator("[data-testid=recurring-event-collapsible] > *");
isCreated = page.locator("[data-testid=recurring-event-collapsible]");
await expect(isCreated).toBeVisible();
expect(

View File

@ -8,8 +8,8 @@ import getApps, { getLocationOptions } from "@calcom/app-store/utils";
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
import { checkPremiumUsername } from "@calcom/ee/lib/core/checkPremiumUsername";
import { sendFeedbackEmail } from "@calcom/emails";
import { parseRecurringEvent } from "@calcom/lib";
import { baseEventTypeSelect, bookingMinimalSelect } from "@calcom/prisma";
import { RecurringEvent } from "@calcom/types/Calendar";
import { checkRegularUsername } from "@lib/core/checkRegularUsername";
import jackson from "@lib/jackson";
@ -429,7 +429,7 @@ const loggedInViewerRouter = createProtectedRouter()
...booking,
eventType: {
...booking.eventType,
recurringEvent: ((booking.eventType && booking.eventType.recurringEvent) || {}) as RecurringEvent,
recurringEvent: parseRecurringEvent(booking.eventType?.recurringEvent),
},
startTime: booking.startTime.toISOString(),
endTime: booking.endTime.toISOString(),

View File

@ -4,6 +4,7 @@ import { z } from "zod";
import EventManager from "@calcom/core/EventManager";
import { sendLocationChangeEmails } from "@calcom/emails";
import { parseRecurringEvent } from "@calcom/lib";
import logger from "@calcom/lib/logger";
import { getTranslation } from "@calcom/lib/server/i18n";
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
@ -118,6 +119,7 @@ export const bookingsRouter = createProtectedRouter()
},
attendees: attendeesList,
uid: booking.uid,
recurringEvent: parseRecurringEvent(booking.eventType?.recurringEvent),
location,
destinationCalendar: booking?.destinationCalendar || booking?.user?.destinationCalendar,
};

View File

@ -283,6 +283,8 @@ export const eventTypesRouter = createProtectedRouter()
until: recurringEvent.until as unknown as Prisma.InputJsonObject,
tzid: recurringEvent.tzid,
};
} else {
data.recurringEvent = Prisma.DbNull;
}
if (destinationCalendar) {

View File

@ -1,28 +1,46 @@
import { UserPlan } from "@prisma/client";
import { getLuckyUsers } from "../../pages/api/book/event";
const baseUser = {
id: 0,
username: "test",
name: "Test User",
credentials: [],
timeZone: "GMT",
bufferTime: 0,
email: "test@example.com",
destinationCalendar: null,
locale: "en",
theme: null,
brandColor: "#292929",
darkBrandColor: "#fafafa",
availability: [],
selectedCalendars: [],
startTime: 0,
endTime: 0,
schedules: [],
defaultScheduleId: null,
plan: UserPlan.PRO,
avatar: "",
hideBranding: true,
};
it("can find lucky users", async () => {
const users = [
{
...baseUser,
id: 1,
username: "test",
name: "Test User",
credentials: [],
timeZone: "GMT",
bufferTime: 0,
email: "test@example.com",
destinationCalendar: null,
locale: "en",
},
{
...baseUser,
id: 2,
username: "test2",
name: "Test 2 User",
credentials: [],
timeZone: "GMT",
bufferTime: 0,
email: "test2@example.com",
destinationCalendar: null,
locale: "en",
},
];
expect(

View File

@ -1,4 +1,4 @@
import type { CalendarEvent, Person, RecurringEvent } from "@calcom/types/Calendar";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
import AttendeeAwaitingPaymentEmail from "./templates/attendee-awaiting-payment-email";
import AttendeeCancelledEmail from "./templates/attendee-cancelled-email";
@ -20,14 +20,14 @@ import OrganizerRescheduledEmail from "./templates/organizer-rescheduled-email";
import OrganizerScheduledEmail from "./templates/organizer-scheduled-email";
import TeamInviteEmail, { TeamInvite } from "./templates/team-invite-email";
export const sendScheduledEmails = async (calEvent: CalendarEvent, recurringEvent: RecurringEvent = {}) => {
export const sendScheduledEmails = async (calEvent: CalendarEvent) => {
const emailsToSend: Promise<unknown>[] = [];
emailsToSend.push(
...calEvent.attendees.map((attendee) => {
return new Promise((resolve, reject) => {
try {
const scheduledEmail = new AttendeeScheduledEmail(calEvent, attendee, recurringEvent);
const scheduledEmail = new AttendeeScheduledEmail(calEvent, attendee);
resolve(scheduledEmail.sendEmail());
} catch (e) {
reject(console.error("AttendeeRescheduledEmail.sendEmail failed", e));
@ -39,7 +39,7 @@ export const sendScheduledEmails = async (calEvent: CalendarEvent, recurringEven
emailsToSend.push(
new Promise((resolve, reject) => {
try {
const scheduledEmail = new OrganizerScheduledEmail(calEvent, recurringEvent);
const scheduledEmail = new OrganizerScheduledEmail(calEvent);
resolve(scheduledEmail.sendEmail());
} catch (e) {
reject(console.error("OrganizerScheduledEmail.sendEmail failed", e));
@ -50,14 +50,14 @@ export const sendScheduledEmails = async (calEvent: CalendarEvent, recurringEven
await Promise.all(emailsToSend);
};
export const sendRescheduledEmails = async (calEvent: CalendarEvent, recurringEvent: RecurringEvent = {}) => {
export const sendRescheduledEmails = async (calEvent: CalendarEvent) => {
const emailsToSend: Promise<unknown>[] = [];
emailsToSend.push(
...calEvent.attendees.map((attendee) => {
return new Promise((resolve, reject) => {
try {
const scheduledEmail = new AttendeeRescheduledEmail(calEvent, attendee, recurringEvent);
const scheduledEmail = new AttendeeRescheduledEmail(calEvent, attendee);
resolve(scheduledEmail.sendEmail());
} catch (e) {
reject(console.error("AttendeeRescheduledEmail.sendEmail failed", e));
@ -69,7 +69,7 @@ export const sendRescheduledEmails = async (calEvent: CalendarEvent, recurringEv
emailsToSend.push(
new Promise((resolve, reject) => {
try {
const scheduledEmail = new OrganizerRescheduledEmail(calEvent, recurringEvent);
const scheduledEmail = new OrganizerRescheduledEmail(calEvent);
resolve(scheduledEmail.sendEmail());
} catch (e) {
reject(console.error("OrganizerScheduledEmail.sendEmail failed", e));
@ -80,13 +80,10 @@ export const sendRescheduledEmails = async (calEvent: CalendarEvent, recurringEv
await Promise.all(emailsToSend);
};
export const sendOrganizerRequestEmail = async (
calEvent: CalendarEvent,
recurringEvent: RecurringEvent = {}
) => {
export const sendOrganizerRequestEmail = async (calEvent: CalendarEvent) => {
await new Promise((resolve, reject) => {
try {
const organizerRequestEmail = new OrganizerRequestEmail(calEvent, recurringEvent);
const organizerRequestEmail = new OrganizerRequestEmail(calEvent);
resolve(organizerRequestEmail.sendEmail());
} catch (e) {
reject(console.error("OrganizerRequestEmail.sendEmail failed", e));
@ -94,14 +91,10 @@ export const sendOrganizerRequestEmail = async (
});
};
export const sendAttendeeRequestEmail = async (
calEvent: CalendarEvent,
attendee: Person,
recurringEvent: RecurringEvent = {}
) => {
export const sendAttendeeRequestEmail = async (calEvent: CalendarEvent, attendee: Person) => {
await new Promise((resolve, reject) => {
try {
const attendeeRequestEmail = new AttendeeRequestEmail(calEvent, attendee, recurringEvent);
const attendeeRequestEmail = new AttendeeRequestEmail(calEvent, attendee);
resolve(attendeeRequestEmail.sendEmail());
} catch (e) {
reject(console.error("AttendRequestEmail.sendEmail failed", e));
@ -109,14 +102,14 @@ export const sendAttendeeRequestEmail = async (
});
};
export const sendDeclinedEmails = async (calEvent: CalendarEvent, recurringEvent: RecurringEvent = {}) => {
export const sendDeclinedEmails = async (calEvent: CalendarEvent) => {
const emailsToSend: Promise<unknown>[] = [];
emailsToSend.push(
...calEvent.attendees.map((attendee) => {
return new Promise((resolve, reject) => {
try {
const declinedEmail = new AttendeeDeclinedEmail(calEvent, attendee, recurringEvent);
const declinedEmail = new AttendeeDeclinedEmail(calEvent, attendee);
resolve(declinedEmail.sendEmail());
} catch (e) {
reject(console.error("AttendeeRescheduledEmail.sendEmail failed", e));
@ -128,14 +121,14 @@ export const sendDeclinedEmails = async (calEvent: CalendarEvent, recurringEvent
await Promise.all(emailsToSend);
};
export const sendCancelledEmails = async (calEvent: CalendarEvent, recurringEvent: RecurringEvent = {}) => {
export const sendCancelledEmails = async (calEvent: CalendarEvent) => {
const emailsToSend: Promise<unknown>[] = [];
emailsToSend.push(
...calEvent.attendees.map((attendee) => {
return new Promise((resolve, reject) => {
try {
const scheduledEmail = new AttendeeCancelledEmail(calEvent, attendee, recurringEvent);
const scheduledEmail = new AttendeeCancelledEmail(calEvent, attendee);
resolve(scheduledEmail.sendEmail());
} catch (e) {
reject(console.error("AttendeeCancelledEmail.sendEmail failed", e));
@ -147,7 +140,7 @@ export const sendCancelledEmails = async (calEvent: CalendarEvent, recurringEven
emailsToSend.push(
new Promise((resolve, reject) => {
try {
const scheduledEmail = new OrganizerCancelledEmail(calEvent, recurringEvent);
const scheduledEmail = new OrganizerCancelledEmail(calEvent);
resolve(scheduledEmail.sendEmail());
} catch (e) {
reject(console.error("OrganizerCancelledEmail.sendEmail failed", e));
@ -158,13 +151,10 @@ export const sendCancelledEmails = async (calEvent: CalendarEvent, recurringEven
await Promise.all(emailsToSend);
};
export const sendOrganizerRequestReminderEmail = async (
calEvent: CalendarEvent,
recurringEvent: RecurringEvent = {}
) => {
export const sendOrganizerRequestReminderEmail = async (calEvent: CalendarEvent) => {
await new Promise((resolve, reject) => {
try {
const organizerRequestReminderEmail = new OrganizerRequestReminderEmail(calEvent, recurringEvent);
const organizerRequestReminderEmail = new OrganizerRequestReminderEmail(calEvent);
resolve(organizerRequestReminderEmail.sendEmail());
} catch (e) {
reject(console.error("OrganizerRequestReminderEmail.sendEmail failed", e));
@ -172,17 +162,14 @@ export const sendOrganizerRequestReminderEmail = async (
});
};
export const sendAwaitingPaymentEmail = async (
calEvent: CalendarEvent,
recurringEvent: RecurringEvent = {}
) => {
export const sendAwaitingPaymentEmail = async (calEvent: CalendarEvent) => {
const emailsToSend: Promise<unknown>[] = [];
emailsToSend.push(
...calEvent.attendees.map((attendee) => {
return new Promise((resolve, reject) => {
try {
const paymentEmail = new AttendeeAwaitingPaymentEmail(calEvent, attendee, recurringEvent);
const paymentEmail = new AttendeeAwaitingPaymentEmail(calEvent, attendee);
resolve(paymentEmail.sendEmail());
} catch (e) {
reject(console.error("AttendeeAwaitingPaymentEmail.sendEmail failed", e));
@ -194,13 +181,10 @@ export const sendAwaitingPaymentEmail = async (
await Promise.all(emailsToSend);
};
export const sendOrganizerPaymentRefundFailedEmail = async (
calEvent: CalendarEvent,
recurringEvent: RecurringEvent = {}
) => {
export const sendOrganizerPaymentRefundFailedEmail = async (calEvent: CalendarEvent) => {
await new Promise((resolve, reject) => {
try {
const paymentRefundFailedEmail = new OrganizerPaymentRefundFailedEmail(calEvent, recurringEvent);
const paymentRefundFailedEmail = new OrganizerPaymentRefundFailedEmail(calEvent);
resolve(paymentRefundFailedEmail.sendEmail());
} catch (e) {
reject(console.error("OrganizerPaymentRefundFailedEmail.sendEmail failed", e));
@ -232,19 +216,14 @@ export const sendTeamInviteEmail = async (teamInviteEvent: TeamInvite) => {
export const sendRequestRescheduleEmail = async (
calEvent: CalendarEvent,
metadata: { rescheduleLink: string },
recurringEvent: RecurringEvent = {}
metadata: { rescheduleLink: string }
) => {
const emailsToSend: Promise<unknown>[] = [];
emailsToSend.push(
new Promise((resolve, reject) => {
try {
const requestRescheduleEmail = new AttendeeRequestRescheduledEmail(
calEvent,
metadata,
recurringEvent
);
const requestRescheduleEmail = new AttendeeRequestRescheduledEmail(calEvent, metadata);
resolve(requestRescheduleEmail.sendEmail());
} catch (e) {
reject(console.error("AttendeeRequestRescheduledEmail.sendEmail failed", e));
@ -255,11 +234,7 @@ export const sendRequestRescheduleEmail = async (
emailsToSend.push(
new Promise((resolve, reject) => {
try {
const requestRescheduleEmail = new OrganizerRequestRescheduleEmail(
calEvent,
metadata,
recurringEvent
);
const requestRescheduleEmail = new OrganizerRequestRescheduleEmail(calEvent, metadata);
resolve(requestRescheduleEmail.sendEmail());
} catch (e) {
reject(console.error("OrganizerRequestRescheduledEmail.sendEmail failed", e));
@ -270,17 +245,14 @@ export const sendRequestRescheduleEmail = async (
await Promise.all(emailsToSend);
};
export const sendLocationChangeEmails = async (
calEvent: CalendarEvent,
recurringEvent: RecurringEvent = {}
) => {
export const sendLocationChangeEmails = async (calEvent: CalendarEvent) => {
const emailsToSend: Promise<unknown>[] = [];
emailsToSend.push(
...calEvent.attendees.map((attendee) => {
return new Promise((resolve, reject) => {
try {
const scheduledEmail = new AttendeeLocationChangeEmail(calEvent, attendee, recurringEvent);
const scheduledEmail = new AttendeeLocationChangeEmail(calEvent, attendee);
resolve(scheduledEmail.sendEmail());
} catch (e) {
reject(console.error("AttendeeLocationChangeEmail.sendEmail failed", e));
@ -292,7 +264,7 @@ export const sendLocationChangeEmails = async (
emailsToSend.push(
new Promise((resolve, reject) => {
try {
const scheduledEmail = new OrganizerLocationChangeEmail(calEvent, recurringEvent);
const scheduledEmail = new OrganizerLocationChangeEmail(calEvent);
resolve(scheduledEmail.sendEmail());
} catch (e) {
reject(console.error("OrganizerLocationChangeEmail.sendEmail failed", e));

View File

@ -3,31 +3,26 @@ import timezone from "dayjs/plugin/timezone";
import { TFunction } from "next-i18next";
import rrule from "rrule";
import type { CalendarEvent, RecurringEvent } from "@calcom/types/Calendar";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { Info } from "./Info";
dayjs.extend(timezone);
function getRecurringWhen(props: { calEvent: CalendarEvent; recurringEvent: RecurringEvent }) {
function getRecurringWhen(props: { calEvent: CalendarEvent }) {
const t = props.calEvent.attendees[0].language.translate;
return props.recurringEvent?.count && props.recurringEvent?.freq
const { calEvent: { recurringEvent } = {} } = props;
return recurringEvent?.count && recurringEvent?.freq
? ` - ${t("every_for_freq", {
freq: t(`${rrule.FREQUENCIES[props.recurringEvent.freq].toString().toLowerCase()}`),
})} ${props.recurringEvent.count} ${t(
`${rrule.FREQUENCIES[props.recurringEvent.freq].toString().toLowerCase()}`,
{ count: props.recurringEvent.count }
)}`
freq: t(`${rrule.FREQUENCIES[recurringEvent.freq].toString().toLowerCase()}`),
})} ${recurringEvent.count} ${t(`${rrule.FREQUENCIES[recurringEvent.freq].toString().toLowerCase()}`, {
count: recurringEvent.count,
})}`
: "";
}
export function WhenInfo(props: {
calEvent: CalendarEvent;
recurringEvent: RecurringEvent;
timeZone: string;
t: TFunction;
}) {
const { timeZone, t } = props;
export function WhenInfo(props: { calEvent: CalendarEvent; timeZone: string; t: TFunction }) {
const { timeZone, t, calEvent: { recurringEvent } = {} } = props;
function getRecipientStart(format: string) {
return dayjs(props.calEvent.startTime).tz(timeZone).format(format);
@ -44,7 +39,7 @@ export function WhenInfo(props: {
lineThrough={!!props.calEvent.cancellationReason}
description={
<>
{props.recurringEvent?.count ? `${t("starting")} ` : ""}
{recurringEvent?.count ? `${t("starting")} ` : ""}
{t(getRecipientStart("dddd").toLowerCase())}, {t(getRecipientStart("MMMM").toLowerCase())}{" "}
{getRecipientStart("D, YYYY | h:mma")} - {getRecipientEnd("h:mma")}{" "}
<span style={{ color: "#888888" }}>({timeZone})</span>

View File

@ -2,7 +2,9 @@ import { AttendeeScheduledEmail } from "./AttendeeScheduledEmail";
export const AttendeeDeclinedEmail = (props: React.ComponentProps<typeof AttendeeScheduledEmail>) => (
<AttendeeScheduledEmail
title={props.recurringEvent?.count ? "event_request_declined_recurring" : "event_request_declined"}
title={
props.calEvent.recurringEvent?.count ? "event_request_declined_recurring" : "event_request_declined"
}
headerType="xCircle"
subject="event_declined_subject"
callToAction={null}

View File

@ -3,10 +3,10 @@ import { AttendeeScheduledEmail } from "./AttendeeScheduledEmail";
export const AttendeeRequestEmail = (props: React.ComponentProps<typeof AttendeeScheduledEmail>) => (
<AttendeeScheduledEmail
title={props.calEvent.organizer.language.translate(
props.recurringEvent?.count ? "booking_submitted_recurring" : "booking_submitted"
props.calEvent.recurringEvent?.count ? "booking_submitted_recurring" : "booking_submitted"
)}
subtitle={props.calEvent.organizer.language.translate(
props.recurringEvent.count
props.calEvent.recurringEvent?.count
? "user_needs_to_confirm_or_reject_booking_recurring"
: "user_needs_to_confirm_or_reject_booking",
{ user: props.calEvent.organizer.name }

View File

@ -1,4 +1,4 @@
import type { CalendarEvent, Person, RecurringEvent } from "@calcom/types/Calendar";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
import { BaseScheduledEmail } from "./BaseScheduledEmail";
@ -6,7 +6,6 @@ export const AttendeeScheduledEmail = (
props: {
calEvent: CalendarEvent;
attendee: Person;
recurringEvent: RecurringEvent;
} & Partial<React.ComponentProps<typeof BaseScheduledEmail>>
) => {
return (

View File

@ -5,7 +5,7 @@ import toArray from "dayjs/plugin/toArray";
import utc from "dayjs/plugin/utc";
import type { TFunction } from "next-i18next";
import type { CalendarEvent, Person, RecurringEvent } from "@calcom/types/Calendar";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
import {
BaseEmailHtml,
@ -26,7 +26,6 @@ export const BaseScheduledEmail = (
props: {
calEvent: CalendarEvent;
attendee: Person;
recurringEvent: RecurringEvent;
timeZone: string;
t: TFunction;
} & Partial<React.ComponentProps<typeof BaseEmailHtml>>
@ -56,7 +55,7 @@ export const BaseScheduledEmail = (
title={t(
props.title
? props.title
: props.recurringEvent?.count
: props.calEvent.recurringEvent?.count
? "your_event_has_been_scheduled_recurring"
: "your_event_has_been_scheduled"
)}
@ -69,7 +68,7 @@ export const BaseScheduledEmail = (
<Info label={t("cancellation_reason")} description={props.calEvent.cancellationReason} withSpacer />
<Info label={t("rejection_reason")} description={props.calEvent.rejectionReason} withSpacer />
<Info label={t("what")} description={props.calEvent.type} withSpacer />
<WhenInfo calEvent={props.calEvent} recurringEvent={props.recurringEvent} t={t} timeZone={timeZone} />
<WhenInfo calEvent={props.calEvent} t={t} timeZone={timeZone} />
<WhoInfo calEvent={props.calEvent} t={t} />
<LocationInfo calEvent={props.calEvent} t={t} />
<Info label={t("description")} description={props.calEvent.description} withSpacer />

View File

@ -4,7 +4,7 @@ import { OrganizerScheduledEmail } from "./OrganizerScheduledEmail";
export const OrganizerRequestEmail = (props: React.ComponentProps<typeof OrganizerScheduledEmail>) => (
<OrganizerScheduledEmail
title={
props.title || props.recurringEvent?.count
props.title || props.calEvent.recurringEvent?.count
? "event_awaiting_approval_recurring"
: "event_awaiting_approval"
}

View File

@ -1,4 +1,4 @@
import type { CalendarEvent, Person, RecurringEvent } from "@calcom/types/Calendar";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
import { BaseScheduledEmail } from "./BaseScheduledEmail";
@ -6,7 +6,6 @@ export const OrganizerScheduledEmail = (
props: {
calEvent: CalendarEvent;
attendee: Person;
recurringEvent: RecurringEvent;
} & Partial<React.ComponentProps<typeof BaseScheduledEmail>>
) => {
const t = props.calEvent.organizer.language.translate;
@ -15,7 +14,9 @@ export const OrganizerScheduledEmail = (
timeZone={props.calEvent.organizer.timeZone}
t={t}
subject={t("confirmed_event_type_subject")}
title={t(props.recurringEvent?.count ? "new_event_scheduled_recurring" : "new_event_scheduled")}
title={t(
props.calEvent.recurringEvent?.count ? "new_event_scheduled_recurring" : "new_event_scheduled"
)}
{...props}
/>
);

View File

@ -15,7 +15,6 @@ export default class AttendeeAwaitingPaymentEmail extends AttendeeScheduledEmail
html: renderEmail("AttendeeAwaitingPaymentEmail", {
calEvent: this.calEvent,
attendee: this.attendee,
recurringEvent: this.recurringEvent,
}),
text: this.getTextBody("meeting_awaiting_payment"),
};

View File

@ -15,7 +15,6 @@ export default class AttendeeCancelledEmail extends AttendeeScheduledEmail {
html: renderEmail("AttendeeCancelledEmail", {
calEvent: this.calEvent,
attendee: this.attendee,
recurringEvent: this.recurringEvent,
}),
text: this.getTextBody("event_request_cancelled", "emailed_you_and_any_other_attendees"),
};

View File

@ -15,10 +15,9 @@ export default class AttendeeDeclinedEmail extends AttendeeScheduledEmail {
html: renderEmail("AttendeeDeclinedEmail", {
calEvent: this.calEvent,
attendee: this.attendee,
recurringEvent: this.recurringEvent,
}),
text: this.getTextBody(
this.recurringEvent?.count ? "event_request_declined_recurring" : "event_request_declined"
this.calEvent.recurringEvent?.count ? "event_request_declined_recurring" : "event_request_declined"
),
};
}

View File

@ -19,7 +19,6 @@ export default class AttendeeLocationChangeEmail extends AttendeeScheduledEmail
html: renderEmail("AttendeeLocationChangeEmail", {
calEvent: this.calEvent,
attendee: this.attendee,
recurringEvent: this.recurringEvent,
}),
text: this.getTextBody("event_location_changed"),
};

View File

@ -24,7 +24,6 @@ export default class AttendeeRequestEmail extends AttendeeScheduledEmail {
html: renderEmail("AttendeeRequestEmail", {
calEvent: this.calEvent,
attendee: this.attendee,
recurringEvent: this.recurringEvent,
}),
text: this.getTextBody(
this.calEvent.attendees[0].language.translate("booking_submitted", {

View File

@ -5,10 +5,10 @@ import toArray from "dayjs/plugin/toArray";
import utc from "dayjs/plugin/utc";
import { createEvent, DateArray, Person } from "ics";
import { renderEmail } from "../";
import { getCancelLink } from "@calcom/lib/CalEventParser";
import { CalendarEvent, RecurringEvent } from "@calcom/types/Calendar";
import { CalendarEvent } from "@calcom/types/Calendar";
import { renderEmail } from "../";
import OrganizerScheduledEmail from "./organizer-scheduled-email";
dayjs.extend(utc);
@ -18,8 +18,8 @@ dayjs.extend(toArray);
export default class AttendeeRequestRescheduledEmail extends OrganizerScheduledEmail {
private metadata: { rescheduleLink: string };
constructor(calEvent: CalendarEvent, metadata: { rescheduleLink: string }, recurringEvent: RecurringEvent) {
super(calEvent, recurringEvent);
constructor(calEvent: CalendarEvent, metadata: { rescheduleLink: string }) {
super(calEvent);
this.metadata = metadata;
}
protected getNodeMailerPayload(): Record<string, unknown> {
@ -40,7 +40,6 @@ export default class AttendeeRequestRescheduledEmail extends OrganizerScheduledE
calEvent: this.calEvent,
attendee: this.calEvent.organizer,
metadata: this.metadata,
recurringEvent: this.recurringEvent,
}),
text: this.getTextBody(),
};

View File

@ -19,7 +19,6 @@ export default class AttendeeRescheduledEmail extends AttendeeScheduledEmail {
html: renderEmail("AttendeeCancelledEmail", {
calEvent: this.calEvent,
attendee: this.attendee,
recurringEvent: this.recurringEvent,
}),
text: this.getTextBody("event_has_been_rescheduled", "emailed_you_and_any_other_attendees"),
};

View File

@ -8,7 +8,7 @@ import { TFunction } from "next-i18next";
import rrule from "rrule";
import { getRichDescription } from "@calcom/lib/CalEventParser";
import type { CalendarEvent, Person, RecurringEvent } from "@calcom/types/Calendar";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
import { renderEmail } from "../";
import BaseEmail from "./_base-email";
@ -21,23 +21,22 @@ dayjs.extend(toArray);
export default class AttendeeScheduledEmail extends BaseEmail {
calEvent: CalendarEvent;
attendee: Person;
recurringEvent: RecurringEvent;
t: TFunction;
constructor(calEvent: CalendarEvent, attendee: Person, recurringEvent: RecurringEvent) {
constructor(calEvent: CalendarEvent, attendee: Person) {
super();
this.name = "SEND_BOOKING_CONFIRMATION";
this.calEvent = calEvent;
this.attendee = attendee;
this.t = attendee.language.translate;
this.recurringEvent = recurringEvent;
}
protected getiCalEventAsString(): string | undefined {
// Taking care of recurrence rule beforehand
let recurrenceRule: string | undefined = undefined;
if (this.recurringEvent?.count) {
recurrenceRule = new rrule(this.recurringEvent).toString().replace("RRULE:", "");
if (this.calEvent.recurringEvent?.count) {
// ics appends "RRULE:" already, so removing it from RRule generated string
recurrenceRule = new rrule(this.calEvent.recurringEvent).toString().replace("RRULE:", "");
}
const icsEvent = createEvent({
start: dayjs(this.calEvent.startTime)
@ -84,7 +83,6 @@ export default class AttendeeScheduledEmail extends BaseEmail {
html: renderEmail("AttendeeScheduledEmail", {
calEvent: this.calEvent,
attendee: this.attendee,
recurringEvent: this.recurringEvent,
}),
text: this.getTextBody(),
};
@ -93,7 +91,7 @@ export default class AttendeeScheduledEmail extends BaseEmail {
protected getTextBody(title = "", subtitle = "emailed_you_and_any_other_attendees"): string {
return `
${this.t(
title || this.recurringEvent?.count
title || this.calEvent.recurringEvent?.count
? "your_event_has_been_scheduled_recurring"
: "your_event_has_been_scheduled"
)}

View File

@ -24,7 +24,6 @@ export default class OrganizerCancelledEmail extends OrganizerScheduledEmail {
html: renderEmail("OrganizerCancelledEmail", {
attendee: this.calEvent.organizer,
calEvent: this.calEvent,
recurringEvent: this.recurringEvent,
}),
text: this.getTextBody("event_request_cancelled"),
};

View File

@ -28,7 +28,6 @@ export default class OrganizerLocationChangeEmail extends OrganizerScheduledEmai
html: renderEmail("OrganizerLocationChangeEmail", {
attendee: this.calEvent.organizer,
calEvent: this.calEvent,
recurringEvent: this.recurringEvent,
}),
text: this.getTextBody("event_location_changed"),
};

View File

@ -1,5 +1,4 @@
import { renderEmail } from "../";
import OrganizerScheduledEmail from "./organizer-scheduled-email";
export default class OrganizerPaymentRefundFailedEmail extends OrganizerScheduledEmail {
@ -25,7 +24,6 @@ export default class OrganizerPaymentRefundFailedEmail extends OrganizerSchedule
html: renderEmail("OrganizerPaymentRefundFailedEmail", {
calEvent: this.calEvent,
attendee: this.calEvent.organizer,
recurringEvent: this.recurringEvent,
}),
text: this.getTextBody(
"a_refund_failed",

View File

@ -24,7 +24,6 @@ export default class OrganizerRequestEmail extends OrganizerScheduledEmail {
html: renderEmail("OrganizerRequestEmail", {
calEvent: this.calEvent,
attendee: this.calEvent.organizer,
recurringEvent: this.recurringEvent,
}),
text: this.getTextBody("event_awaiting_approval"),
};

View File

@ -24,7 +24,6 @@ export default class OrganizerRequestReminderEmail extends OrganizerRequestEmail
html: renderEmail("OrganizerRequestReminderEmail", {
calEvent: this.calEvent,
attendee: this.calEvent.organizer,
recurringEvent: this.recurringEvent,
}),
text: this.getTextBody("event_still_awaiting_approval"),
};

View File

@ -5,9 +5,9 @@ import toArray from "dayjs/plugin/toArray";
import utc from "dayjs/plugin/utc";
import { createEvent, DateArray, Person } from "ics";
import { renderEmail } from "../";
import { CalendarEvent, RecurringEvent } from "@calcom/types/Calendar";
import { CalendarEvent } from "@calcom/types/Calendar";
import { renderEmail } from "../";
import OrganizerScheduledEmail from "./organizer-scheduled-email";
dayjs.extend(utc);
@ -17,8 +17,8 @@ dayjs.extend(toArray);
export default class OrganizerRequestRescheduledEmail extends OrganizerScheduledEmail {
private metadata: { rescheduleLink: string };
constructor(calEvent: CalendarEvent, metadata: { rescheduleLink: string }, recurringEvent: RecurringEvent) {
super(calEvent, recurringEvent);
constructor(calEvent: CalendarEvent, metadata: { rescheduleLink: string }) {
super(calEvent);
this.metadata = metadata;
}
protected getNodeMailerPayload(): Record<string, unknown> {
@ -39,7 +39,6 @@ export default class OrganizerRequestRescheduledEmail extends OrganizerScheduled
html: renderEmail("OrganizerScheduledEmail", {
calEvent: this.calEvent,
attendee: this.calEvent.organizer,
recurringEvent: this.recurringEvent,
}),
text: this.getTextBody(
this.t("request_reschedule_title_organizer", {

View File

@ -34,7 +34,6 @@ export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail {
html: renderEmail("OrganizerRescheduledEmail", {
calEvent: this.calEvent,
attendee: this.calEvent.organizer,
recurringEvent: this.recurringEvent,
}),
text: this.getTextBody("event_has_been_rescheduled"),
};

View File

@ -8,7 +8,7 @@ import { TFunction } from "next-i18next";
import rrule from "rrule";
import { getRichDescription } from "@calcom/lib/CalEventParser";
import type { CalendarEvent, RecurringEvent } from "@calcom/types/Calendar";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { renderEmail } from "../";
import BaseEmail from "./_base-email";
@ -20,22 +20,22 @@ dayjs.extend(toArray);
export default class OrganizerScheduledEmail extends BaseEmail {
calEvent: CalendarEvent;
recurringEvent: RecurringEvent;
t: TFunction;
constructor(calEvent: CalendarEvent, recurringEvent: RecurringEvent) {
constructor(calEvent: CalendarEvent) {
super();
this.name = "SEND_BOOKING_CONFIRMATION";
this.calEvent = calEvent;
this.recurringEvent = recurringEvent;
this.t = this.calEvent.organizer.language.translate;
}
protected getiCalEventAsString(): string | undefined {
// Taking care of recurrence rule beforehand
let recurrenceRule: string | undefined = undefined;
if (this.recurringEvent?.count) {
recurrenceRule = new rrule(this.recurringEvent).toString().replace("RRULE:", "");
if (this.calEvent.recurringEvent?.count) {
// ics appends "RRULE:" already, so removing it from RRule generated string
recurrenceRule = new rrule(this.calEvent.recurringEvent).toString().replace("RRULE:", "");
}
const icsEvent = createEvent({
start: dayjs(this.calEvent.startTime)
@ -91,7 +91,6 @@ export default class OrganizerScheduledEmail extends BaseEmail {
html: renderEmail("OrganizerScheduledEmail", {
calEvent: this.calEvent,
attendee: this.calEvent.organizer,
recurringEvent: this.recurringEvent,
}),
text: this.getTextBody(),
};
@ -104,7 +103,9 @@ export default class OrganizerScheduledEmail extends BaseEmail {
callToAction = ""
): string {
return `
${this.t(title || this.recurringEvent?.count ? "new_event_scheduled_recurring" : "new_event_scheduled")}
${this.t(
title || this.calEvent.recurringEvent?.count ? "new_event_scheduled_recurring" : "new_event_scheduled"
)}
${this.t(subtitle)}
${extraInfo}
${getRichDescription(this.calEvent)}

View File

@ -58,7 +58,7 @@ const commons = {
},
isWeb3Active: false,
hideCalendarNotes: false,
recurringEvent: {},
recurringEvent: null,
destinationCalendar: null,
team: null,
requiresConfirmation: false,

View File

@ -1 +1,2 @@
export { default as isPrismaObj, isPrismaObjOrUndefined } from "./isPrismaObj";
export * from "./isRecurringEvent";

View File

@ -0,0 +1,13 @@
import { recurringEventType as recurringEventSchema } from "@calcom/prisma/zod-utils";
import type { RecurringEvent } from "@calcom/types/Calendar";
export function isRecurringEvent(obj: unknown): obj is RecurringEvent {
const parsedRecuEvt = recurringEventSchema.safeParse(obj);
return parsedRecuEvt.success;
}
export function parseRecurringEvent(obj: unknown): RecurringEvent | null {
let recurringEvent: RecurringEvent | null = null;
if (isRecurringEvent(obj)) recurringEvent = obj;
return recurringEvent;
}

View File

@ -59,7 +59,7 @@ model EventType {
periodDays Int?
periodCountCalendarDays Boolean?
requiresConfirmation Boolean @default(false)
/// @zod.custom(imports.recurringEvent)
/// @zod.custom(imports.recurringEventType)
recurringEvent Json?
disableGuests Boolean @default(false)
hideCalendarNotes Boolean @default(false)

View File

@ -1,9 +1,20 @@
import { Frequency as RRuleFrequency } from "rrule";
import dayjs from "dayjs";
import { z } from "zod";
import { LocationType } from "@calcom/core/location";
import { slugify } from "@calcom/lib/slugify";
// Let's not import 118kb just to get an enum
export enum Frequency {
YEARLY = 0,
MONTHLY = 1,
WEEKLY = 2,
DAILY = 3,
HOURLY = 4,
MINUTELY = 5,
SECONDLY = 6,
}
export const eventTypeLocations = z.array(
z.object({
type: z.nativeEnum(LocationType),
@ -15,15 +26,33 @@ export const eventTypeLocations = z.array(
);
// Matching RRule.Options: rrule/dist/esm/src/types.d.ts
export const recurringEvent = z.object({
dtstart: z.date().optional(),
interval: z.number().optional(),
count: z.number().optional(),
freq: z.nativeEnum(RRuleFrequency).optional(),
until: z.date().optional(),
tzid: z.string().optional(),
});
export const recurringEventType = z
.object({
dtstart: z.date().optional(),
interval: z.number(),
count: z.number(),
freq: z.nativeEnum(Frequency),
until: z.date().optional(),
tzid: z.string().optional(),
})
.nullable();
export const eventTypeSlug = z.string().transform((val) => slugify(val.trim()));
export const stringToDate = z.string().transform((a) => new Date(a));
export const stringOrNumber = z.union([z.string().transform((v) => parseInt(v, 10)), z.number().int()]);
export const stringOrNumber = z.union([
z.string().transform((v, ctx) => {
const parsed = parseInt(v);
if (isNaN(parsed)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Not a number",
});
}
return parsed;
}),
z.number().int(),
]);
export const stringToDayjs = z.string().transform((val) => dayjs(val));

View File

@ -3,7 +3,8 @@ import type { Dayjs } from "dayjs";
import type { calendar_v3 } from "googleapis";
import type { Time } from "ical.js";
import type { TFunction } from "next-i18next";
import type { Frequency as RRuleFrequency } from "rrule";
import type { Frequency } from "@calcom/prisma/zod-utils";
import type { Event } from "./Event";
import type { Ensure } from "./utils";
@ -87,9 +88,9 @@ export interface AdditionalInformation {
export interface RecurringEvent {
dtstart?: Date | undefined;
interval?: number;
count?: number;
freq?: RRuleFrequency;
interval: number;
count: number;
freq: Frequency;
until?: Date | undefined;
tzid?: string | undefined;
}
@ -120,6 +121,7 @@ export interface CalendarEvent {
rejectionReason?: string | null;
hideCalendarNotes?: boolean;
recurrence?: string;
recurringEvent?: RecurringEvent | null;
}
export interface EntryPoint {