Resolved conflicts; included reference creation into EventManager; use EventManager everywhere
This commit is contained in:
commit
86e2add30f
19 files changed
+843
-308
No files matched your search
+2
-10
@@ -574,7 +574,7 @@ const updateEvent = async (
|
||||
credential: Credential,
|
||||
uidToUpdate: string,
|
||||
calEvent: CalendarEvent,
|
||||
noMail: false
|
||||
noMail = false
|
||||
): Promise<EventResult> => {
|
||||
const parser: CalEventParser = new CalEventParser(calEvent);
|
||||
const newUid: string = parser.getUid();
|
||||
@@ -626,12 +626,4 @@ const deleteEvent = (credential: Credential, uid: string): Promise<unknown> => {
|
||||
return Promise.resolve({});
|
||||
};
|
||||
|
||||
export {
|
||||
getBusyCalendarTimes,
|
||||
createEvent,
|
||||
updateEvent,
|
||||
deleteEvent,
|
||||
CalendarEvent,
|
||||
listCalendars,
|
||||
IntegrationCalendar,
|
||||
};
|
||||
export { getBusyCalendarTimes, createEvent, updateEvent, deleteEvent, listCalendars };
|
||||
@@ -46,6 +46,31 @@ export default class EventOrganizerMail extends EventMail {
|
||||
return icsEvent.value;
|
||||
}
|
||||
|
||||
protected getBodyHeader(): string {
|
||||
return "A new event has been scheduled.";
|
||||
}
|
||||
|
||||
protected getBodyText(): string {
|
||||
return "You and any other attendees have been emailed with this information.";
|
||||
}
|
||||
|
||||
protected getImage(): string {
|
||||
return `<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style="height: 60px; width: 60px; color: #31c48d"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the email text as HTML representation.
|
||||
*
|
||||
@@ -67,22 +92,9 @@ export default class EventOrganizerMail extends EventMail {
|
||||
margin-top: 40px;
|
||||
"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style="height: 60px; width: 60px; color: #31c48d"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<h1 style="font-weight: 500; color: #161e2e;">A new event has been scheduled.</h1>
|
||||
<p style="color: #4b5563; margin-bottom: 30px;">You and any other attendees have been emailed with this information.</p>
|
||||
${this.getImage()}
|
||||
<h1 style="font-weight: 500; color: #161e2e;">${this.getBodyHeader()}</h1>
|
||||
<p style="color: #4b5563; margin-bottom: 30px;">${this.getBodyText()}</p>
|
||||
<hr />
|
||||
<table style="border-spacing: 20px; color: #161e2e; margin-bottom: 10px;">
|
||||
<colgroup>
|
||||
@@ -165,8 +177,6 @@ export default class EventOrganizerMail extends EventMail {
|
||||
* @protected
|
||||
*/
|
||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
||||
const organizerStart: Dayjs = <Dayjs>dayjs(this.calEvent.startTime).tz(this.calEvent.organizer.timeZone);
|
||||
|
||||
return {
|
||||
icalEvent: {
|
||||
filename: "event.ics",
|
||||
@@ -174,14 +184,19 @@ export default class EventOrganizerMail extends EventMail {
|
||||
},
|
||||
from: `Calendso <${this.getMailerOptions().from}>`,
|
||||
to: this.calEvent.organizer.email,
|
||||
subject: `New event: ${this.calEvent.attendees[0].name} - ${organizerStart.format("LT dddd, LL")} - ${
|
||||
this.calEvent.type
|
||||
}`,
|
||||
subject: this.getSubject(),
|
||||
html: this.getHtmlRepresentation(),
|
||||
text: this.getPlainTextRepresentation(),
|
||||
};
|
||||
}
|
||||
|
||||
protected getSubject(): string {
|
||||
const organizerStart: Dayjs = <Dayjs>dayjs(this.calEvent.startTime).tz(this.calEvent.organizer.timeZone);
|
||||
return `New event: ${this.calEvent.attendees[0].name} - ${organizerStart.format("LT dddd, LL")} - ${
|
||||
this.calEvent.type
|
||||
}`;
|
||||
}
|
||||
|
||||
protected printNodeMailerError(error: string): void {
|
||||
console.error("SEND_NEW_EVENT_NOTIFICATION_ERROR", this.calEvent.organizer.email, error);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import toArray from "dayjs/plugin/toArray";
|
||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||
import EventOrganizerMail from "@lib/emails/EventOrganizerMail";
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(toArray);
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
export default class EventOrganizerRequestMail extends EventOrganizerMail {
|
||||
protected getBodyHeader(): string {
|
||||
return "A new event is waiting for your approval.";
|
||||
}
|
||||
|
||||
protected getBodyText(): string {
|
||||
return "Check your bookings page to confirm or reject the booking.";
|
||||
}
|
||||
|
||||
protected getAdditionalBody(): string {
|
||||
return `<a href="${process.env.BASE_URL}/bookings">Confirm or reject the booking</a>`;
|
||||
}
|
||||
|
||||
protected getImage(): string {
|
||||
return `<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style="height: 60px; width: 60px; color: #01579b"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
protected getSubject(): string {
|
||||
const organizerStart: Dayjs = <Dayjs>dayjs(this.calEvent.startTime).tz(this.calEvent.organizer.timeZone);
|
||||
return `New event request: ${this.calEvent.attendees[0].name} - ${organizerStart.format(
|
||||
"LT dddd, LL"
|
||||
)} - ${this.calEvent.type}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import toArray from "dayjs/plugin/toArray";
|
||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||
import EventOrganizerRequestMail from "@lib/emails/EventOrganizerRequestMail";
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(toArray);
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
export default class EventOrganizerRequestReminderMail extends EventOrganizerRequestMail {
|
||||
protected getBodyHeader(): string {
|
||||
return "An event is still waiting for your approval.";
|
||||
}
|
||||
|
||||
protected getSubject(): string {
|
||||
const organizerStart: Dayjs = <Dayjs>dayjs(this.calEvent.startTime).tz(this.calEvent.organizer.timeZone);
|
||||
return `Event request is still waiting: ${this.calEvent.attendees[0].name} - ${organizerStart.format(
|
||||
"LT dddd, LL"
|
||||
)} - ${this.calEvent.type}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
import EventMail from "./EventMail";
|
||||
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
export default class EventRejectionMail extends EventMail {
|
||||
/**
|
||||
* Returns the email text as HTML representation.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected getHtmlRepresentation(): string {
|
||||
return (
|
||||
`
|
||||
<body style="background: #f4f5f7; font-family: Helvetica, sans-serif">
|
||||
<div
|
||||
style="
|
||||
margin: 0 auto;
|
||||
max-width: 450px;
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 2rem 2rem 2rem 2rem;
|
||||
text-align: center;
|
||||
margin-top: 40px;
|
||||
"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style="height: 60px; width: 60px; color: #ba2525"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<h1 style="font-weight: 500; color: #161e2e;">Your meeting request has been rejected</h1>
|
||||
<p style="color: #4b5563; margin-bottom: 30px;">You and any other attendees have been emailed with this information.</p>
|
||||
<hr />
|
||||
` +
|
||||
`
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 20px; color: #ccc; font-size: 12px;">
|
||||
<img style="opacity: 0.25; width: 120px;" src="https://app.calendso.com/calendso-logo-word.svg" alt="Calendso Logo"></div>
|
||||
</body>
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the payload object for the nodemailer.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
||||
return {
|
||||
to: `${this.calEvent.attendees[0].name} <${this.calEvent.attendees[0].email}>`,
|
||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||
replyTo: this.calEvent.organizer.email,
|
||||
subject: `Rejected: ${this.calEvent.type} with ${
|
||||
this.calEvent.organizer.name
|
||||
} on ${this.getInviteeStart().format("dddd, LL")}`,
|
||||
html: this.getHtmlRepresentation(),
|
||||
text: this.getPlainTextRepresentation(),
|
||||
};
|
||||
}
|
||||
|
||||
protected printNodeMailerError(error: string): void {
|
||||
console.error("SEND_BOOKING_CONFIRMATION_ERROR", this.calEvent.attendees[0].email, error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the inviteeStart value used at multiple points.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
protected getInviteeStart(): Dayjs {
|
||||
return <Dayjs>dayjs(this.calEvent.startTime).tz(this.calEvent.attendees[0].timeZone);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { CalendarEvent, createEvent, updateEvent } from "@lib/calendarClient";
|
||||
import { Credential } from "@prisma/client";
|
||||
import async from "async";
|
||||
import { createMeeting, updateMeeting } from "@lib/videoClient";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
export interface EventResult {
|
||||
type: string;
|
||||
@@ -12,13 +13,18 @@ export interface EventResult {
|
||||
originalEvent: CalendarEvent;
|
||||
}
|
||||
|
||||
export interface CreateUpdateResult {
|
||||
results: Array<EventResult>;
|
||||
referencesToCreate: Array<PartialReference>;
|
||||
}
|
||||
|
||||
export interface PartialBooking {
|
||||
id: number;
|
||||
references: Array<PartialReference>;
|
||||
}
|
||||
|
||||
export interface PartialReference {
|
||||
id: number;
|
||||
id?: number;
|
||||
type: string;
|
||||
uid: string;
|
||||
}
|
||||
@@ -32,7 +38,7 @@ export default class EventManager {
|
||||
this.videoCredentials = credentials.filter((cred) => cred.type.endsWith("_video"));
|
||||
}
|
||||
|
||||
public async create(event: CalendarEvent): Promise<Array<EventResult>> {
|
||||
public async create(event: CalendarEvent): Promise<CreateUpdateResult> {
|
||||
const isVideo = EventManager.isIntegration(event.location);
|
||||
|
||||
// First, create all calendar events. If this is a video event, don't send a mail right here.
|
||||
@@ -43,10 +49,37 @@ export default class EventManager {
|
||||
results.push(await this.createVideoEvent(event));
|
||||
}
|
||||
|
||||
return results;
|
||||
const referencesToCreate: Array<PartialReference> = results.map((result) => {
|
||||
return {
|
||||
type: result.type,
|
||||
uid: result.createdEvent.id.toString(),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
results,
|
||||
referencesToCreate,
|
||||
};
|
||||
}
|
||||
|
||||
public async update(event: CalendarEvent, booking: PartialBooking): Promise<Array<EventResult>> {
|
||||
public async update(event: CalendarEvent, rescheduleUid: string): Promise<CreateUpdateResult> {
|
||||
// Get details of existing booking.
|
||||
const booking = await prisma.booking.findFirst({
|
||||
where: {
|
||||
uid: rescheduleUid,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
references: {
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
uid: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const isVideo = EventManager.isIntegration(event.location);
|
||||
|
||||
// First, update all calendar events. If this is a video event, don't send a mail right here.
|
||||
@@ -57,7 +90,30 @@ export default class EventManager {
|
||||
results.push(await this.updateVideoEvent(event, booking));
|
||||
}
|
||||
|
||||
return results;
|
||||
// Now we can delete the old booking and its references.
|
||||
const bookingReferenceDeletes = prisma.bookingReference.deleteMany({
|
||||
where: {
|
||||
bookingId: booking.id,
|
||||
},
|
||||
});
|
||||
const attendeeDeletes = prisma.attendee.deleteMany({
|
||||
where: {
|
||||
bookingId: booking.id,
|
||||
},
|
||||
});
|
||||
const bookingDeletes = prisma.booking.delete({
|
||||
where: {
|
||||
uid: rescheduleUid,
|
||||
},
|
||||
});
|
||||
|
||||
// Wait for all deletions to be applied.
|
||||
await Promise.all([bookingReferenceDeletes, attendeeDeletes, bookingDeletes]);
|
||||
|
||||
return {
|
||||
results,
|
||||
referencesToCreate: [...booking.references],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user