From b285f27d00289939b5183e7ce3587b3f12db7c32 Mon Sep 17 00:00:00 2001 From: Monto <138862352+montocoder@users.noreply.github.com> Date: Tue, 29 Aug 2023 13:56:26 +0200 Subject: [PATCH] feat: option for adding ics events to workflow reminders (#10856) Co-authored-by: SMLukwiya Co-authored-by: Monto <138862352+monto7926@users.noreply.github.com> Co-authored-by: Udit Takkar --- apps/web/public/static/locales/en/common.json | 1 + .../workflows/api/scheduleEmailReminders.ts | 76 +++++++++++++++++ .../components/WorkflowDetailsPage.tsx | 1 + .../components/WorkflowStepContainer.tsx | 23 +++++ .../lib/reminders/emailReminderManager.ts | 84 +++++++++++++++++-- .../lib/reminders/reminderScheduler.ts | 3 +- .../lib/reminders/smsReminderManager.ts | 3 +- .../features/ee/workflows/pages/workflow.tsx | 1 + packages/features/form-builder/utils.ts | 5 +- .../migration.sql | 2 + packages/prisma/schema.prisma | 1 + .../viewer/workflows/update.handler.ts | 1 + .../routers/viewer/workflows/update.schema.ts | 1 + 13 files changed, 192 insertions(+), 10 deletions(-) create mode 100644 packages/prisma/migrations/20230828094603_add_include_calendar_event/migration.sql diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index c7546eab0b..082de5a93c 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -2026,5 +2026,6 @@ "value": "Value", "your_organization_updated_sucessfully": "Your organization updated successfully", "seat_options_doesnt_multiple_durations": "Seat option doesn't support multiple durations", + "include_calendar_event": "Include calendar event", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/packages/features/ee/workflows/api/scheduleEmailReminders.ts b/packages/features/ee/workflows/api/scheduleEmailReminders.ts index f6ac593d94..b63b1ca47c 100644 --- a/packages/features/ee/workflows/api/scheduleEmailReminders.ts +++ b/packages/features/ee/workflows/api/scheduleEmailReminders.ts @@ -1,10 +1,16 @@ /* Schedule any workflow reminder that falls within 72 hours for email */ +import type { Prisma } from "@prisma/client"; import client from "@sendgrid/client"; import sgMail from "@sendgrid/mail"; +import { createEvent } from "ics"; +import type { DateArray } from "ics"; import type { NextApiRequest, NextApiResponse } from "next"; +import { RRule } from "rrule"; +import { v4 as uuidv4 } from "uuid"; import dayjs from "@calcom/dayjs"; import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses"; +import { parseRecurringEvent } from "@calcom/lib"; import { defaultHandler } from "@calcom/lib/server"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import prisma from "@calcom/prisma"; @@ -20,6 +26,65 @@ const senderEmail = process.env.SENDGRID_EMAIL as string; sgMail.setApiKey(sendgridAPIKey); +type Booking = Prisma.BookingGetPayload<{ + include: { + eventType: true; + user: true; + attendees: true; + }; +}>; + +function getiCalEventAsString(booking: Booking) { + let recurrenceRule: string | undefined = undefined; + const recurringEvent = parseRecurringEvent(booking.eventType?.recurringEvent); + if (recurringEvent?.count) { + recurrenceRule = new RRule(recurringEvent).toString().replace("RRULE:", ""); + } + + const uid = uuidv4(); + + const icsEvent = createEvent({ + uid, + startInputType: "utc", + start: dayjs(booking.startTime.toISOString() || "") + .utc() + .toArray() + .slice(0, 6) + .map((v, i) => (i === 1 ? v + 1 : v)) as DateArray, + duration: { + minutes: dayjs(booking.endTime.toISOString() || "").diff( + dayjs(booking.startTime.toISOString() || ""), + "minute" + ), + }, + title: booking.eventType?.title || "", + description: booking.description || "", + location: booking.location || "", + organizer: { + email: booking.user?.email || "", + name: booking.user?.name || "", + }, + attendees: [ + { + name: booking.attendees[0].name, + email: booking.attendees[0].email, + partstat: "ACCEPTED", + role: "REQ-PARTICIPANT", + rsvp: true, + }, + ], + method: "REQUEST", + ...{ recurrenceRule }, + status: "CONFIRMED", + }); + + if (icsEvent.error) { + throw icsEvent.error; + } + + return icsEvent.value; +} + async function handler(req: NextApiRequest, res: NextApiResponse) { const apiKey = req.headers.authorization || req.query.apiKey; if (process.env.CRON_API_KEY !== apiKey) { @@ -258,6 +323,17 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { enable: sandboxMode, }, }, + attachments: reminder.workflowStep.includeCalendarEvent + ? [ + { + content: Buffer.from(getiCalEventAsString(reminder.booking) || "").toString("base64"), + filename: "event.ics", + type: "text/calendar; method=REQUEST", + disposition: "attachment", + contentId: uuidv4(), + }, + ] + : undefined, }); } diff --git a/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx b/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx index 4760d25175..a5950cc49c 100644 --- a/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx +++ b/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx @@ -113,6 +113,7 @@ export default function WorkflowDetailsPage(props: Props) { sender: isSMSAction(action) ? sender || SENDER_ID : SENDER_ID, senderName: !isSMSAction(action) ? senderName || SENDER_NAME : SENDER_NAME, numberVerificationPending: false, + includeCalendarEvent: false, }; steps?.push(step); form.setValue("steps", steps); diff --git a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx index 228c890c9e..65f9e14ac5 100644 --- a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx +++ b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx @@ -861,6 +861,29 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { {form.formState?.errors?.steps[step.stepNumber - 1]?.reminderBody?.message || ""}

)} + {isEmailSubjectNeeded && ( +
+ ( + + form.setValue( + `steps.${step.stepNumber - 1}.includeCalendarEvent`, + e.target.checked + ) + } + /> + )} + /> +
+ )} {!props.readOnly && (