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 && (