From 28e7559d5b9dd9b204644154b15843cc0ac04fea Mon Sep 17 00:00:00 2001 From: Leo Giovanetti Date: Wed, 11 May 2022 16:00:25 -0300 Subject: [PATCH 01/14] Loading state and rejected bookings gone --- apps/web/components/booking/pages/BookingPage.tsx | 12 +++++++++--- apps/web/server/routers/viewer.tsx | 6 +++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/apps/web/components/booking/pages/BookingPage.tsx b/apps/web/components/booking/pages/BookingPage.tsx index 789576f23ea5ce..28a7f4a7c58655 100644 --- a/apps/web/components/booking/pages/BookingPage.tsx +++ b/apps/web/components/booking/pages/BookingPage.tsx @@ -763,7 +763,11 @@ const BookingPage = ({ - {mutation.isError && ( + {(mutation.isError || recurringMutation.isError) && (
@@ -782,7 +786,9 @@ const BookingPage = ({

{rescheduleUid ? t("reschedule_fail") : t("booking_fail")}{" "} - {(mutation.error as HttpError)?.message} + {eventType.recurringEvent?.freq && recurringEventCount + ? (mutation.error as HttpError)?.message + : (recurringMutation.error as HttpError)?.message}

diff --git a/apps/web/server/routers/viewer.tsx b/apps/web/server/routers/viewer.tsx index 2153fb87d61096..25a61cee0461e4 100644 --- a/apps/web/server/routers/viewer.tsx +++ b/apps/web/server/routers/viewer.tsx @@ -320,7 +320,11 @@ const loggedInViewerRouter = createProtectedRouter() // handled separately for each occurrence OR: [ { - AND: [{ NOT: { recurringEventId: { equals: null } } }, { confirmed: false }], + AND: [ + { NOT: { recurringEventId: { equals: null } } }, + { confirmed: false }, + { NOT: { status: { equals: BookingStatus.REJECTED } } }, + ], }, { AND: [ From f8d6e22e78995231d4ca366b17d332042d8154ac Mon Sep 17 00:00:00 2001 From: Leo Giovanetti Date: Thu, 12 May 2022 11:28:32 -0300 Subject: [PATCH 02/14] Listing fixes --- apps/web/pages/bookings/[status].tsx | 15 +++++++++++++-- apps/web/server/routers/viewer.tsx | 20 +------------------- 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/apps/web/pages/bookings/[status].tsx b/apps/web/pages/bookings/[status].tsx index f838a5bcd7c9c7..368c1cfabd28e5 100644 --- a/apps/web/pages/bookings/[status].tsx +++ b/apps/web/pages/bookings/[status].tsx @@ -4,6 +4,7 @@ import { Fragment } from "react"; import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components"; import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Booking } from "@calcom/prisma/client"; import { Alert } from "@calcom/ui/Alert"; import Button from "@calcom/ui/Button"; @@ -58,7 +59,17 @@ export default function Bookings() { } return { recurringCount }; }; - + const shownBookings: Record = {}; + const filterBookings = (booking: BookingOutput) => { + if (!booking.recurringEventId) { + return true; + } + if (shownBookings[booking.recurringEventId]) { + return false; + } + shownBookings[booking.recurringEventId] = true; + return true; + }; return ( }> @@ -77,7 +88,7 @@ export default function Bookings() { {query.data.pages.map((page, index) => ( - {page.bookings.map((booking) => ( + {page.bookings.filter(filterBookings).map((booking) => ( { + const bookings = bookingsQuery.map((booking) => { return { ...booking, eventType: { @@ -444,24 +444,6 @@ const loggedInViewerRouter = createProtectedRouter() }; }); const bookingsFetched = bookings.length; - const seenBookings: Record = {}; - - // Remove duplicate recurring bookings for upcoming status. - // Couldn't use distinct in query because the distinct column would be different for recurring and non recurring event. - // We might be actually sending less then the limit, due to this filter - // TODO: Figure out a way to fix it. - if (bookingListingByStatus === "upcoming") { - bookings = bookings.filter((booking) => { - if (!booking.recurringEventId) { - return true; - } - if (seenBookings[booking.recurringEventId]) { - return false; - } - seenBookings[booking.recurringEventId] = true; - return true; - }); - } let nextCursor: typeof skip | null = skip; From de1eccc74594010976d8f1b38d2bbeb601326184 Mon Sep 17 00:00:00 2001 From: Leo Giovanetti Date: Tue, 31 May 2022 16:13:03 -0300 Subject: [PATCH 03/14] Tweaking upcoming vs recurring bookings tabs --- apps/web/components/booking/BookingListItem.tsx | 8 ++++---- apps/web/pages/bookings/[status].tsx | 15 ++++++++------- apps/web/pages/success.tsx | 2 +- apps/web/server/routers/viewer.tsx | 7 ++----- 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index 194d39ceee6d73..6b6810a79cf174 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -63,7 +63,7 @@ function BookingListItem(booking: BookingItemProps) { * Only pass down the recurring event id when we need to confirm the entire series, which happens in * the "Upcoming" tab, to support confirming discretionally in the "Recurring" tab. */ - if (booking.listingStatus === "upcoming" && booking.recurringEventId !== null) { + if (booking.listingStatus === "recurring" && booking.recurringEventId !== null) { body = Object.assign({}, body, { recurringEventId: booking.recurringEventId }); } const res = await fetch("/api/book/confirm", { @@ -91,7 +91,7 @@ function BookingListItem(booking: BookingItemProps) { { id: "reject", label: - booking.listingStatus === "upcoming" && booking.recurringEventId !== null + booking.listingStatus === "recurring" && booking.recurringEventId !== null ? t("reject_all") : t("reject"), onClick: () => { @@ -103,7 +103,7 @@ function BookingListItem(booking: BookingItemProps) { { id: "confirm", label: - booking.listingStatus === "upcoming" && booking.recurringEventId !== null + booking.listingStatus === "recurring" && booking.recurringEventId !== null ? t("confirm_all") : t("confirm"), onClick: () => { @@ -283,7 +283,7 @@ function BookingListItem(booking: BookingItemProps) {
{booking.recurringCount && booking.eventType?.recurringEvent?.freq && - booking.listingStatus === "upcoming" && ( + booking.listingStatus === "recurring" && (
= {}; const filterBookings = (booking: BookingOutput) => { - if (!booking.recurringEventId) { - return true; + if (status === "recurring") { + if (!booking.recurringEventId) { + return true; + } + if (shownBookings[booking.recurringEventId]) { + return false; + } + shownBookings[booking.recurringEventId] = true; } - if (shownBookings[booking.recurringEventId]) { - return false; - } - shownBookings[booking.recurringEventId] = true; return true; }; return ( diff --git a/apps/web/pages/success.tsx b/apps/web/pages/success.tsx index 678166117aeb3c..bf9a462c244441 100644 --- a/apps/web/pages/success.tsx +++ b/apps/web/pages/success.tsx @@ -618,7 +618,7 @@ function RecurringBookings({ }: RecurringBookingsProps) { const [moreEventsVisible, setMoreEventsVisible] = useState(false); const { t } = useLocale(); - return !isReschedule && recurringBookings && listingStatus === "upcoming" ? ( + return !isReschedule && recurringBookings && listingStatus === "recurring" ? ( <> {eventType.recurringEvent?.count && recurringBookings.slice(0, 4).map((dateStr, idx) => ( diff --git a/apps/web/server/routers/viewer.tsx b/apps/web/server/routers/viewer.tsx index 850d8b614b349c..fd846eda684e79 100644 --- a/apps/web/server/routers/viewer.tsx +++ b/apps/web/server/routers/viewer.tsx @@ -327,11 +327,7 @@ const loggedInViewerRouter = createProtectedRouter() // handled separately for each occurrence OR: [ { - AND: [ - { NOT: { recurringEventId: { equals: null } } }, - { confirmed: false }, - { NOT: { status: { equals: BookingStatus.REJECTED } } }, - ], + AND: [{ NOT: { recurringEventId: { equals: null } } }, { confirmed: true }], }, { AND: [ @@ -348,6 +344,7 @@ const loggedInViewerRouter = createProtectedRouter() endTime: { gte: new Date() }, AND: [ { NOT: { recurringEventId: { equals: null } } }, + { confirmed: false }, { NOT: { status: { equals: BookingStatus.CANCELLED } } }, { NOT: { status: { equals: BookingStatus.REJECTED } } }, ], From a73bef23141a92fbc7a7b0daa861010bbdf3f4db Mon Sep 17 00:00:00 2001 From: Leo Giovanetti Date: Mon, 6 Jun 2022 21:09:14 -0300 Subject: [PATCH 04/14] Tweaking new emails to avoid recurringEvent param --- .../components/booking/BookingListItem.tsx | 52 ++++++++---- .../api/integrations/stripepayment/webhook.ts | 9 +- apps/web/pages/[user]/[type].tsx | 2 +- apps/web/pages/api/book/confirm.ts | 21 +++-- apps/web/pages/api/book/event.ts | 43 ++++------ apps/web/pages/api/cancel.ts | 44 +++++++--- apps/web/pages/api/cron/bookingReminder.ts | 16 +++- apps/web/pages/team/[slug]/[type].tsx | 2 +- apps/web/public/static/locales/en/common.json | 3 + apps/web/server/routers/viewer.tsx | 26 +++++- apps/web/server/routers/viewer/bookings.tsx | 10 ++- packages/emails/email-manager.ts | 84 +++++++------------ packages/emails/src/components/WhenInfo.tsx | 32 ++++--- .../src/templates/AttendeeScheduledEmail.tsx | 3 +- .../src/templates/BaseScheduledEmail.tsx | 5 +- .../src/templates/OrganizerScheduledEmail.tsx | 5 +- .../attendee-request-reschedule-email.ts | 9 +- .../templates/attendee-scheduled-email.ts | 13 ++- .../organizer-request-reschedule-email.ts | 9 +- .../templates/organizer-scheduled-email.ts | 16 ++-- 20 files changed, 223 insertions(+), 181 deletions(-) diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index fc012f27127972..1fd5cb7e09accb 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -62,7 +62,7 @@ function BookingListItem(booking: BookingItemProps) { }; /** * Only pass down the recurring event id when we need to confirm the entire series, which happens in - * the "Upcoming" tab, to support confirming discretionally in the "Recurring" tab. + * the "Recurring" tab, to support confirming discretionally in the "Recurring" tab. */ if (booking.listingStatus === "recurring" && booking.recurringEventId !== null) { body = Object.assign({}, body, { recurringEventId: booking.recurringEventId }); @@ -119,10 +119,13 @@ function BookingListItem(booking: BookingItemProps) { }, ]; - const bookedActions: ActionType[] = [ + let bookedActions: ActionType[] = [ { id: "cancel", - label: t("cancel"), + label: + booking.listingStatus === "recurring" && booking.recurringEventId !== null + ? t("cancel_all_remaining") + : t("cancel"), href: `/cancel/${booking.uid}`, icon: XIcon, }, @@ -158,6 +161,10 @@ function BookingListItem(booking: BookingItemProps) { }, ]; + if (booking.listingStatus === "recurring" && booking.recurringEventId !== null) { + bookedActions = bookedActions.filter((action) => action.id !== "edit_booking"); + } + const RequestSentMessage = () => { return (
@@ -192,8 +199,10 @@ function BookingListItem(booking: BookingItemProps) { // Calculate the booking date(s) let recurringStrings: string[] = []; + let recurringDates: Date[] = []; + const today = new Date(); if (booking.recurringCount && booking.eventType.recurringEvent?.freq !== null) { - [recurringStrings] = parseRecurringDates( + [recurringStrings, recurringDates] = parseRecurringDates( { startDate: booking.startTime, recurringEvent: booking.eventType.recurringEvent, @@ -201,6 +210,11 @@ function BookingListItem(booking: BookingItemProps) { }, i18n ); + if (booking.status === BookingStatus.PENDING) { + // Only take into consideration next up instances if booking is confirmed + recurringDates = recurringDates.filter((aDate) => aDate >= today); + recurringStrings = recurringDates.map((_, key) => recurringStrings[key]); + } } let location = booking.location || ""; @@ -301,22 +315,26 @@ function BookingListItem(booking: BookingItemProps) {
( -

{aDate}

+

{recurringStrings[key]}

))}>

- {`${t("every_for_freq", { - freq: t( - `${RRuleFrequency[booking.eventType.recurringEvent.freq] - .toString() - .toLowerCase()}` - ), - })} ${booking.recurringCount} ${t( - `${RRuleFrequency[booking.eventType.recurringEvent.freq] - .toString() - .toLowerCase()}`, - { count: booking.recurringCount } - )}`} + {booking.status !== BookingStatus.PENDING + ? `${t("event_remaining", { + count: recurringDates.length, + })}` + : `${t("every_for_freq", { + freq: t( + `${RRuleFrequency[booking.eventType.recurringEvent.freq] + .toString() + .toLowerCase()}` + ), + })} ${booking.recurringCount} ${t( + `${RRuleFrequency[booking.eventType.recurringEvent.freq] + .toString() + .toLowerCase()}`, + { count: booking.recurringCount } + )}`}

diff --git a/apps/web/ee/pages/api/integrations/stripepayment/webhook.ts b/apps/web/ee/pages/api/integrations/stripepayment/webhook.ts index 02a39ca8d965da..270176dec2180a 100644 --- a/apps/web/ee/pages/api/integrations/stripepayment/webhook.ts +++ b/apps/web/ee/pages/api/integrations/stripepayment/webhook.ts @@ -1,6 +1,7 @@ import { BookingStatus, Prisma } from "@prisma/client"; import { buffer } from "micro"; import type { NextApiRequest, NextApiResponse } from "next"; +import rrule from "rrule"; import Stripe from "stripe"; import EventManager from "@calcom/core/EventManager"; @@ -103,6 +104,11 @@ async function handlePaymentSuccess(event: Stripe.Event) { const attendeesList = await Promise.all(attendeesListPromises); + // Taking care of recurrence rule + let recurrence: string | undefined = undefined; + if (eventType.recurringEvent?.count) { + recurrence = new rrule(eventType.recurringEvent).toString(); + } const evt: CalendarEvent = { type: booking.title, title: booking.title, @@ -118,6 +124,7 @@ async function handlePaymentSuccess(event: Stripe.Event) { }, attendees: attendeesList, uid: booking.uid, + ...{ recurrence }, destinationCalendar: booking.destinationCalendar || user.destinationCalendar, }; @@ -153,7 +160,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, diff --git a/apps/web/pages/[user]/[type].tsx b/apps/web/pages/[user]/[type].tsx index 879ee702c6bdb0..9e1b01da77a2bf 100644 --- a/apps/web/pages/[user]/[type].tsx +++ b/apps/web/pages/[user]/[type].tsx @@ -262,7 +262,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: (eventType.recurringEvent || undefined) as RecurringEvent, locations: locationHiddenFilter(locations), }); diff --git a/apps/web/pages/api/book/confirm.ts b/apps/web/pages/api/book/confirm.ts index 22a02ebc87a3c7..5003f851eead1e 100644 --- a/apps/web/pages/api/book/confirm.ts +++ b/apps/web/pages/api/book/confirm.ts @@ -1,5 +1,6 @@ import { Booking, BookingStatus, Prisma, SchedulingType, User } from "@prisma/client"; import type { NextApiRequest } from "next"; +import rrule from "rrule"; import { z } from "zod"; import EventManager from "@calcom/core/EventManager"; @@ -160,6 +161,12 @@ async function patchHandler(req: NextApiRequest) { const attendeesList = await Promise.all(attendeesListPromises); + // Taking care of recurrence rule + const recurringEvent = booking.eventType?.recurringEvent as RecurringEvent; + let recurrence: string | undefined = undefined; + if (recurringEvent?.count) { + recurrence = new rrule(recurringEvent).toString(); + } const evt: CalendarEvent = { type: booking.title, title: booking.title, @@ -176,11 +183,10 @@ async function patchHandler(req: NextApiRequest) { attendees: attendeesList, location: booking.location ?? "", uid: booking.uid, + ...{ recurrence }, destinationCalendar: booking?.destinationCalendar || currentUser.destinationCalendar, }; - const recurringEvent = booking.eventType?.recurringEvent as RecurringEvent; - if (recurringEventId && recurringEvent) { const groupedRecurringBookings = await prisma.booking.groupBy({ where: { @@ -217,17 +223,14 @@ 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); } } if (recurringEventId) { - // The booking to confirm is a recurring event and comes from /booking/upcoming, proceeding to mark all related + // The booking to confirm is a recurring event and comes from /booking/recurring, proceeding to mark all related // bookings as confirmed. Prisma updateMany does not support relations, so doing this in two steps for now. const unconfirmedRecurringBookings = await prisma.booking.findMany({ where: { @@ -266,7 +269,7 @@ async function patchHandler(req: NextApiRequest) { } else { evt.rejectionReason = rejectionReason; if (recurringEventId) { - // The booking to reject is a recurring event and comes from /booking/upcoming, proceeding to mark all related + // The booking to reject is a recurring event and comes from /booking/recurring, 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({ where: { @@ -298,7 +301,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; diff --git a/apps/web/pages/api/book/event.ts b/apps/web/pages/api/book/event.ts index e934681cf6aab2..1928714e95e33d 100644 --- a/apps/web/pages/api/book/event.ts +++ b/apps/web/pages/api/book/event.ts @@ -414,6 +414,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) // 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) eventType.recurringEvent = Object.assign({}, eventType.recurringEvent, { count: recurringCount }); + // Taking care of recurrence rule + evt.recurrence = new rrule(eventType.recurringEvent).toString(); } // Initialize EventManager with credentials @@ -721,15 +723,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 +760,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 ( diff --git a/apps/web/pages/api/cancel.ts b/apps/web/pages/api/cancel.ts index 85f18a349ee324..a377d795c6810a 100644 --- a/apps/web/pages/api/cancel.ts +++ b/apps/web/pages/api/cancel.ts @@ -2,6 +2,7 @@ import { BookingStatus, Credential, WebhookTriggerEvents } from "@prisma/client" import async from "async"; import dayjs from "dayjs"; import { NextApiRequest, NextApiResponse } from "next"; +import rrule from "rrule"; import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter"; import { getCalendar } from "@calcom/core/CalendarManager"; @@ -9,7 +10,7 @@ import { deleteMeeting } from "@calcom/core/videoClient"; import { sendCancelledEmails } from "@calcom/emails"; import { isPrismaObjOrUndefined } from "@calcom/lib"; import prisma, { bookingMinimalSelect } from "@calcom/prisma"; -import type { CalendarEvent } from "@calcom/types/Calendar"; +import type { CalendarEvent, RecurringEvent } from "@calcom/types/Calendar"; import { refund } from "@ee/lib/stripe/server"; import { asStringOrNull } from "@lib/asStringOrNull"; @@ -35,6 +36,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }, select: { ...bookingMinimalSelect, + recurringEventId: true, userId: true, user: { select: { @@ -59,6 +61,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) eventType: { select: { title: true, + recurringEvent: true, }, }, uid: true, @@ -107,6 +110,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const attendeesList = await Promise.all(attendeesListPromises); const tOrganizer = await getTranslation(organizer.locale ?? "en", "common"); + // Taking care of recurrence rule + const recurringEvent = bookingToDelete.eventType?.recurringEvent as RecurringEvent; + let recurrence: string | undefined = undefined; + if (recurringEvent?.count) { + recurrence = new rrule(recurringEvent).toString(); + } const evt: CalendarEvent = { title: bookingToDelete?.title, type: (bookingToDelete?.eventType?.title as string) || bookingToDelete?.title, @@ -121,6 +130,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) language: { translate: tOrganizer, locale: organizer.locale ?? "en" }, }, attendees: attendeesList, + ...{ recurrence }, uid: bookingToDelete?.uid, location: bookingToDelete?.location, destinationCalendar: bookingToDelete?.destinationCalendar || bookingToDelete?.user.destinationCalendar, @@ -144,15 +154,28 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) // by cancelling first, and blocking whilst doing so; we can ensure a cancel // action always succeeds even if subsequent integrations fail cancellation. - await prisma.booking.update({ - where: { - uid, - }, - data: { - status: BookingStatus.CANCELLED, - cancellationReason: cancellationReason, - }, - }); + if (bookingToDelete.eventType?.recurringEvent) { + // Proceed to mark as cancelled all recurring event instances + await prisma.booking.updateMany({ + where: { + recurringEventId: bookingToDelete.recurringEventId, + }, + data: { + status: BookingStatus.CANCELLED, + cancellationReason: cancellationReason, + }, + }); + } else { + await prisma.booking.update({ + where: { + uid, + }, + data: { + status: BookingStatus.CANCELLED, + cancellationReason: cancellationReason, + }, + }); + } /** TODO: Remove this without breaking functionality */ if (bookingToDelete.location === "integrations:daily") { @@ -175,6 +198,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } }); + // Recurring event is mutually exclusive with paid events, so nothing to tweak for recurring events for now if (bookingToDelete && bookingToDelete.paid) { const evt: CalendarEvent = { type: bookingToDelete?.eventType?.title as string, diff --git a/apps/web/pages/api/cron/bookingReminder.ts b/apps/web/pages/api/cron/bookingReminder.ts index 53ca7cf41d6366..31fde400c8ad58 100644 --- a/apps/web/pages/api/cron/bookingReminder.ts +++ b/apps/web/pages/api/cron/bookingReminder.ts @@ -1,11 +1,12 @@ import { BookingStatus, ReminderType } from "@prisma/client"; import dayjs from "dayjs"; import type { NextApiRequest, NextApiResponse } from "next"; +import rrule from "rrule"; import { sendOrganizerRequestReminderEmail } from "@calcom/emails"; import { isPrismaObjOrUndefined } from "@calcom/lib"; import prisma, { bookingMinimalSelect } from "@calcom/prisma"; -import type { CalendarEvent } from "@calcom/types/Calendar"; +import type { CalendarEvent, RecurringEvent } from "@calcom/types/Calendar"; import { getTranslation } from "@server/lib/i18n"; @@ -43,6 +44,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) destinationCalendar: true, }, }, + eventType: { + select: { + recurringEvent: true, + }, + }, uid: true, destinationCalendar: true, }, @@ -60,6 +66,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }, }); + // Taking care of recurrence rule + const recurringEvent = bookings[0].eventType?.recurringEvent as RecurringEvent; + let recurrence: string | undefined = undefined; + if (recurringEvent?.count) { + recurrence = new rrule(recurringEvent).toString(); + } + for (const booking of bookings.filter((b) => !reminders.some((r) => r.referenceId == b.id))) { const { user } = booking; const name = user?.name || user?.username; @@ -100,6 +113,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }, attendees: attendeesList, uid: booking.uid, + ...{ recurrence }, destinationCalendar: booking.destinationCalendar || user.destinationCalendar, }; diff --git a/apps/web/pages/team/[slug]/[type].tsx b/apps/web/pages/team/[slug]/[type].tsx index 4d7dd6a637a26d..8238913a2251b8 100644 --- a/apps/web/pages/team/[slug]/[type].tsx +++ b/apps/web/pages/team/[slug]/[type].tsx @@ -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: (eventType.recurringEvent || undefined) as RecurringEvent, locations: locationHiddenFilter(locations), }); diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 6c13d3def654a9..207a787c4ba9d6 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -478,6 +478,7 @@ "danger_zone": "Danger Zone", "back": "Back", "cancel": "Cancel", + "cancel_all_remaining": "Cancel all remaining", "apply": "Apply", "cancel_event": "Cancel event", "continue": "Continue", @@ -553,6 +554,8 @@ "repeats_up_to": "Repeats up to {{count}} time", "repeats_up_to_plural": "Repeats up to {{count}} times", "every_for_freq": "Every {{freq}} for", + "event_remaining": "{{count}} event remaining", + "event_remaining_plural": "{{count}} events remaining", "repeats_every": "Repeats every", "weekly": "week", "weekly_plural": "weeks", diff --git a/apps/web/server/routers/viewer.tsx b/apps/web/server/routers/viewer.tsx index bcb20ea8bde772..0c674fc58e6ef7 100644 --- a/apps/web/server/routers/viewer.tsx +++ b/apps/web/server/routers/viewer.tsx @@ -319,7 +319,10 @@ const loggedInViewerRouter = createProtectedRouter() // handled separately for each occurrence OR: [ { - AND: [{ NOT: { recurringEventId: { equals: null } } }, { status: BookingStatus.PENDING }], + AND: [ + { NOT: { recurringEventId: { equals: null } } }, + { NOT: { status: BookingStatus.PENDING } }, + ], }, { AND: [ @@ -336,7 +339,6 @@ const loggedInViewerRouter = createProtectedRouter() endTime: { gte: new Date() }, AND: [ { NOT: { recurringEventId: { equals: null } } }, - { confirmed: false }, { NOT: { status: { equals: BookingStatus.CANCELLED } } }, { NOT: { status: { equals: BookingStatus.REJECTED } } }, ], @@ -425,7 +427,7 @@ const loggedInViewerRouter = createProtectedRouter() _count: true, }); - const bookings = bookingsQuery.map((booking) => { + let bookings = bookingsQuery.map((booking) => { return { ...booking, eventType: { @@ -437,6 +439,24 @@ const loggedInViewerRouter = createProtectedRouter() }; }); const bookingsFetched = bookings.length; + const seenBookings: Record = {}; + + // Remove duplicate recurring bookings for upcoming status. + // Couldn't use distinct in query because the distinct column would be different for recurring and non recurring event. + // We might be actually sending less then the limit, due to this filter + // TODO: Figure out a way to fix it. + if (bookingListingByStatus === "upcoming") { + bookings = bookings.filter((booking) => { + if (!booking.recurringEventId) { + return true; + } + if (seenBookings[booking.recurringEventId]) { + return false; + } + seenBookings[booking.recurringEventId] = true; + return true; + }); + } let nextCursor: typeof skip | null = skip; if (bookingsFetched > take) { diff --git a/apps/web/server/routers/viewer/bookings.tsx b/apps/web/server/routers/viewer/bookings.tsx index 88548aae82883e..5cd3f3c81c4e91 100644 --- a/apps/web/server/routers/viewer/bookings.tsx +++ b/apps/web/server/routers/viewer/bookings.tsx @@ -1,12 +1,13 @@ import { SchedulingType } from "@prisma/client"; import dayjs from "dayjs"; +import rrule from "rrule"; import { z } from "zod"; import EventManager from "@calcom/core/EventManager"; import { sendLocationChangeEmails } from "@calcom/emails"; import logger from "@calcom/lib/logger"; import { getTranslation } from "@calcom/lib/server/i18n"; -import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar"; +import type { AdditionalInformation, CalendarEvent, RecurringEvent } from "@calcom/types/Calendar"; import { createProtectedRouter } from "@server/createRouter"; import { TRPCError } from "@trpc/server"; @@ -104,6 +105,12 @@ export const bookingsRouter = createProtectedRouter() const attendeesList = await Promise.all(attendeesListPromises); + // Taking care of recurrence rule + const recurringEvent = booking.eventType?.recurringEvent as RecurringEvent; + let recurrence: string | undefined = undefined; + if (recurringEvent?.count) { + recurrence = new rrule(recurringEvent).toString(); + } const evt: CalendarEvent = { title: booking.title || "", type: (booking.eventType?.title as string) || booking?.title || "", @@ -118,6 +125,7 @@ export const bookingsRouter = createProtectedRouter() }, attendees: attendeesList, uid: booking.uid, + ...{ recurrence }, location, destinationCalendar: booking?.destinationCalendar || booking?.user?.destinationCalendar, }; diff --git a/packages/emails/email-manager.ts b/packages/emails/email-manager.ts index f195ac4c9f4a9d..12d264805a2b93 100644 --- a/packages/emails/email-manager.ts +++ b/packages/emails/email-manager.ts @@ -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[] = []; 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[] = []; 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[] = []; 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[] = []; 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[] = []; 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[] = []; 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[] = []; 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)); diff --git a/packages/emails/src/components/WhenInfo.tsx b/packages/emails/src/components/WhenInfo.tsx index 533fb01fa36c99..f7e337627bcd8f 100644 --- a/packages/emails/src/components/WhenInfo.tsx +++ b/packages/emails/src/components/WhenInfo.tsx @@ -3,30 +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 }) { - const t = props.calEvent.attendees[0].language.translate; - return props.recurringEvent?.count && props.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 } - )}` - : ""; +function getRecurringWhen({ calEvent }: { calEvent: CalendarEvent }) { + if (calEvent.recurrence !== undefined) { + const t = calEvent.attendees[0].language.translate; + const recurringEvent = rrule.fromString(calEvent.recurrence).options; + return ` - ${t("every_for_freq", { + freq: t(`${rrule.FREQUENCIES[recurringEvent.freq].toString().toLowerCase()}`), + })} ${recurringEvent.count} ${t(`${rrule.FREQUENCIES[recurringEvent.freq].toString().toLowerCase()}`, { + count: recurringEvent.count as number, + })}`; + } + return ""; } -export function WhenInfo(props: { - calEvent: CalendarEvent; - recurringEvent: RecurringEvent; - timeZone: string; - t: TFunction; -}) { +export function WhenInfo(props: { calEvent: CalendarEvent; timeZone: string; t: TFunction }) { const { timeZone, t } = props; function getRecipientStart(format: string) { @@ -44,7 +40,7 @@ export function WhenInfo(props: { lineThrough={!!props.calEvent.cancellationReason} description={ <> - {props.recurringEvent?.count ? `${t("starting")} ` : ""} + {props.calEvent.recurrence ? `${t("starting")} ` : ""} {t(getRecipientStart("dddd").toLowerCase())}, {t(getRecipientStart("MMMM").toLowerCase())}{" "} {getRecipientStart("D, YYYY | h:mma")} - {getRecipientEnd("h:mma")}{" "} ({timeZone}) diff --git a/packages/emails/src/templates/AttendeeScheduledEmail.tsx b/packages/emails/src/templates/AttendeeScheduledEmail.tsx index d67fca1d8c6f0e..cbb18264990aef 100644 --- a/packages/emails/src/templates/AttendeeScheduledEmail.tsx +++ b/packages/emails/src/templates/AttendeeScheduledEmail.tsx @@ -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> ) => { return ( diff --git a/packages/emails/src/templates/BaseScheduledEmail.tsx b/packages/emails/src/templates/BaseScheduledEmail.tsx index 22779b464cd20e..682d4c924eebe1 100644 --- a/packages/emails/src/templates/BaseScheduledEmail.tsx +++ b/packages/emails/src/templates/BaseScheduledEmail.tsx @@ -26,7 +26,6 @@ export const BaseScheduledEmail = ( props: { calEvent: CalendarEvent; attendee: Person; - recurringEvent: RecurringEvent; timeZone: string; t: TFunction; } & Partial> @@ -56,7 +55,7 @@ export const BaseScheduledEmail = ( title={t( props.title ? props.title - : props.recurringEvent?.count + : props.calEvent.recurrence ? "your_event_has_been_scheduled_recurring" : "your_event_has_been_scheduled" )} @@ -69,7 +68,7 @@ export const BaseScheduledEmail = ( - + diff --git a/packages/emails/src/templates/OrganizerScheduledEmail.tsx b/packages/emails/src/templates/OrganizerScheduledEmail.tsx index ce76fc82360c41..127432989f3337 100644 --- a/packages/emails/src/templates/OrganizerScheduledEmail.tsx +++ b/packages/emails/src/templates/OrganizerScheduledEmail.tsx @@ -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> ) => { const t = props.calEvent.organizer.language.translate; @@ -15,7 +14,7 @@ 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.recurrence ? "new_event_scheduled_recurring" : "new_event_scheduled")} {...props} /> ); diff --git a/packages/emails/templates/attendee-request-reschedule-email.ts b/packages/emails/templates/attendee-request-reschedule-email.ts index 721f6def8acc2f..02c73a0a064c5b 100644 --- a/packages/emails/templates/attendee-request-reschedule-email.ts +++ b/packages/emails/templates/attendee-request-reschedule-email.ts @@ -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 { @@ -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(), }; diff --git a/packages/emails/templates/attendee-scheduled-email.ts b/packages/emails/templates/attendee-scheduled-email.ts index 6b53bed1fd715d..57dff4b2fff071 100644 --- a/packages/emails/templates/attendee-scheduled-email.ts +++ b/packages/emails/templates/attendee-scheduled-email.ts @@ -21,23 +21,21 @@ 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 + // Taking care of recurrence rule let recurrenceRule: string | undefined = undefined; - if (this.recurringEvent?.count) { - recurrenceRule = new rrule(this.recurringEvent).toString().replace("RRULE:", ""); + if (this.calEvent.recurrence) { + recurrenceRule = this.calEvent.recurrence.replace("RRULE:", ""); } const icsEvent = createEvent({ start: dayjs(this.calEvent.startTime) @@ -84,7 +82,6 @@ export default class AttendeeScheduledEmail extends BaseEmail { html: renderEmail("AttendeeScheduledEmail", { calEvent: this.calEvent, attendee: this.attendee, - recurringEvent: this.recurringEvent, }), text: this.getTextBody(), }; @@ -93,7 +90,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.recurrence ? "your_event_has_been_scheduled_recurring" : "your_event_has_been_scheduled" )} diff --git a/packages/emails/templates/organizer-request-reschedule-email.ts b/packages/emails/templates/organizer-request-reschedule-email.ts index f01da57495fae2..fc69d04f9ec85f 100644 --- a/packages/emails/templates/organizer-request-reschedule-email.ts +++ b/packages/emails/templates/organizer-request-reschedule-email.ts @@ -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 { @@ -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", { diff --git a/packages/emails/templates/organizer-scheduled-email.ts b/packages/emails/templates/organizer-scheduled-email.ts index e08f8a17bebef1..78d58c0e85e045 100644 --- a/packages/emails/templates/organizer-scheduled-email.ts +++ b/packages/emails/templates/organizer-scheduled-email.ts @@ -5,10 +5,9 @@ import toArray from "dayjs/plugin/toArray"; import utc from "dayjs/plugin/utc"; import { createEvent, DateArray, Person } from "ics"; 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 +19,20 @@ 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 + // Taking care of recurrence rule let recurrenceRule: string | undefined = undefined; - if (this.recurringEvent?.count) { - recurrenceRule = new rrule(this.recurringEvent).toString().replace("RRULE:", ""); + if (this.calEvent.recurrence) { + recurrenceRule = this.calEvent.recurrence.replace("RRULE:", ""); } const icsEvent = createEvent({ start: dayjs(this.calEvent.startTime) @@ -91,7 +88,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 +100,7 @@ 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.recurrence ? "new_event_scheduled_recurring" : "new_event_scheduled")} ${this.t(subtitle)} ${extraInfo} ${getRichDescription(this.calEvent)} From 546eec7ab3813e9e9649923392169b30321166b9 Mon Sep 17 00:00:00 2001 From: Leo Giovanetti Date: Tue, 7 Jun 2022 17:32:08 -0300 Subject: [PATCH 05/14] Tweaks to support recurring events in common flows --- .../components/booking/BookingListItem.tsx | 26 ++-- apps/web/components/booking/CancelBooking.tsx | 16 ++- .../eventtype/EventTypeDescription.tsx | 4 +- apps/web/pages/bookings/[status].tsx | 2 +- apps/web/pages/cancel/[uid].tsx | 134 +++++++++++++++--- apps/web/pages/cancel/success.tsx | 7 +- apps/web/pages/success.tsx | 44 +++--- apps/web/public/static/locales/en/common.json | 3 - apps/web/server/routers/viewer.tsx | 4 +- packages/emails/src/components/WhenInfo.tsx | 13 +- .../src/templates/AttendeeDeclinedEmail.tsx | 2 +- .../src/templates/AttendeeRequestEmail.tsx | 4 +- .../src/templates/OrganizerRequestEmail.tsx | 8 +- .../templates/organizer-request-email.ts | 5 +- 14 files changed, 198 insertions(+), 74 deletions(-) diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index 1fd5cb7e09accb..74eb8842a6680d 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -197,11 +197,13 @@ function BookingListItem(booking: BookingItemProps) { setLocationMutation.mutate({ bookingId: booking.id, newLocation }); }; - // Calculate the booking date(s) + // Calculate the booking date(s) and setup recurring event data to show let recurringStrings: string[] = []; let recurringDates: Date[] = []; const today = new Date(); - if (booking.recurringCount && booking.eventType.recurringEvent?.freq !== null) { + let bookingFrequency = ""; + if (booking.recurringCount && booking.eventType.recurringEvent?.freq !== undefined) { + bookingFrequency = RRuleFrequency[booking.eventType.recurringEvent?.freq].toString().toLowerCase(); [recurringStrings, recurringDates] = parseRecurringDates( { startDate: booking.startTime, @@ -310,7 +312,7 @@ function BookingListItem(booking: BookingItemProps) {
{booking.recurringCount && booking.eventType?.recurringEvent?.freq && - booking.listingStatus === "recurring" && ( + (booking.listingStatus === "recurring" || booking.listingStatus === "cancelled") && (

- {booking.status !== BookingStatus.PENDING - ? `${t("event_remaining", { + {booking.status === BookingStatus.ACCEPTED + ? `${t(recurringDates.length > 1 ? "event_remaining_plural" : "event_remaining", { count: recurringDates.length, })}` : `${t("every_for_freq", { - freq: t( - `${RRuleFrequency[booking.eventType.recurringEvent.freq] - .toString() - .toLowerCase()}` - ), + freq: t(bookingFrequency), })} ${booking.recurringCount} ${t( - `${RRuleFrequency[booking.eventType.recurringEvent.freq] - .toString() - .toLowerCase()}`, - { count: booking.recurringCount } + booking.recurringCount > 1 ? `${bookingFrequency}_plural` : bookingFrequency, + { + count: booking.recurringCount, + } )}`}

diff --git a/apps/web/components/booking/CancelBooking.tsx b/apps/web/components/booking/CancelBooking.tsx index d3ebc2cd0ae5d2..d30718d32e3d13 100644 --- a/apps/web/components/booking/CancelBooking.tsx +++ b/apps/web/components/booking/CancelBooking.tsx @@ -3,6 +3,7 @@ import { useRouter } from "next/router"; import { useState } from "react"; import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { RecurringEvent } from "@calcom/types/Calendar"; import { Button } from "@calcom/ui/Button"; import useTheme from "@lib/hooks/useTheme"; @@ -17,6 +18,7 @@ type Props = { name: string | null; slug: string | null; }; + recurringEvent: RecurringEvent; team?: string | null; setIsCancellationMode: (value: boolean) => void; theme: string | null; @@ -59,11 +61,13 @@ export default function CancelBooking(props: Props) { rows={3} />
-
- -
+ {!props.recurringEvent && ( +
+ +
+ )}
+ {!props.booking.eventType?.recurringEvent && ( + + )} } {!loading && session?.user && ( - )} diff --git a/apps/web/pages/success.tsx b/apps/web/pages/success.tsx index e6ed66bb32470b..7f2514b1b9c965 100644 --- a/apps/web/pages/success.tsx +++ b/apps/web/pages/success.tsx @@ -13,7 +13,7 @@ import { useSession } from "next-auth/react"; import Link from "next/link"; import { useRouter } from "next/router"; import { useEffect, useRef, useState } from "react"; -import RRule from "rrule"; +import RRule, { Frequency as RRuleFrequency } from "rrule"; import { z } from "zod"; import { SpaceBookingSuccessPage } from "@calcom/app-store/spacebooking/components"; @@ -270,7 +270,7 @@ export default function Success(props: SuccessProps) { data-testid="success-page"> {userIsOwner && !isEmbed && (
- + {t("back_to_bookings")} @@ -344,10 +344,9 @@ export default function Success(props: SuccessProps) {
{t("when")}
@@ -429,16 +428,21 @@ export default function Success(props: SuccessProps) { -
{t("or_lowercase")}
-
- {t("Reschedule")} -
+ {!props.recurringBookings && ( + <> +
{t("or_lowercase")}
+
+ {t("Reschedule")} +
+ + )}
) : ( + {eventType.recurringEvent?.count && ( + + {t("every_for_freq", { + freq: t(`${bookingFrequency}`), + })}{" "} + {eventType.recurringEvent?.count}{" "} + {t(eventType.recurringEvent?.count > 1 ? `${bookingFrequency}_plural` : `${bookingFrequency}`, { + count: eventType.recurringEvent?.count, + })} + + )} {eventType.recurringEvent?.count && recurringBookings.slice(0, 4).map((dateStr, idx) => (
diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 207a787c4ba9d6..12e25244693515 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -862,8 +862,6 @@ "add_link_from_giphy": "Add link from Giphy", "add_gif_to_confirmation": "Adding a GIF to confirmation page", "find_gif_spice_confirmation": "Find GIF to spice up your confirmation page", - "display_location_label":"Display on booking page", - "display_location_info_badge":"Location will be visible before the booking is confirmed", "share_feedback": "Share feedback", "resources": "Resources", "support_documentation": "Support documentation", @@ -883,7 +881,6 @@ "attendees_name": "Attendee's name", "dynamically_display_attendee_or_organizer": "Dynamically display the name of your attendee for you, or your name if it's viewed by your attendee", "event_location": "Event's location", - "meeting_url_provided_after_confirmed":"A Meeting URL will be created once the event is confirmed.", "reschedule_optional": "Reason for rescheduling (optional)", "reschedule_placeholder": "Let others know why you need to reschedule", "event_cancelled":"This event is cancelled", diff --git a/apps/web/server/routers/viewer.tsx b/apps/web/server/routers/viewer.tsx index 0c674fc58e6ef7..7453ec19ea5ef0 100644 --- a/apps/web/server/routers/viewer.tsx +++ b/apps/web/server/routers/viewer.tsx @@ -321,7 +321,9 @@ const loggedInViewerRouter = createProtectedRouter() { AND: [ { NOT: { recurringEventId: { equals: null } } }, - { NOT: { status: BookingStatus.PENDING } }, + { NOT: { status: { equals: BookingStatus.PENDING } } }, + { NOT: { status: { equals: BookingStatus.CANCELLED } } }, + { NOT: { status: { equals: BookingStatus.REJECTED } } }, ], }, { diff --git a/packages/emails/src/components/WhenInfo.tsx b/packages/emails/src/components/WhenInfo.tsx index f7e337627bcd8f..050c1b559fd9e1 100644 --- a/packages/emails/src/components/WhenInfo.tsx +++ b/packages/emails/src/components/WhenInfo.tsx @@ -13,11 +13,14 @@ function getRecurringWhen({ calEvent }: { calEvent: CalendarEvent }) { if (calEvent.recurrence !== undefined) { const t = calEvent.attendees[0].language.translate; const recurringEvent = rrule.fromString(calEvent.recurrence).options; - return ` - ${t("every_for_freq", { - freq: t(`${rrule.FREQUENCIES[recurringEvent.freq].toString().toLowerCase()}`), - })} ${recurringEvent.count} ${t(`${rrule.FREQUENCIES[recurringEvent.freq].toString().toLowerCase()}`, { - count: recurringEvent.count as number, - })}`; + const freqString = rrule.FREQUENCIES[recurringEvent.freq].toString().toLowerCase(); + if (recurringEvent !== null && recurringEvent.count !== null) { + return ` - ${t("every_for_freq", { + freq: t(freqString), + })} ${recurringEvent.count} ${t(recurringEvent.count > 1 ? `${freqString}_plural` : freqString, { + count: recurringEvent.count, + })}`; + } } return ""; } diff --git a/packages/emails/src/templates/AttendeeDeclinedEmail.tsx b/packages/emails/src/templates/AttendeeDeclinedEmail.tsx index 4c66c836882d62..2ed8ad0e480752 100644 --- a/packages/emails/src/templates/AttendeeDeclinedEmail.tsx +++ b/packages/emails/src/templates/AttendeeDeclinedEmail.tsx @@ -2,7 +2,7 @@ import { AttendeeScheduledEmail } from "./AttendeeScheduledEmail"; export const AttendeeDeclinedEmail = (props: React.ComponentProps) => ( ) => ( ) => ( } diff --git a/packages/emails/templates/organizer-request-email.ts b/packages/emails/templates/organizer-request-email.ts index 2278535a138ebb..77cb462982fd67 100644 --- a/packages/emails/templates/organizer-request-email.ts +++ b/packages/emails/templates/organizer-request-email.ts @@ -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"), }; @@ -36,7 +35,9 @@ export default class OrganizerRequestEmail extends OrganizerScheduledEmail { "someone_requested_an_event", "", `${this.calEvent.organizer.language.translate("confirm_or_reject_request")} -${process.env.NEXT_PUBLIC_WEBAPP_URL} + "/bookings/upcoming"` +${process.env.NEXT_PUBLIC_WEBAPP_URL} + ${ + this.calEvent.recurrence ? "/bookings/recurring" : "/bookings/upcoming" + }` ); } } From 1a6d54998e024ddf508c01c33dbba9b01d81423d Mon Sep 17 00:00:00 2001 From: Leo Giovanetti Date: Tue, 7 Jun 2022 17:47:04 -0300 Subject: [PATCH 06/14] Missed one renderEmail --- apps/web/pages/api/email.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/web/pages/api/email.ts b/apps/web/pages/api/email.ts index a8fdca5b297b26..64bb5ac641be49 100644 --- a/apps/web/pages/api/email.ts +++ b/apps/web/pages/api/email.ts @@ -49,7 +49,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { renderEmail("OrganizerRequestReminderEmail", { attendee: evt.attendees[0], calEvent: evt, - recurringEvent: {}, }) ); res.end(); From 07f19a75a8e22ddb1c90f48ebd847ed8e5cc71aa Mon Sep 17 00:00:00 2001 From: Leo Giovanetti Date: Tue, 7 Jun 2022 18:37:26 -0300 Subject: [PATCH 07/14] Removing uneeded references --- packages/emails/templates/attendee-awaiting-payment-email.ts | 1 - packages/emails/templates/attendee-cancelled-email.ts | 1 - packages/emails/templates/attendee-declined-email.ts | 3 +-- packages/emails/templates/attendee-location-change-email.ts | 1 - packages/emails/templates/attendee-request-email.ts | 1 - packages/emails/templates/attendee-rescheduled-email.ts | 1 - packages/emails/templates/organizer-cancelled-email.ts | 1 - packages/emails/templates/organizer-location-change-email.ts | 1 - .../emails/templates/organizer-payment-refund-failed-email.ts | 2 -- packages/emails/templates/organizer-request-reminder-email.ts | 1 - packages/emails/templates/organizer-rescheduled-email.ts | 1 - 11 files changed, 1 insertion(+), 13 deletions(-) diff --git a/packages/emails/templates/attendee-awaiting-payment-email.ts b/packages/emails/templates/attendee-awaiting-payment-email.ts index 66f6d38c01ee1f..8c012190e57a74 100644 --- a/packages/emails/templates/attendee-awaiting-payment-email.ts +++ b/packages/emails/templates/attendee-awaiting-payment-email.ts @@ -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"), }; diff --git a/packages/emails/templates/attendee-cancelled-email.ts b/packages/emails/templates/attendee-cancelled-email.ts index 71531b549e4b00..13d6f12e3747c9 100644 --- a/packages/emails/templates/attendee-cancelled-email.ts +++ b/packages/emails/templates/attendee-cancelled-email.ts @@ -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"), }; diff --git a/packages/emails/templates/attendee-declined-email.ts b/packages/emails/templates/attendee-declined-email.ts index e183e75a0ed6fa..8eae093c87b965 100644 --- a/packages/emails/templates/attendee-declined-email.ts +++ b/packages/emails/templates/attendee-declined-email.ts @@ -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.recurrence ? "event_request_declined_recurring" : "event_request_declined" ), }; } diff --git a/packages/emails/templates/attendee-location-change-email.ts b/packages/emails/templates/attendee-location-change-email.ts index fa47ac824ba37f..925ba0806df7c0 100644 --- a/packages/emails/templates/attendee-location-change-email.ts +++ b/packages/emails/templates/attendee-location-change-email.ts @@ -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"), }; diff --git a/packages/emails/templates/attendee-request-email.ts b/packages/emails/templates/attendee-request-email.ts index f82143ccc3a423..027a004c103e3e 100644 --- a/packages/emails/templates/attendee-request-email.ts +++ b/packages/emails/templates/attendee-request-email.ts @@ -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", { diff --git a/packages/emails/templates/attendee-rescheduled-email.ts b/packages/emails/templates/attendee-rescheduled-email.ts index 10f65ce3f5a75c..94137353253a03 100644 --- a/packages/emails/templates/attendee-rescheduled-email.ts +++ b/packages/emails/templates/attendee-rescheduled-email.ts @@ -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"), }; diff --git a/packages/emails/templates/organizer-cancelled-email.ts b/packages/emails/templates/organizer-cancelled-email.ts index b931b8c5004884..ef132374020569 100644 --- a/packages/emails/templates/organizer-cancelled-email.ts +++ b/packages/emails/templates/organizer-cancelled-email.ts @@ -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"), }; diff --git a/packages/emails/templates/organizer-location-change-email.ts b/packages/emails/templates/organizer-location-change-email.ts index 565d9ff6f77938..043b85630b84e5 100644 --- a/packages/emails/templates/organizer-location-change-email.ts +++ b/packages/emails/templates/organizer-location-change-email.ts @@ -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"), }; diff --git a/packages/emails/templates/organizer-payment-refund-failed-email.ts b/packages/emails/templates/organizer-payment-refund-failed-email.ts index 1632670d74b715..59142da7710097 100644 --- a/packages/emails/templates/organizer-payment-refund-failed-email.ts +++ b/packages/emails/templates/organizer-payment-refund-failed-email.ts @@ -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", diff --git a/packages/emails/templates/organizer-request-reminder-email.ts b/packages/emails/templates/organizer-request-reminder-email.ts index e2e4f1b68c1699..d87ed752c19796 100644 --- a/packages/emails/templates/organizer-request-reminder-email.ts +++ b/packages/emails/templates/organizer-request-reminder-email.ts @@ -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"), }; diff --git a/packages/emails/templates/organizer-rescheduled-email.ts b/packages/emails/templates/organizer-rescheduled-email.ts index d86a5c9a3be06f..551a555773a8d6 100644 --- a/packages/emails/templates/organizer-rescheduled-email.ts +++ b/packages/emails/templates/organizer-rescheduled-email.ts @@ -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"), }; From bf3bf97b1a06cff0e3aaa625a10be7e6febefc11 Mon Sep 17 00:00:00 2001 From: Leo Giovanetti Date: Tue, 7 Jun 2022 19:35:20 -0300 Subject: [PATCH 08/14] Reverting manual plural fixes --- apps/web/components/booking/BookingListItem.tsx | 11 ++++------- .../web/components/eventtype/EventTypeDescription.tsx | 2 +- apps/web/pages/cancel/[uid].tsx | 2 +- apps/web/pages/success.tsx | 2 +- packages/emails/src/components/WhenInfo.tsx | 2 +- 5 files changed, 8 insertions(+), 11 deletions(-) diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index 74eb8842a6680d..dc188168d16491 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -322,17 +322,14 @@ function BookingListItem(booking: BookingItemProps) {

{booking.status === BookingStatus.ACCEPTED - ? `${t(recurringDates.length > 1 ? "event_remaining_plural" : "event_remaining", { + ? `${t("event_remaining", { count: recurringDates.length, })}` : `${t("every_for_freq", { freq: t(bookingFrequency), - })} ${booking.recurringCount} ${t( - booking.recurringCount > 1 ? `${bookingFrequency}_plural` : bookingFrequency, - { - count: booking.recurringCount, - } - )}`} + })} ${booking.recurringCount} ${t(`${bookingFrequency}`, { + count: booking.recurringCount, + })}`}

diff --git a/apps/web/components/eventtype/EventTypeDescription.tsx b/apps/web/components/eventtype/EventTypeDescription.tsx index 49e41d5b03ecac..eb9c7a76973707 100644 --- a/apps/web/components/eventtype/EventTypeDescription.tsx +++ b/apps/web/components/eventtype/EventTypeDescription.tsx @@ -64,7 +64,7 @@ export const EventTypeDescription = ({ eventType, className }: EventTypeDescript {recurringEvent?.count && recurringEvent.count > 0 && (
  • diff --git a/apps/web/pages/cancel/[uid].tsx b/apps/web/pages/cancel/[uid].tsx index 48e3c6c012c229..90e83aaff4b78e 100644 --- a/apps/web/pages/cancel/[uid].tsx +++ b/apps/web/pages/cancel/[uid].tsx @@ -107,7 +107,7 @@ export default function Type(props: inferSSRProps) { {t( `${RRuleFrequency[props.booking.eventType.recurringEvent.freq] .toString() - .toLowerCase()}${props.recurringInstances.length > 1 ? "_plural" : ""}`, + .toLowerCase()}`, { count: props.recurringInstances.length, } diff --git a/apps/web/pages/success.tsx b/apps/web/pages/success.tsx index 7f2514b1b9c965..8f89b60aee67a6 100644 --- a/apps/web/pages/success.tsx +++ b/apps/web/pages/success.tsx @@ -629,7 +629,7 @@ function RecurringBookings({ eventType, recurringBookings, date, listingStatus } freq: t(`${bookingFrequency}`), })}{" "} {eventType.recurringEvent?.count}{" "} - {t(eventType.recurringEvent?.count > 1 ? `${bookingFrequency}_plural` : `${bookingFrequency}`, { + {t(`${bookingFrequency}`, { count: eventType.recurringEvent?.count, })} diff --git a/packages/emails/src/components/WhenInfo.tsx b/packages/emails/src/components/WhenInfo.tsx index 050c1b559fd9e1..50c1ab5b8d0558 100644 --- a/packages/emails/src/components/WhenInfo.tsx +++ b/packages/emails/src/components/WhenInfo.tsx @@ -17,7 +17,7 @@ function getRecurringWhen({ calEvent }: { calEvent: CalendarEvent }) { if (recurringEvent !== null && recurringEvent.count !== null) { return ` - ${t("every_for_freq", { freq: t(freqString), - })} ${recurringEvent.count} ${t(recurringEvent.count > 1 ? `${freqString}_plural` : freqString, { + })} ${recurringEvent.count} ${t(`${freqString}`, { count: recurringEvent.count, })}`; } From 239597ddc79ab5488c0eb7ce608b4359b20d58f0 Mon Sep 17 00:00:00 2001 From: Leo Giovanetti Date: Thu, 9 Jun 2022 17:45:33 -0300 Subject: [PATCH 09/14] Refactoring recurring event strings --- .../components/booking/BookingListItem.tsx | 34 +++--- .../booking/pages/AvailabilityPage.tsx | 109 +++++++++--------- .../components/booking/pages/BookingPage.tsx | 13 +-- .../eventtype/RecurringEventController.tsx | 2 +- apps/web/pages/[user]/book.tsx | 2 +- apps/web/pages/cancel/[uid].tsx | 23 +--- apps/web/pages/d/[link]/book.tsx | 2 +- apps/web/pages/success.tsx | 13 +-- apps/web/pages/team/[slug]/book.tsx | 2 +- apps/web/public/static/locales/en/common.json | 6 +- packages/emails/src/components/WhenInfo.tsx | 18 +-- .../templates/attendee-scheduled-email.ts | 3 +- packages/lib/package.json | 1 + packages/lib/recurringStrings.ts | 45 ++++++++ 14 files changed, 147 insertions(+), 126 deletions(-) create mode 100644 packages/lib/recurringStrings.ts diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index dc188168d16491..5ec1b2fb7383b8 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -13,11 +13,11 @@ 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 classNames from "@calcom/lib/classNames"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import showToast from "@calcom/lib/notification"; +import { getEveryFreqFor } from "@calcom/lib/recurringStrings"; import Button from "@calcom/ui/Button"; import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog"; import { Tooltip } from "@calcom/ui/Tooltip"; @@ -201,9 +201,7 @@ function BookingListItem(booking: BookingItemProps) { let recurringStrings: string[] = []; let recurringDates: Date[] = []; const today = new Date(); - let bookingFrequency = ""; if (booking.recurringCount && booking.eventType.recurringEvent?.freq !== undefined) { - bookingFrequency = RRuleFrequency[booking.eventType.recurringEvent?.freq].toString().toLowerCase(); [recurringStrings, recurringDates] = parseRecurringDates( { startDate: booking.startTime, @@ -300,9 +298,7 @@ function BookingListItem(booking: BookingItemProps) { - +
    {startTime}
    @@ -319,18 +315,20 @@ function BookingListItem(booking: BookingItemProps) { content={recurringStrings.map((aDate, key) => (

    {recurringStrings[key]}

    ))}> -

    - - {booking.status === BookingStatus.ACCEPTED - ? `${t("event_remaining", { - count: recurringDates.length, - })}` - : `${t("every_for_freq", { - freq: t(bookingFrequency), - })} ${booking.recurringCount} ${t(`${bookingFrequency}`, { - count: booking.recurringCount, - })}`} -

    +
    + +

    + {booking.status === BookingStatus.ACCEPTED + ? `${t("event_remaining", { + count: recurringDates.length, + })}` + : getEveryFreqFor({ + t, + recurringEvent: booking.eventType.recurringEvent, + recurringCount: booking.recurringCount, + })} +

    +
    diff --git a/apps/web/components/booking/pages/AvailabilityPage.tsx b/apps/web/components/booking/pages/AvailabilityPage.tsx index 0f6ed009b428c4..c180cbc673de51 100644 --- a/apps/web/components/booking/pages/AvailabilityPage.tsx +++ b/apps/web/components/booking/pages/AvailabilityPage.tsx @@ -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 { @@ -34,6 +33,7 @@ import { import classNames from "@calcom/lib/classNames"; import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { getRecurringFreq } from "@calcom/lib/recurringStrings"; import { localStorage } from "@calcom/lib/webstorage"; import { asStringOrNull } from "@lib/asStringOrNull"; @@ -296,36 +296,32 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage {eventType.length} {t("minutes")}

    - {!rescheduleUid && eventType.recurringEvent?.count && eventType.recurringEvent?.freq && ( -
    - -

    - {t("every_for_freq", { - freq: t( - `${RRuleFrequency[eventType.recurringEvent.freq].toString().toLowerCase()}` - ), - })} -

    - { - setRecurringEventCount(parseInt(event?.target.value)); - }} - /> -

    - {t( - `${RRuleFrequency[eventType.recurringEvent.freq].toString().toLowerCase()}`, - { + {!rescheduleUid && + eventType.recurringEvent?.count && + eventType.recurringEvent?.freq && + eventType.recurringEvent?.interval && ( +

    + +

    + {getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })} +

    + { + setRecurringEventCount(parseInt(event?.target.value)); + }} + /> +

    + {t("occurrence", { count: recurringEventCount, - } - )} -

    -
    - )} + })} +

    +
    + )} {eventType.price > 0 && (
    @@ -436,33 +432,32 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage {eventType.length} {t("minutes")}

    - {!rescheduleUid && eventType.recurringEvent?.count && eventType.recurringEvent?.freq && ( -
    - -

    - {t("every_for_freq", { - freq: t( - `${RRuleFrequency[eventType.recurringEvent.freq].toString().toLowerCase()}` - ), - })} -

    - { - setRecurringEventCount(parseInt(event?.target.value)); - }} - /> -

    - {t(`${RRuleFrequency[eventType.recurringEvent.freq].toString().toLowerCase()}`, { - count: recurringEventCount, - })} -

    -
    - )} + {!rescheduleUid && + eventType.recurringEvent?.count && + eventType.recurringEvent?.freq && + eventType.recurringEvent?.interval && ( +
    + +

    + {getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })} +

    + { + setRecurringEventCount(parseInt(event?.target.value)); + }} + /> +

    + {t("occurrence", { + count: recurringEventCount, + })} +

    +
    + )} {eventType.price > 0 && (

    diff --git a/apps/web/components/booking/pages/BookingPage.tsx b/apps/web/components/booking/pages/BookingPage.tsx index ca73d4805d8bed..a1cd88012a8d4f 100644 --- a/apps/web/components/booking/pages/BookingPage.tsx +++ b/apps/web/components/booking/pages/BookingPage.tsx @@ -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 { getEveryFreqFor } from "@calcom/lib/recurringStrings"; import { createPaymentLink } from "@calcom/stripe/client"; import { Button } from "@calcom/ui/Button"; import { Tooltip } from "@calcom/ui/Tooltip"; @@ -516,12 +516,11 @@ const BookingPage = ({

    - {`${t("every_for_freq", { - freq: t(`${RRuleFrequency[eventType.recurringEvent.freq].toString().toLowerCase()}`), - })} ${recurringEventCount} ${t( - `${RRuleFrequency[eventType.recurringEvent.freq].toString().toLowerCase()}`, - { count: parseInt(recurringEventCount.toString()) } - )}`} + {getEveryFreqFor({ + t, + recurringEvent: eventType.recurringEvent, + recurringCount: recurringEventCount, + })}

    )} diff --git a/apps/web/components/eventtype/RecurringEventController.tsx b/apps/web/components/eventtype/RecurringEventController.tsx index bd81513ea7bfa1..f5bfa2ad712085 100644 --- a/apps/web/components/eventtype/RecurringEventController.tsx +++ b/apps/web/components/eventtype/RecurringEventController.tsx @@ -134,7 +134,7 @@ export default function RecurringEventController({ }} />

    - {t(`${RRuleFrequency[recurringEventFrequency].toString().toLowerCase()}`, { + {t("occurrence", { count: recurringEventCount, })}

    diff --git a/apps/web/pages/[user]/book.tsx b/apps/web/pages/[user]/book.tsx index 9891e1db04e4d9..c11faac06e54d7 100644 --- a/apps/web/pages/[user]/book.tsx +++ b/apps/web/pages/[user]/book.tsx @@ -221,7 +221,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { (eventType.recurringEvent?.count && recurringEventCountQuery && (parseInt(recurringEventCountQuery) <= eventType.recurringEvent.count - ? recurringEventCountQuery + ? parseInt(recurringEventCountQuery) : eventType.recurringEvent.count)) || null; diff --git a/apps/web/pages/cancel/[uid].tsx b/apps/web/pages/cancel/[uid].tsx index 90e83aaff4b78e..b8b1178a8fe2e6 100644 --- a/apps/web/pages/cancel/[uid].tsx +++ b/apps/web/pages/cancel/[uid].tsx @@ -4,10 +4,10 @@ import dayjs from "dayjs"; import { GetServerSidePropsContext } from "next"; import { useRouter } from "next/router"; import { useState } from "react"; -import { Frequency as RRuleFrequency } from "rrule"; import classNames from "@calcom/lib/classNames"; import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { getEveryFreqFor } from "@calcom/lib/recurringStrings"; import prisma, { bookingMinimalSelect } from "@calcom/prisma"; import { RecurringEvent } from "@calcom/types/Calendar"; import { Button } from "@calcom/ui/Button"; @@ -96,22 +96,11 @@ export default function Type(props: inferSSRProps) {

    - {t("every_for_freq", { - freq: t( - RRuleFrequency[props.booking.eventType.recurringEvent.freq] - .toString() - .toLowerCase() - ), - })}{" "} - {props.recurringInstances.length}{" "} - {t( - `${RRuleFrequency[props.booking.eventType.recurringEvent.freq] - .toString() - .toLowerCase()}`, - { - count: props.recurringInstances.length, - } - )} + {getEveryFreqFor({ + t, + recurringEvent: props.booking.eventType.recurringEvent, + recurringCount: props.recurringInstances.length, + })}

    )} diff --git a/apps/web/pages/d/[link]/book.tsx b/apps/web/pages/d/[link]/book.tsx index ba2e528cb43623..b63dd6c2728752 100644 --- a/apps/web/pages/d/[link]/book.tsx +++ b/apps/web/pages/d/[link]/book.tsx @@ -158,7 +158,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { (eventTypeObject?.recurringEvent?.count && recurringEventCountQuery && (parseInt(recurringEventCountQuery) <= eventTypeObject.recurringEvent.count - ? recurringEventCountQuery + ? parseInt(recurringEventCountQuery) : eventType.recurringEvent.count)) || null; diff --git a/apps/web/pages/success.tsx b/apps/web/pages/success.tsx index 8f89b60aee67a6..0438eccbce00f4 100644 --- a/apps/web/pages/success.tsx +++ b/apps/web/pages/success.tsx @@ -13,7 +13,7 @@ import { useSession } from "next-auth/react"; import Link from "next/link"; import { useRouter } from "next/router"; import { useEffect, useRef, useState } from "react"; -import RRule, { Frequency as RRuleFrequency } from "rrule"; +import RRule from "rrule"; import { z } from "zod"; import { SpaceBookingSuccessPage } from "@calcom/app-store/spacebooking/components"; @@ -25,6 +25,7 @@ import { } from "@calcom/embed-core/embed-iframe"; import { getDefaultEvent } from "@calcom/lib/defaultEvents"; import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { getEveryFreqFor } from "@calcom/lib/recurringStrings"; import { localStorage } from "@calcom/lib/webstorage"; import { Prisma } from "@calcom/prisma/client"; import { RecurringEvent } from "@calcom/types/Calendar"; @@ -619,19 +620,11 @@ type RecurringBookingsProps = { function RecurringBookings({ eventType, recurringBookings, date, listingStatus }: RecurringBookingsProps) { const [moreEventsVisible, setMoreEventsVisible] = useState(false); const { t } = useLocale(); - const bookingFrequency = - eventType.recurringEvent?.freq && RRuleFrequency[eventType.recurringEvent?.freq].toString().toLowerCase(); return recurringBookings && listingStatus === "recurring" ? ( <> {eventType.recurringEvent?.count && ( - {t("every_for_freq", { - freq: t(`${bookingFrequency}`), - })}{" "} - {eventType.recurringEvent?.count}{" "} - {t(`${bookingFrequency}`, { - count: eventType.recurringEvent?.count, - })} + {getEveryFreqFor({ t, recurringEvent: eventType.recurringEvent })} )} {eventType.recurringEvent?.count && diff --git a/apps/web/pages/team/[slug]/book.tsx b/apps/web/pages/team/[slug]/book.tsx index 69368e23b2aea9..2895987b6f8ec7 100644 --- a/apps/web/pages/team/[slug]/book.tsx +++ b/apps/web/pages/team/[slug]/book.tsx @@ -97,7 +97,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { (eventType.recurringEvent?.count && recurringEventCountQuery && (parseInt(recurringEventCountQuery) <= eventType.recurringEvent.count - ? recurringEventCountQuery + ? parseInt(recurringEventCountQuery) : eventType.recurringEvent.count)) || null; diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 1c71479c139ecc..539aa6784cdaf4 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -554,9 +554,11 @@ "repeats_up_to_one": "Repeats up to {{count}} time", "repeats_up_to_other": "Repeats up to {{count}} times", "every_for_freq": "Every {{freq}} for", - "event_remaining": "{{count}} event remaining", - "event_remaining_plural": "{{count}} events remaining", + "event_remaining_one": "{{count}} event remaining", + "event_remaining_other": "{{count}} events remaining", "repeats_every": "Repeats every", + "occurrence_one": "occurrence", + "occurrence_other": "occurrences", "weekly_one": "week", "weekly_other": "weeks", "monthly_one": "month", diff --git a/packages/emails/src/components/WhenInfo.tsx b/packages/emails/src/components/WhenInfo.tsx index 50c1ab5b8d0558..be137721b19f70 100644 --- a/packages/emails/src/components/WhenInfo.tsx +++ b/packages/emails/src/components/WhenInfo.tsx @@ -3,7 +3,9 @@ import timezone from "dayjs/plugin/timezone"; import { TFunction } from "next-i18next"; import rrule from "rrule"; +import { getEveryFreqFor } from "@calcom/lib/recurringStrings"; import type { CalendarEvent } from "@calcom/types/Calendar"; +import { RecurringEvent } from "@calcom/types/Calendar"; import { Info } from "./Info"; @@ -12,15 +14,13 @@ dayjs.extend(timezone); function getRecurringWhen({ calEvent }: { calEvent: CalendarEvent }) { if (calEvent.recurrence !== undefined) { const t = calEvent.attendees[0].language.translate; - const recurringEvent = rrule.fromString(calEvent.recurrence).options; - const freqString = rrule.FREQUENCIES[recurringEvent.freq].toString().toLowerCase(); - if (recurringEvent !== null && recurringEvent.count !== null) { - return ` - ${t("every_for_freq", { - freq: t(freqString), - })} ${recurringEvent.count} ${t(`${freqString}`, { - count: recurringEvent.count, - })}`; - } + const rruleOptions = rrule.fromString(calEvent.recurrence).options; + const recurringEvent: RecurringEvent = { + freq: rruleOptions.freq, + count: rruleOptions.count || 1, + interval: rruleOptions.interval, + }; + return ` - ${getEveryFreqFor({ t, recurringEvent })}`; } return ""; } diff --git a/packages/emails/templates/attendee-scheduled-email.ts b/packages/emails/templates/attendee-scheduled-email.ts index 57dff4b2fff071..1e7fa73f5603ee 100644 --- a/packages/emails/templates/attendee-scheduled-email.ts +++ b/packages/emails/templates/attendee-scheduled-email.ts @@ -5,10 +5,9 @@ import toArray from "dayjs/plugin/toArray"; import utc from "dayjs/plugin/utc"; import { createEvent, DateArray } from "ics"; 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"; diff --git a/packages/lib/package.json b/packages/lib/package.json index 017004bd7e88e2..122cc8c25fc6ab 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -12,6 +12,7 @@ "ics": "^2.31.0", "next-i18next": "^11.0.0", "react-hot-toast": "^2.1.0", + "rrule": "^2.6.9", "tsdav": "2.0.2", "tslog": "^3.2.1", "uuid": "^8.3.2" diff --git a/packages/lib/recurringStrings.ts b/packages/lib/recurringStrings.ts new file mode 100644 index 00000000000000..237a3df1bf483e --- /dev/null +++ b/packages/lib/recurringStrings.ts @@ -0,0 +1,45 @@ +import { TFunction } from "next-i18next"; +import { Frequency as RRuleFrequency } from "rrule"; + +import { RecurringEvent } from "@calcom/types/Calendar"; + +export const getRecurringFreq = ({ + t, + recurringEvent, +}: { + t: TFunction; + recurringEvent: RecurringEvent; +}): string => { + if (recurringEvent.interval && recurringEvent.freq) { + return t("every_for_freq", { + freq: `${recurringEvent.interval > 1 ? recurringEvent.interval : ""} ${t( + RRuleFrequency[recurringEvent.freq].toString().toLowerCase(), + { + count: recurringEvent.interval, + } + )}`, + }); + } + return ""; +}; + +export const getEveryFreqFor = ({ + t, + recurringEvent, + recurringCount, + recurringFreq, +}: { + t: TFunction; + recurringEvent: RecurringEvent; + recurringCount?: number; + recurringFreq?: string; +}): string => { + if (recurringEvent.freq) { + return `${recurringFreq || getRecurringFreq({ t, recurringEvent })} ${ + recurringCount || recurringEvent.count + } ${t("occurrences", { + count: recurringCount || recurringEvent.count, + })}`; + } + return ""; +}; From ddbf7ca3975a53ecf2fe2f46e75e1865e1638b31 Mon Sep 17 00:00:00 2001 From: Leo Giovanetti Date: Fri, 10 Jun 2022 11:41:50 -0300 Subject: [PATCH 10/14] Correcting merge issues --- apps/web/pages/api/book/event.ts | 2 -- packages/emails/templates/attendee-scheduled-email.ts | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/web/pages/api/book/event.ts b/apps/web/pages/api/book/event.ts index cba90c29bf1213..774d040f53e1d3 100644 --- a/apps/web/pages/api/book/event.ts +++ b/apps/web/pages/api/book/event.ts @@ -414,8 +414,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) // 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) eventType.recurringEvent = Object.assign({}, eventType.recurringEvent, { count: recurringCount }); - // Taking care of recurrence rule - evt.recurrence = new rrule(eventType.recurringEvent).toString(); } // Initialize EventManager with credentials diff --git a/packages/emails/templates/attendee-scheduled-email.ts b/packages/emails/templates/attendee-scheduled-email.ts index c903fa8b785792..226003b180fc29 100644 --- a/packages/emails/templates/attendee-scheduled-email.ts +++ b/packages/emails/templates/attendee-scheduled-email.ts @@ -5,6 +5,7 @@ import toArray from "dayjs/plugin/toArray"; import utc from "dayjs/plugin/utc"; import { createEvent, DateArray } from "ics"; import { TFunction } from "next-i18next"; +import rrule from "rrule"; import { getRichDescription } from "@calcom/lib/CalEventParser"; import type { CalendarEvent, Person } from "@calcom/types/Calendar"; From 334572c9a818b70a7de533cb1df62bd4c0e117dd Mon Sep 17 00:00:00 2001 From: Leo Giovanetti Date: Fri, 10 Jun 2022 12:24:29 -0300 Subject: [PATCH 11/14] Relying on newly introduced obj --- packages/emails/src/templates/OrganizerRequestEmail.tsx | 2 +- packages/emails/templates/organizer-request-email.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/emails/src/templates/OrganizerRequestEmail.tsx b/packages/emails/src/templates/OrganizerRequestEmail.tsx index 5cc80a063cf1f4..f16d15a5874c54 100644 --- a/packages/emails/src/templates/OrganizerRequestEmail.tsx +++ b/packages/emails/src/templates/OrganizerRequestEmail.tsx @@ -16,7 +16,7 @@ export const OrganizerRequestEmail = (props: React.ComponentProps Date: Fri, 10 Jun 2022 14:54:37 -0300 Subject: [PATCH 12/14] Fixing mobile --- .../booking/pages/AvailabilityPage.tsx | 108 +++++++----------- 1 file changed, 42 insertions(+), 66 deletions(-) diff --git a/apps/web/components/booking/pages/AvailabilityPage.tsx b/apps/web/components/booking/pages/AvailabilityPage.tsx index d7942f47cfe5dc..19c17722803d68 100644 --- a/apps/web/components/booking/pages/AvailabilityPage.tsx +++ b/apps/web/components/booking/pages/AvailabilityPage.tsx @@ -35,7 +35,6 @@ import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { getRecurringFreq } from "@calcom/lib/recurringStrings"; import { localStorage } from "@calcom/lib/webstorage"; -import { Frequency } from "@calcom/prisma/zod-utils"; import { asStringOrNull } from "@lib/asStringOrNull"; import { timeZone } from "@lib/clock"; @@ -297,29 +296,6 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage {eventType.length} {t("minutes")}

    - {!rescheduleUid && eventType.recurringEvent && ( -
    - -

    - {getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })} -

    - { - setRecurringEventCount(parseInt(event?.target.value)); - }} - /> -

    - {t("occurrence", { - count: recurringEventCount, - })} -

    -
    - )} {eventType.price > 0 && (
    @@ -334,29 +310,27 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage )} {!rescheduleUid && eventType.recurringEvent && (
    - -

    - {t("every_for_freq", { - freq: t( - `${Frequency[eventType.recurringEvent.freq].toString().toLowerCase()}` - ), - })} -

    - { - setRecurringEventCount(parseInt(event?.target.value)); - }} - /> -

    - {t(`${Frequency[eventType.recurringEvent.freq].toString().toLowerCase()}`, { - count: recurringEventCount, - })} -

    + +
    +

    + {getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })} +

    + { + setRecurringEventCount(parseInt(event?.target.value)); + }} + /> +

    + {t("occurrence", { + count: recurringEventCount, + })} +

    +
    )} @@ -459,25 +433,27 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage

    {!rescheduleUid && eventType.recurringEvent && (
    - -

    - {getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })} -

    - { - setRecurringEventCount(parseInt(event?.target.value)); - }} - /> -

    - {t("occurrence", { - count: recurringEventCount, - })} -

    + +
    +

    + {getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })} +

    + { + setRecurringEventCount(parseInt(event?.target.value)); + }} + /> +

    + {t("occurrence", { + count: recurringEventCount, + })} +

    +
    )} {eventType.price > 0 && ( From 5b727fb2acf68e82c3ba2d02b23bb77bcca2314c Mon Sep 17 00:00:00 2001 From: Leo Giovanetti Date: Fri, 10 Jun 2022 15:11:50 -0300 Subject: [PATCH 13/14] Final tweaks --- apps/web/components/booking/BookingListItem.tsx | 2 +- apps/web/ee/pages/api/integrations/stripepayment/webhook.ts | 1 - apps/web/pages/api/book/confirm.ts | 1 - apps/web/pages/api/cancel.ts | 3 +-- 4 files changed, 2 insertions(+), 5 deletions(-) diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index fe37bb6ed2ad6c..429f3ea16d0149 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -314,7 +314,7 @@ function BookingListItem(booking: BookingItemProps) {
    ( -

    {recurringStrings[key]}

    +

    {aDate}

    ))}>
    diff --git a/apps/web/ee/pages/api/integrations/stripepayment/webhook.ts b/apps/web/ee/pages/api/integrations/stripepayment/webhook.ts index a76f5c76903b05..dfdb8badaadcba 100644 --- a/apps/web/ee/pages/api/integrations/stripepayment/webhook.ts +++ b/apps/web/ee/pages/api/integrations/stripepayment/webhook.ts @@ -1,7 +1,6 @@ import { BookingStatus, Prisma } from "@prisma/client"; import { buffer } from "micro"; import type { NextApiRequest, NextApiResponse } from "next"; -import rrule from "rrule"; import Stripe from "stripe"; import EventManager from "@calcom/core/EventManager"; diff --git a/apps/web/pages/api/book/confirm.ts b/apps/web/pages/api/book/confirm.ts index cfe524b8c56cc8..05cbd1bfdf8d66 100644 --- a/apps/web/pages/api/book/confirm.ts +++ b/apps/web/pages/api/book/confirm.ts @@ -1,6 +1,5 @@ import { Booking, BookingStatus, Prisma, SchedulingType, User } from "@prisma/client"; import type { NextApiRequest } from "next"; -import rrule from "rrule"; import { z } from "zod"; import EventManager from "@calcom/core/EventManager"; diff --git a/apps/web/pages/api/cancel.ts b/apps/web/pages/api/cancel.ts index 0920c3fed79baf..44d61f15d40f70 100644 --- a/apps/web/pages/api/cancel.ts +++ b/apps/web/pages/api/cancel.ts @@ -2,7 +2,6 @@ import { BookingStatus, Credential, WebhookTriggerEvents } from "@prisma/client" import async from "async"; import dayjs from "dayjs"; import { NextApiRequest, NextApiResponse } from "next"; -import rrule from "rrule"; import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter"; import { getCalendar } from "@calcom/core/CalendarManager"; @@ -10,7 +9,7 @@ import { deleteMeeting } from "@calcom/core/videoClient"; import { sendCancelledEmails } from "@calcom/emails"; import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib"; import prisma, { bookingMinimalSelect } from "@calcom/prisma"; -import type { CalendarEvent, RecurringEvent } from "@calcom/types/Calendar"; +import type { CalendarEvent } from "@calcom/types/Calendar"; import { refund } from "@ee/lib/stripe/server"; import { asStringOrNull } from "@lib/asStringOrNull"; From 720860e9e07545bcf016998e20832cbe80ef1ff1 Mon Sep 17 00:00:00 2001 From: Leo Giovanetti Date: Fri, 10 Jun 2022 15:42:08 -0300 Subject: [PATCH 14/14] Latest fixes --- apps/web/components/booking/CancelBooking.tsx | 2 +- apps/web/components/eventtype/RecurringEventController.tsx | 2 +- apps/web/pages/api/cancel.ts | 1 - apps/web/pages/cancel/[uid].tsx | 4 ++-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/web/components/booking/CancelBooking.tsx b/apps/web/components/booking/CancelBooking.tsx index d30718d32e3d13..1b26e67ca1f204 100644 --- a/apps/web/components/booking/CancelBooking.tsx +++ b/apps/web/components/booking/CancelBooking.tsx @@ -18,7 +18,7 @@ type Props = { name: string | null; slug: string | null; }; - recurringEvent: RecurringEvent; + recurringEvent: RecurringEvent | null; team?: string | null; setIsCancellationMode: (value: boolean) => void; theme: string | null; diff --git a/apps/web/components/eventtype/RecurringEventController.tsx b/apps/web/components/eventtype/RecurringEventController.tsx index a68a99850d9515..35dafd11e7bee3 100644 --- a/apps/web/components/eventtype/RecurringEventController.tsx +++ b/apps/web/components/eventtype/RecurringEventController.tsx @@ -127,7 +127,7 @@ export default function RecurringEventController({ />

    {t("occurrence", { - count: recurringEventCount, + count: recurringEventState.count, })}

    diff --git a/apps/web/pages/api/cancel.ts b/apps/web/pages/api/cancel.ts index 44d61f15d40f70..4106b993a669fc 100644 --- a/apps/web/pages/api/cancel.ts +++ b/apps/web/pages/api/cancel.ts @@ -61,7 +61,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) select: { recurringEvent: true, title: true, - recurringEvent: true, }, }, uid: true, diff --git a/apps/web/pages/cancel/[uid].tsx b/apps/web/pages/cancel/[uid].tsx index b8b1178a8fe2e6..6ba899dd22a944 100644 --- a/apps/web/pages/cancel/[uid].tsx +++ b/apps/web/pages/cancel/[uid].tsx @@ -7,9 +7,9 @@ import { useState } from "react"; import classNames from "@calcom/lib/classNames"; import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent"; import { getEveryFreqFor } from "@calcom/lib/recurringStrings"; import prisma, { bookingMinimalSelect } from "@calcom/prisma"; -import { RecurringEvent } from "@calcom/types/Calendar"; import { Button } from "@calcom/ui/Button"; import { TextField } from "@calcom/ui/form/fields"; @@ -267,7 +267,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => endTime: booking.endTime.toString(), eventType: { ...booking.eventType, - recurringEvent: (booking.eventType?.recurringEvent || null) as RecurringEvent, + recurringEvent: parseRecurringEvent(booking.eventType?.recurringEvent), }, });