+2




![kodiakhq[bot]](/avatar/78f03f45def735066748285e1c83c542df49e7b0562b0a8da815d5b24845d135?size=40)



eeabb53c82
* Fix breadcrumb colors * HorizontalTabs * Team List Item WIP * Horizontal Tabs * Cards * Remove team list item WIP * Login Page * Add welcome back i118n * EventType page work * Update EventType Icons * WIP Availability * Horizontal Tab Work * Add build command for in root * Update build DIr/command * Add Edit Button + change buttons to v2 * Availablitiy page * Fix IPAD * Make mobile look a little nicer * WIP bookingshell * Remove list items from breaking build * Mian bulk of Booking Page. * Few updates to components * Fix chormatic feedback * Fix banner * Fix Empty Screen * Text area + embded window fixes * Semi fix avatar * Troubleshoot container + Active on count * Improve mobile * NITS * Fix padding on input * Fix icons * Starting to move event types settings to tabs * Begin migration to single page form * Single page tabs * Limits Page * Advanced tab * Add RHF to dependancies * Most of advanced tab * Solved RHF mismtach * Build fixes * RHF conditionals fixes * Improved legibility * Major refactor/organisation into optional V2 UI * Portal EditLocationModal * Fix dialoug form * Update imports * Auto Animate + custom inputs WIP * Custom Inputs * WIP Apps * Fixing stories imports * Stripe app * Remove duplicate dialog * Remove duplicate dialog * add Test action button + UI improvements * add test action functionality * Fix embed URL * Fix app toggles + number of active apps * Fix container padding on disabledBorder prop * Removes strict * add confirmation dialog before sending SMS * code clean up * show error message if test action fails * disable test action button in edit mode * fixes SMS testing * use updated values * fix wrongly updated data in useEffect * fix typo * code clean up * EventType Team page WIP * Fix embed * NIT * Add Darkmode gray color * V2 Shell WIP * setup folders for v2 * add lost translations from merge * add all files to v2 + redesign /workflows * use custom template as default template * add first version of v2 for workflow editing page * Fix headings on shell V2 * Fix mobile layout with V2 shell * V2 create event type button * Checked Team Select * Hidden to happen on save - not on toggle * Team Attendee Select animation * Fix scheduling type and remove multi select label * Fix overflow on teams url * use fi icon for down arrow * add v2 shell * add trigger badge * Even Type move order handles * fix save button * enable editing of workflow name * improve delete workflow functionality * adjust empty screen * make trigger start with uppercase * change trash icon * Fix Embed TS errors * Fix TS errors * Fix Eslint errors * Fix TS errors for UI * Fix ESLINT error * create component for time and timeUnit input/dropdown * add workflows to v2 early access middleware * fix type issues * added SidebarCard for promo to v2 and storybook (#3906) Co-authored-by: Julian Benegas <julianbenegas99@gmail.com> Co-authored-by: Alan <alannnc@gmail.com> Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> * Tooltip Provider - Wrapper due to dep upgrade * public event type list darkmode * V2 Color changes to public booking * adjust delete and edit button * create custom empty screen for worklows * add workflow examples to empty page * add loading state to button when creating first workflow * Show action as workflow name when no name exists * if no input leave name empty when updating workflow * use reminder template when creating workflow * improve time unit dropdown * Remove unused component * Fix typecheck * add loading state to new workflow button * add label before action dropdown * fixing badges in worflow list * use lighter color for untitled workflows * fix not showing nr of active eventtypes in workflow list * add workflows to event types * implement v2 desing for AddActionDialog * improve phone input * rounded borders for phoneInput in add action dialog * improve message input * fix mobile view for editing page * use md breakpoint for mobile view * finish workflows in event type settings * code cleanup * code cleanup * fix bug in testing workflow action * add v2 design for addition inputs as variables dialog * add V2 design for add variable dropdown * add notification icons to trigger badge * improve mobile version of workflows in eventtypes * remove ring from time before input * fix bug when workflow has no steps * Final UI improvements/fixes * code clean-up * code clean-up * code clean-up * use v2 design for license required * fix translation issues * fix adding variables in different language in old design * limit for pro users only for now * fix import * fix mobile view for empty screen * ues destructive button color for deleting workfow * remove padding at button of creation dialog * V2 Settings - Security View (#4018) * Create change password screen * Add two factor auth screen * Add two factor auth screen * Remove header file * Updates middleware and rewrites * Adds Meta component to handle layout headings/metadata (#4021) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: zomars <zomars@me.com> * fix: rate limit auth (#3820) * fix: rate limit auth * fix: replace lru-cache w memory-cache * remove comments * fix: yarn.lock * fix: remove changes yarn lock * fix: add missing EOL empty liune * fix: move rate limiter so it kicks the last, limit to 10 tries per minute * fix: move limiter w rest of code * test: trying fix onboardong * fix: undo changes in globalSetup.ts * test: fix disable login for onboarding * fix: use username instead of email for token check * fix: tests * fix: don't run on test * fix: add missing comma * fix: remove uniqueTokenPerInterval * fix: add errorcode to packages lib auth * Update packages/lib/rateLimit.ts fix: improve readability Co-authored-by: Omar López <zomars@me.com> * Update packages/lib/rateLimit.ts fix: no unnecessary any Co-authored-by: Omar López <zomars@me.com> * Update packages/lib/rateLimit.ts fix: improve readability Co-authored-by: Omar López <zomars@me.com> * fix: rename interval -> intervalInMs * fix: check user.email not username which could be empty * fix: rateLimit update all naming Co-authored-by: Agusti Fernandez Pardo <git@agusti.me> Co-authored-by: Omar López <zomars@me.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> * Adds deprecation comments Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: zomars <zomars@me.com> Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: Hariom Balhara <hariombalhara@gmail.com> Co-authored-by: Julian Benegas <julianbenegas99@gmail.com> Co-authored-by: Alan <alannnc@gmail.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Co-authored-by: Agusti Fernandez Pardo <6601142+agustif@users.noreply.github.com> Co-authored-by: Agusti Fernandez Pardo <git@agusti.me>
902 lines
27 KiB
TypeScript
902 lines
27 KiB
TypeScript
import {
|
|
Prisma,
|
|
PrismaPromise,
|
|
WorkflowTemplates,
|
|
WorkflowActions,
|
|
WorkflowTriggerEvents,
|
|
BookingStatus,
|
|
WorkflowMethods,
|
|
TimeUnit,
|
|
} from "@prisma/client";
|
|
import { z } from "zod";
|
|
|
|
import dayjs from "@calcom/dayjs";
|
|
import {
|
|
WORKFLOW_TEMPLATES,
|
|
WORKFLOW_TRIGGER_EVENTS,
|
|
WORKFLOW_ACTIONS,
|
|
TIME_UNIT,
|
|
} from "@calcom/features/ee/workflows/lib/constants";
|
|
import {
|
|
deleteScheduledEmailReminder,
|
|
scheduleEmailReminder,
|
|
} from "@calcom/features/ee/workflows/lib/reminders/emailReminderManager";
|
|
import {
|
|
BookingInfo,
|
|
deleteScheduledSMSReminder,
|
|
scheduleSMSReminder,
|
|
} from "@calcom/features/ee/workflows/lib/reminders/smsReminderManager";
|
|
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
|
import { getTranslation } from "@calcom/lib/server/i18n";
|
|
|
|
import { TRPCError } from "@trpc/server";
|
|
|
|
import { createProtectedRouter } from "../../createRouter";
|
|
|
|
export const workflowsRouter = createProtectedRouter()
|
|
.query("list", {
|
|
async resolve({ ctx }) {
|
|
const workflows = await ctx.prisma.workflow.findMany({
|
|
where: {
|
|
userId: ctx.user.id,
|
|
},
|
|
include: {
|
|
activeOn: {
|
|
select: {
|
|
eventType: {
|
|
select: {
|
|
id: true,
|
|
title: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
steps: true,
|
|
},
|
|
orderBy: {
|
|
id: "asc",
|
|
},
|
|
});
|
|
|
|
return { workflows };
|
|
},
|
|
})
|
|
.query("get", {
|
|
input: z.object({
|
|
id: z.number(),
|
|
}),
|
|
async resolve({ ctx, input }) {
|
|
const workflow = await ctx.prisma.workflow.findFirst({
|
|
where: {
|
|
userId: ctx.user.id,
|
|
id: input.id,
|
|
},
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
time: true,
|
|
timeUnit: true,
|
|
activeOn: {
|
|
select: {
|
|
eventType: true,
|
|
},
|
|
},
|
|
trigger: true,
|
|
steps: {
|
|
orderBy: {
|
|
stepNumber: "asc",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
if (!workflow) {
|
|
throw new TRPCError({
|
|
code: "UNAUTHORIZED",
|
|
});
|
|
}
|
|
return workflow;
|
|
},
|
|
})
|
|
.mutation("create", {
|
|
input: z.object({
|
|
name: z.string(),
|
|
trigger: z.enum(WORKFLOW_TRIGGER_EVENTS),
|
|
action: z.enum(WORKFLOW_ACTIONS),
|
|
timeUnit: z.enum(TIME_UNIT).optional(),
|
|
time: z.number().optional(),
|
|
sendTo: z.string().optional(),
|
|
}),
|
|
async resolve({ ctx, input }) {
|
|
const { name, trigger, action, timeUnit, time, sendTo } = input;
|
|
const userId = ctx.user.id;
|
|
|
|
try {
|
|
const workflow = await ctx.prisma.workflow.create({
|
|
data: {
|
|
name,
|
|
trigger,
|
|
userId,
|
|
timeUnit: time ? timeUnit : undefined,
|
|
time,
|
|
},
|
|
});
|
|
|
|
await ctx.prisma.workflowStep.create({
|
|
data: {
|
|
stepNumber: 1,
|
|
action,
|
|
workflowId: workflow.id,
|
|
sendTo,
|
|
},
|
|
});
|
|
return { workflow };
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
},
|
|
})
|
|
.mutation("createV2", {
|
|
async resolve({ ctx }) {
|
|
const userId = ctx.user.id;
|
|
|
|
try {
|
|
const workflow = await ctx.prisma.workflow.create({
|
|
data: {
|
|
name: "",
|
|
trigger: WorkflowTriggerEvents.BEFORE_EVENT,
|
|
time: 24,
|
|
timeUnit: TimeUnit.HOUR,
|
|
userId,
|
|
},
|
|
});
|
|
|
|
await ctx.prisma.workflowStep.create({
|
|
data: {
|
|
stepNumber: 1,
|
|
action: WorkflowActions.EMAIL_HOST,
|
|
template: WorkflowTemplates.REMINDER,
|
|
workflowId: workflow.id,
|
|
},
|
|
});
|
|
return { workflow };
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
},
|
|
})
|
|
.mutation("delete", {
|
|
input: z.object({
|
|
id: z.number(),
|
|
}),
|
|
async resolve({ ctx, input }) {
|
|
const { id } = input;
|
|
|
|
const workflowToDelete = await ctx.prisma.workflow.findFirst({
|
|
where: {
|
|
id,
|
|
userId: ctx.user.id,
|
|
},
|
|
});
|
|
|
|
if (workflowToDelete) {
|
|
const scheduledReminders = await ctx.prisma.workflowReminder.findMany({
|
|
where: {
|
|
workflowStep: {
|
|
workflowId: id,
|
|
},
|
|
scheduled: true,
|
|
NOT: {
|
|
referenceId: null,
|
|
},
|
|
},
|
|
});
|
|
|
|
scheduledReminders.forEach((reminder) => {
|
|
if (reminder.referenceId) {
|
|
if (reminder.method === WorkflowMethods.EMAIL) {
|
|
deleteScheduledEmailReminder(reminder.referenceId);
|
|
} else if (reminder.method === WorkflowMethods.SMS) {
|
|
deleteScheduledSMSReminder(reminder.referenceId);
|
|
}
|
|
}
|
|
});
|
|
|
|
await ctx.prisma.workflow.deleteMany({
|
|
where: {
|
|
userId: ctx.user.id,
|
|
id,
|
|
},
|
|
});
|
|
}
|
|
|
|
return {
|
|
id,
|
|
};
|
|
},
|
|
})
|
|
.mutation("update", {
|
|
input: z.object({
|
|
id: z.number(),
|
|
name: z.string(),
|
|
activeOn: z.number().array(),
|
|
steps: z
|
|
.object({
|
|
id: z.number(),
|
|
stepNumber: z.number(),
|
|
action: z.enum(WORKFLOW_ACTIONS),
|
|
workflowId: z.number(),
|
|
sendTo: z.string().optional().nullable(),
|
|
reminderBody: z.string().optional().nullable(),
|
|
emailSubject: z.string().optional().nullable(),
|
|
template: z.enum(WORKFLOW_TEMPLATES),
|
|
})
|
|
.array(),
|
|
trigger: z.enum(WORKFLOW_TRIGGER_EVENTS),
|
|
time: z.number().nullable(),
|
|
timeUnit: z.enum(TIME_UNIT).nullable(),
|
|
}),
|
|
async resolve({ input, ctx }) {
|
|
const { user } = ctx;
|
|
const { id, name, activeOn, steps, trigger, time, timeUnit } = input;
|
|
|
|
const userWorkflow = await ctx.prisma.workflow.findUnique({
|
|
where: {
|
|
id,
|
|
},
|
|
select: {
|
|
userId: true,
|
|
steps: true,
|
|
},
|
|
});
|
|
|
|
if (!userWorkflow || userWorkflow.userId !== user.id) throw new TRPCError({ code: "UNAUTHORIZED" });
|
|
|
|
const oldActiveOnEventTypes = await ctx.prisma.workflowsOnEventTypes.findMany({
|
|
where: {
|
|
workflowId: id,
|
|
},
|
|
select: {
|
|
eventTypeId: true,
|
|
},
|
|
});
|
|
|
|
const newActiveEventTypes = activeOn.filter((eventType) => {
|
|
if (
|
|
!oldActiveOnEventTypes ||
|
|
!oldActiveOnEventTypes
|
|
.map((oldEventType) => {
|
|
return oldEventType.eventTypeId;
|
|
})
|
|
.includes(eventType)
|
|
) {
|
|
return eventType;
|
|
}
|
|
});
|
|
|
|
//check if new event types belong to user
|
|
for (const newEventTypeId of newActiveEventTypes) {
|
|
const newEventType = await ctx.prisma.eventType.findFirst({
|
|
where: {
|
|
id: newEventTypeId,
|
|
},
|
|
include: {
|
|
team: {
|
|
include: {
|
|
members: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (
|
|
newEventType &&
|
|
newEventType.userId !== user.id &&
|
|
newEventType?.team?.members.filter((membership) => {
|
|
membership.userId === user.id;
|
|
}).length
|
|
) {
|
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
|
}
|
|
}
|
|
|
|
//remove all scheduled Email and SMS reminders for eventTypes that are not active any more
|
|
const removedEventTypes = oldActiveOnEventTypes
|
|
.map((eventType) => {
|
|
return eventType.eventTypeId;
|
|
})
|
|
.filter((eventType) => {
|
|
if (!activeOn.includes(eventType)) {
|
|
return eventType;
|
|
}
|
|
});
|
|
|
|
const remindersToDeletePromise: PrismaPromise<
|
|
{
|
|
id: number;
|
|
referenceId: string | null;
|
|
method: string;
|
|
scheduled: boolean;
|
|
}[]
|
|
>[] = [];
|
|
removedEventTypes.forEach((eventTypeId) => {
|
|
const reminderToDelete = ctx.prisma.workflowReminder.findMany({
|
|
where: {
|
|
booking: {
|
|
eventTypeId: eventTypeId,
|
|
userId: ctx.user.id,
|
|
},
|
|
workflowStepId: {
|
|
in: userWorkflow.steps.map((step) => {
|
|
return step.id;
|
|
}),
|
|
},
|
|
},
|
|
select: {
|
|
id: true,
|
|
referenceId: true,
|
|
method: true,
|
|
scheduled: true,
|
|
},
|
|
});
|
|
remindersToDeletePromise.push(reminderToDelete);
|
|
});
|
|
|
|
const remindersToDelete = await Promise.all(remindersToDeletePromise);
|
|
|
|
const deleteReminderPromise: PrismaPromise<Prisma.BatchPayload>[] = [];
|
|
remindersToDelete.flat().forEach((reminder) => {
|
|
//already scheduled reminders
|
|
if (reminder.referenceId) {
|
|
if (reminder.method === WorkflowMethods.EMAIL) {
|
|
deleteScheduledEmailReminder(reminder.referenceId);
|
|
} else if (reminder.method === WorkflowMethods.SMS) {
|
|
deleteScheduledSMSReminder(reminder.referenceId);
|
|
}
|
|
}
|
|
const deleteReminder = ctx.prisma.workflowReminder.deleteMany({
|
|
where: {
|
|
id: reminder.id,
|
|
booking: {
|
|
userId: ctx.user.id,
|
|
},
|
|
},
|
|
});
|
|
deleteReminderPromise.push(deleteReminder);
|
|
});
|
|
|
|
await Promise.all(deleteReminderPromise);
|
|
|
|
//update active on & reminders for new eventTypes
|
|
await ctx.prisma.workflowsOnEventTypes.deleteMany({
|
|
where: {
|
|
workflowId: id,
|
|
},
|
|
});
|
|
|
|
let newEventTypes: number[] = [];
|
|
if (activeOn.length) {
|
|
if (trigger === WorkflowTriggerEvents.BEFORE_EVENT) {
|
|
newEventTypes = newActiveEventTypes;
|
|
}
|
|
if (newEventTypes.length > 0) {
|
|
//create reminders for all bookings with newEventTypes
|
|
const bookingsForReminders = await ctx.prisma.booking.findMany({
|
|
where: {
|
|
eventTypeId: { in: newEventTypes },
|
|
status: BookingStatus.ACCEPTED,
|
|
startTime: {
|
|
gte: new Date(),
|
|
},
|
|
},
|
|
include: {
|
|
attendees: true,
|
|
eventType: true,
|
|
user: true,
|
|
},
|
|
});
|
|
|
|
steps.forEach(async (step) => {
|
|
if (step.action !== WorkflowActions.SMS_ATTENDEE) {
|
|
//as we do not have attendees phone number (user is notified about that when setting this action)
|
|
bookingsForReminders.forEach(async (booking) => {
|
|
const bookingInfo = {
|
|
uid: booking.uid,
|
|
attendees: booking.attendees.map((attendee) => {
|
|
return { name: attendee.name, email: attendee.email, timeZone: attendee.timeZone };
|
|
}),
|
|
organizer: booking.user
|
|
? {
|
|
language: { locale: booking.user.locale || "" },
|
|
name: booking.user.name || "",
|
|
email: booking.user.email,
|
|
timeZone: booking.user.timeZone,
|
|
}
|
|
: { name: "", email: "", timeZone: "", language: { locale: "" } },
|
|
startTime: booking.startTime.toISOString(),
|
|
endTime: booking.endTime.toISOString(),
|
|
title: booking.title,
|
|
language: { locale: booking?.user?.locale || "" },
|
|
};
|
|
if (
|
|
step.action === WorkflowActions.EMAIL_HOST ||
|
|
step.action === WorkflowActions.EMAIL_ATTENDEE
|
|
) {
|
|
const sendTo =
|
|
step.action === WorkflowActions.EMAIL_HOST
|
|
? bookingInfo.organizer?.email
|
|
: bookingInfo.attendees[0].email;
|
|
await scheduleEmailReminder(
|
|
bookingInfo,
|
|
WorkflowTriggerEvents.BEFORE_EVENT,
|
|
step.action,
|
|
{
|
|
time,
|
|
timeUnit,
|
|
},
|
|
sendTo,
|
|
step.emailSubject || "",
|
|
step.reminderBody || "",
|
|
step.id,
|
|
step.template
|
|
);
|
|
} else if (step.action === WorkflowActions.SMS_NUMBER) {
|
|
await scheduleSMSReminder(
|
|
bookingInfo,
|
|
step.sendTo || "",
|
|
WorkflowTriggerEvents.BEFORE_EVENT,
|
|
step.action,
|
|
{
|
|
time,
|
|
timeUnit,
|
|
},
|
|
step.reminderBody || "",
|
|
step.id,
|
|
step.template
|
|
);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
//create all workflow - eventtypes relationships
|
|
activeOn.forEach(async (eventTypeId) => {
|
|
await ctx.prisma.workflowsOnEventTypes.createMany({
|
|
data: {
|
|
workflowId: id,
|
|
eventTypeId,
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
userWorkflow.steps.map(async (oldStep) => {
|
|
const newStep = steps.filter((s) => s.id === oldStep.id)[0];
|
|
const remindersFromStep = await ctx.prisma.workflowReminder.findMany({
|
|
where: {
|
|
workflowStepId: oldStep.id,
|
|
},
|
|
include: {
|
|
booking: true,
|
|
},
|
|
});
|
|
//step was deleted
|
|
if (!newStep) {
|
|
//delete already scheduled reminders
|
|
if (remindersFromStep.length > 0) {
|
|
remindersFromStep.forEach((reminder) => {
|
|
if (reminder.referenceId) {
|
|
if (reminder.method === WorkflowMethods.EMAIL) {
|
|
deleteScheduledEmailReminder(reminder.referenceId);
|
|
} else if (reminder.method === WorkflowMethods.SMS) {
|
|
deleteScheduledSMSReminder(reminder.referenceId);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
await ctx.prisma.workflowStep.delete({
|
|
where: {
|
|
id: oldStep.id,
|
|
},
|
|
});
|
|
//step was edited
|
|
} else if (JSON.stringify(oldStep) !== JSON.stringify(newStep)) {
|
|
await ctx.prisma.workflowStep.update({
|
|
where: {
|
|
id: oldStep.id,
|
|
},
|
|
data: {
|
|
action: newStep.action,
|
|
sendTo: newStep.action === WorkflowActions.SMS_NUMBER ? newStep.sendTo : null,
|
|
stepNumber: newStep.stepNumber,
|
|
workflowId: newStep.workflowId,
|
|
reminderBody: newStep.template === WorkflowTemplates.CUSTOM ? newStep.reminderBody : null,
|
|
emailSubject: newStep.template === WorkflowTemplates.CUSTOM ? newStep.emailSubject : null,
|
|
template: newStep.template,
|
|
},
|
|
});
|
|
//cancel all reminders of step and create new ones (not for newEventTypes)
|
|
const remindersToUpdate = remindersFromStep.filter((reminder) => {
|
|
if (reminder.booking?.eventTypeId && !newEventTypes.includes(reminder.booking?.eventTypeId)) {
|
|
return reminder;
|
|
}
|
|
});
|
|
remindersToUpdate.forEach(async (reminder) => {
|
|
if (reminder.referenceId) {
|
|
if (reminder.method === WorkflowMethods.EMAIL) {
|
|
deleteScheduledEmailReminder(reminder.referenceId);
|
|
} else if (reminder.method === WorkflowMethods.SMS) {
|
|
deleteScheduledSMSReminder(reminder.referenceId);
|
|
}
|
|
}
|
|
await ctx.prisma.workflowReminder.deleteMany({
|
|
where: {
|
|
id: reminder.id,
|
|
},
|
|
});
|
|
});
|
|
const eventTypesToUpdateReminders = activeOn.filter((eventTypeId) => {
|
|
if (!newEventTypes.includes(eventTypeId)) {
|
|
return eventTypeId;
|
|
}
|
|
});
|
|
if (eventTypesToUpdateReminders && trigger === WorkflowTriggerEvents.BEFORE_EVENT) {
|
|
const bookingsOfEventTypes = await ctx.prisma.booking.findMany({
|
|
where: {
|
|
eventTypeId: {
|
|
in: eventTypesToUpdateReminders,
|
|
},
|
|
status: BookingStatus.ACCEPTED,
|
|
startTime: {
|
|
gte: new Date(),
|
|
},
|
|
},
|
|
include: {
|
|
attendees: true,
|
|
eventType: true,
|
|
user: true,
|
|
},
|
|
});
|
|
bookingsOfEventTypes.forEach(async (booking) => {
|
|
const bookingInfo = {
|
|
uid: booking.uid,
|
|
attendees: booking.attendees.map((attendee) => {
|
|
return { name: attendee.name, email: attendee.email, timeZone: attendee.timeZone };
|
|
}),
|
|
organizer: booking.user
|
|
? {
|
|
language: { locale: booking.user.locale || "" },
|
|
name: booking.user.name || "",
|
|
email: booking.user.email,
|
|
timeZone: booking.user.timeZone,
|
|
}
|
|
: { name: "", email: "", timeZone: "", language: { locale: "" } },
|
|
startTime: booking.startTime.toISOString(),
|
|
endTime: booking.endTime.toISOString(),
|
|
title: booking.title,
|
|
language: { locale: booking?.user?.locale || "" },
|
|
};
|
|
if (
|
|
newStep.action === WorkflowActions.EMAIL_HOST ||
|
|
newStep.action === WorkflowActions.EMAIL_ATTENDEE
|
|
) {
|
|
const sendTo =
|
|
newStep.action === WorkflowActions.EMAIL_HOST
|
|
? bookingInfo.organizer?.email
|
|
: bookingInfo.attendees[0].email;
|
|
await scheduleEmailReminder(
|
|
bookingInfo,
|
|
WorkflowTriggerEvents.BEFORE_EVENT,
|
|
newStep.action,
|
|
{
|
|
time,
|
|
timeUnit,
|
|
},
|
|
sendTo,
|
|
newStep.emailSubject || "",
|
|
newStep.reminderBody || "",
|
|
newStep.id,
|
|
newStep.template
|
|
);
|
|
} else if (newStep.action === WorkflowActions.SMS_NUMBER) {
|
|
await scheduleSMSReminder(
|
|
bookingInfo,
|
|
newStep.sendTo || "",
|
|
WorkflowTriggerEvents.BEFORE_EVENT,
|
|
newStep.action,
|
|
{
|
|
time,
|
|
timeUnit,
|
|
},
|
|
newStep.reminderBody || "",
|
|
newStep.id || 0,
|
|
newStep.template
|
|
);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
//added steps
|
|
const addedSteps = steps.map((s) => {
|
|
if (s.id <= 0) {
|
|
const { id, ...stepToAdd } = s;
|
|
if (stepToAdd) {
|
|
return stepToAdd;
|
|
}
|
|
}
|
|
});
|
|
|
|
if (addedSteps) {
|
|
const eventTypesToCreateReminders = activeOn.map((activeEventType) => {
|
|
if (activeEventType && !newEventTypes.includes(activeEventType)) {
|
|
return activeEventType;
|
|
}
|
|
});
|
|
addedSteps.forEach(async (step) => {
|
|
if (step) {
|
|
const createdStep = await ctx.prisma.workflowStep.create({
|
|
data: step,
|
|
});
|
|
if (
|
|
trigger === WorkflowTriggerEvents.BEFORE_EVENT &&
|
|
eventTypesToCreateReminders &&
|
|
step.action !== WorkflowActions.SMS_ATTENDEE
|
|
) {
|
|
const bookingsForReminders = await ctx.prisma.booking.findMany({
|
|
where: {
|
|
eventTypeId: { in: eventTypesToCreateReminders as number[] },
|
|
status: BookingStatus.ACCEPTED,
|
|
startTime: {
|
|
gte: new Date(),
|
|
},
|
|
},
|
|
include: {
|
|
attendees: true,
|
|
eventType: true,
|
|
user: true,
|
|
},
|
|
});
|
|
bookingsForReminders.forEach(async (booking) => {
|
|
const bookingInfo = {
|
|
uid: booking.uid,
|
|
attendees: booking.attendees.map((attendee) => {
|
|
return { name: attendee.name, email: attendee.email, timeZone: attendee.timeZone };
|
|
}),
|
|
organizer: booking.user
|
|
? {
|
|
name: booking.user.name || "",
|
|
email: booking.user.email,
|
|
timeZone: booking.user.timeZone,
|
|
language: { locale: booking.user.locale || "" },
|
|
}
|
|
: { name: "", email: "", timeZone: "", language: { locale: "" } },
|
|
startTime: booking.startTime.toISOString(),
|
|
endTime: booking.endTime.toISOString(),
|
|
title: booking.title,
|
|
language: { locale: booking?.user?.locale || "" },
|
|
};
|
|
|
|
if (
|
|
step.action === WorkflowActions.EMAIL_ATTENDEE ||
|
|
step.action === WorkflowActions.EMAIL_HOST
|
|
) {
|
|
const sendTo =
|
|
step.action === WorkflowActions.EMAIL_HOST
|
|
? bookingInfo.organizer?.email
|
|
: bookingInfo.attendees[0].email;
|
|
await scheduleEmailReminder(
|
|
bookingInfo,
|
|
trigger,
|
|
step.action,
|
|
{
|
|
time,
|
|
timeUnit,
|
|
},
|
|
sendTo,
|
|
step.emailSubject || "",
|
|
step.reminderBody || "",
|
|
createdStep.id,
|
|
step.template
|
|
);
|
|
} else if (step.action === WorkflowActions.SMS_NUMBER && step.sendTo) {
|
|
await scheduleSMSReminder(
|
|
bookingInfo,
|
|
step.sendTo,
|
|
WorkflowTriggerEvents.BEFORE_EVENT,
|
|
step.action,
|
|
{
|
|
time,
|
|
timeUnit,
|
|
},
|
|
step.reminderBody || "",
|
|
createdStep.id,
|
|
step.template
|
|
);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
//update trigger, name, time, timeUnit
|
|
await ctx.prisma.workflow.update({
|
|
where: {
|
|
id,
|
|
},
|
|
data: {
|
|
name,
|
|
trigger,
|
|
time,
|
|
timeUnit,
|
|
},
|
|
});
|
|
|
|
const workflow = await ctx.prisma.workflow.findFirst({
|
|
where: {
|
|
id,
|
|
},
|
|
include: {
|
|
activeOn: {
|
|
select: {
|
|
eventType: true,
|
|
},
|
|
},
|
|
steps: true,
|
|
},
|
|
});
|
|
|
|
return {
|
|
workflow,
|
|
};
|
|
},
|
|
})
|
|
.mutation("testAction", {
|
|
input: z.object({
|
|
action: z.enum(WORKFLOW_ACTIONS),
|
|
emailSubject: z.string(),
|
|
reminderBody: z.string(),
|
|
template: z.enum(WORKFLOW_TEMPLATES),
|
|
sendTo: z.string().optional(),
|
|
}),
|
|
async resolve({ ctx, input }) {
|
|
const { action, emailSubject, reminderBody, template, sendTo } = input;
|
|
try {
|
|
const booking = await ctx.prisma.booking.findFirst({
|
|
orderBy: {
|
|
createdAt: "desc",
|
|
},
|
|
where: {
|
|
userId: ctx.user.id,
|
|
},
|
|
include: {
|
|
attendees: true,
|
|
user: true,
|
|
},
|
|
});
|
|
|
|
let evt: BookingInfo;
|
|
if (booking) {
|
|
evt = {
|
|
uid: booking?.uid,
|
|
attendees:
|
|
booking?.attendees.map((attendee) => {
|
|
return { name: attendee.name, email: attendee.email, timeZone: attendee.timeZone };
|
|
}) || [],
|
|
organizer: {
|
|
language: {
|
|
locale: booking?.user?.locale || "",
|
|
},
|
|
name: booking?.user?.name || "",
|
|
email: booking?.user?.email || "",
|
|
timeZone: booking?.user?.timeZone || "",
|
|
},
|
|
startTime: booking?.startTime.toISOString() || "",
|
|
endTime: booking?.endTime.toISOString() || "",
|
|
title: booking?.title || "",
|
|
location: booking?.location || null,
|
|
additionalNotes: booking?.description || null,
|
|
customInputs: booking?.customInputs,
|
|
};
|
|
} else {
|
|
//if no booking exists create an example booking
|
|
evt = {
|
|
attendees: [{ name: "John Doe", email: "john.doe@example.com", timeZone: "Europe/London" }],
|
|
organizer: {
|
|
language: {
|
|
locale: ctx.user.locale,
|
|
},
|
|
name: ctx.user.name || "",
|
|
email: ctx.user.email,
|
|
timeZone: ctx.user.timeZone,
|
|
},
|
|
startTime: dayjs().add(10, "hour").toISOString(),
|
|
endTime: dayjs().add(11, "hour").toISOString(),
|
|
title: "Example Booking",
|
|
location: "Office",
|
|
additionalNotes: "These are additional notes",
|
|
};
|
|
}
|
|
|
|
if (action === WorkflowActions.EMAIL_ATTENDEE || action === WorkflowActions.EMAIL_HOST) {
|
|
scheduleEmailReminder(
|
|
evt,
|
|
WorkflowTriggerEvents.NEW_EVENT,
|
|
action,
|
|
{ time: null, timeUnit: null },
|
|
ctx.user.email,
|
|
emailSubject,
|
|
reminderBody,
|
|
0,
|
|
template
|
|
);
|
|
return { message: "Notification sent" };
|
|
} else if (action === WorkflowActions.SMS_NUMBER && sendTo) {
|
|
scheduleSMSReminder(
|
|
evt,
|
|
sendTo,
|
|
WorkflowTriggerEvents.NEW_EVENT,
|
|
action,
|
|
{ time: null, timeUnit: null },
|
|
reminderBody,
|
|
0,
|
|
template
|
|
);
|
|
return { message: "Notification sent" };
|
|
}
|
|
return {
|
|
ok: false,
|
|
status: 500,
|
|
message: "Notification could not be sent",
|
|
};
|
|
} catch (_err) {
|
|
const error = getErrorFromUnknown(_err);
|
|
return {
|
|
ok: false,
|
|
status: 500,
|
|
message: error.message,
|
|
};
|
|
}
|
|
},
|
|
})
|
|
.mutation("activateEventType", {
|
|
input: z.object({
|
|
eventTypeId: z.number(),
|
|
workflowId: z.number(),
|
|
}),
|
|
async resolve({ ctx, input }) {
|
|
const { eventTypeId, workflowId } = input;
|
|
|
|
const eventType = await ctx.prisma.eventType.findFirst({
|
|
where: {
|
|
id: eventTypeId,
|
|
},
|
|
});
|
|
|
|
//check if event type is already active
|
|
|
|
const isActive = await ctx.prisma.workflowsOnEventTypes.findFirst({
|
|
where: {
|
|
workflowId,
|
|
eventTypeId,
|
|
},
|
|
});
|
|
|
|
if (isActive) {
|
|
await ctx.prisma.workflowsOnEventTypes.deleteMany({
|
|
where: {
|
|
workflowId,
|
|
eventTypeId,
|
|
},
|
|
});
|
|
} else {
|
|
await ctx.prisma.workflowsOnEventTypes.create({
|
|
data: {
|
|
workflowId,
|
|
eventTypeId,
|
|
},
|
|
});
|
|
}
|
|
},
|
|
});
|