From 8020576b06126f7f083a4ebb787a076f56624047 Mon Sep 17 00:00:00 2001 From: femyeda Date: Mon, 28 Jun 2021 11:01:07 -0500 Subject: [PATCH 01/33] update message for not available --- components/booking/AvailableTimes.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/booking/AvailableTimes.tsx b/components/booking/AvailableTimes.tsx index 6f4e413033..188d901982 100644 --- a/components/booking/AvailableTimes.tsx +++ b/components/booking/AvailableTimes.tsx @@ -102,7 +102,7 @@ const AvailableTimes = (props) => { ))} {!error && loaded && times.length == 0 && (
-

{props.user.name} is all booked today.

+

{props.user.name} isn't available at this time.

)} {!error && !loaded &&
} From e714bd5b8e58dd1bc2b3788070783ea9848c7b28 Mon Sep 17 00:00:00 2001 From: femyeda Date: Mon, 28 Jun 2021 15:34:27 -0500 Subject: [PATCH 02/33] use google events conference data version 1 - fix issue where google meet details were no longer add to the created event --- lib/calendarClient.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/calendarClient.ts b/lib/calendarClient.ts index 4d8d7421b6..444532197e 100644 --- a/lib/calendarClient.ts +++ b/lib/calendarClient.ts @@ -373,6 +373,7 @@ const GoogleCalendar = (credential): CalendarApiAdapter => { auth: myGoogleAuth, calendarId: "primary", resource: payload, + conferenceDataVersion: 1, }, function (err, event) { if (err) { From 726d211f27d56ed0b55ff7af21d88c2298229dc5 Mon Sep 17 00:00:00 2001 From: femyeda Date: Mon, 28 Jun 2021 19:30:45 -0500 Subject: [PATCH 03/33] attach hangouts location to invitee and organizer email notification --- lib/calendarClient.ts | 36 ++++++----- lib/emails/EventAttendeeMail.ts | 72 ++++++++++++++++----- lib/emails/EventMail.ts | 61 +++++++++++------- lib/emails/EventOrganizerMail.ts | 104 ++++++++++++++++++++++--------- 4 files changed, 191 insertions(+), 82 deletions(-) diff --git a/lib/calendarClient.ts b/lib/calendarClient.ts index 444532197e..d075269971 100644 --- a/lib/calendarClient.ts +++ b/lib/calendarClient.ts @@ -111,7 +111,7 @@ interface Person { timeZone: string; } -interface CalendarEvent { +export interface CalendarEvent { type: string; title: string; startTime: string; @@ -123,18 +123,18 @@ interface CalendarEvent { conferenceData?: ConferenceData; } -interface ConferenceData { +export interface ConferenceData { createRequest: any; } -interface IntegrationCalendar { +export interface IntegrationCalendar { integration: string; primary: boolean; externalId: string; name: string; } -interface CalendarApiAdapter { +export interface CalendarApiAdapter { createEvent(event: CalendarEvent): Promise; updateEvent(uid: string, event: CalendarEvent); @@ -512,8 +512,22 @@ const createEvent = async (credential, calEvent: CalendarEvent): Promise => const creationResult = credential ? await calendars([credential])[0].createEvent(calEvent) : null; - const organizerMail = new EventOrganizerMail(calEvent, uid); - const attendeeMail = new EventAttendeeMail(calEvent, uid); + const maybeHangoutLink = creationResult?.hangoutLink; + const maybeEntryPoints = creationResult?.entryPoints; + const maybeConferenceData = creationResult?.conferenceData; + + const organizerMail = new EventOrganizerMail(calEvent, uid, { + hangoutLink: maybeHangoutLink, + conferenceData: maybeConferenceData, + entryPoints: maybeEntryPoints, + }); + + const attendeeMail = new EventAttendeeMail(calEvent, uid, { + hangoutLink: maybeHangoutLink, + conferenceData: maybeConferenceData, + entryPoints: maybeEntryPoints, + }); + try { await organizerMail.sendEmail(); } catch (e) { @@ -571,12 +585,4 @@ const deleteEvent = (credential, uid: string): Promise => { return Promise.resolve({}); }; -export { - getBusyCalendarTimes, - createEvent, - updateEvent, - deleteEvent, - CalendarEvent, - listCalendars, - IntegrationCalendar, -}; +export { getBusyCalendarTimes, createEvent, updateEvent, deleteEvent, listCalendars }; diff --git a/lib/emails/EventAttendeeMail.ts b/lib/emails/EventAttendeeMail.ts index 55f231f939..4a1b3d10c5 100644 --- a/lib/emails/EventAttendeeMail.ts +++ b/lib/emails/EventAttendeeMail.ts @@ -1,9 +1,9 @@ -import dayjs, {Dayjs} from "dayjs"; +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'; +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); @@ -15,20 +15,60 @@ export default class EventAttendeeMail extends EventMail { * @protected */ protected getHtmlRepresentation(): string { - return ` + return ( + `
Hi ${this.calEvent.attendees[0].name},

- Your ${this.calEvent.type} with ${this.calEvent.organizer.name} at ${this.getInviteeStart().format('h:mma')} - (${this.calEvent.attendees[0].timeZone}) on ${this.getInviteeStart().format('dddd, LL')} is scheduled.
-
` + this.getAdditionalBody() + ( - this.calEvent.location ? `Location: ${this.calEvent.location}

` : '' - ) + + Your ${this.calEvent.type} with ${this.calEvent.organizer.name} at ${this.getInviteeStart().format( + "h:mma" + )} + (${this.calEvent.attendees[0].timeZone}) on ${this.getInviteeStart().format( + "dddd, LL" + )} is scheduled.
+
` + + this.getAdditionalBody() + + "
" + `Additional notes:
${this.calEvent.description}
- ` + this.getAdditionalFooter() + ` + ` + + this.getAdditionalFooter() + + `
- `; + ` + ); + } + + /** + * Adds the video call information to the mail body. + * + * @protected + */ + protected getLocation(): string { + if (this.additionInformation?.hangoutLink) { + return `Location: ${this.additionInformation?.hangoutLink}
`; + } + + if (this.additionInformation?.entryPoints && this.additionInformation?.entryPoints.length > 0) { + const locations = this.additionInformation?.entryPoints + .map((entryPoint) => { + return ` + Join by ${entryPoint.entryPointType}:
+ ${entryPoint.label}
+ `; + }) + .join("
"); + + return `Locations:
${locations}`; + } + + return this.calEvent.location ? `Location: ${this.calEvent.location}

` : ""; + } + + protected getAdditionalBody(): string { + return ` + ${this.getLocation()} + `; } /** @@ -36,12 +76,14 @@ export default class EventAttendeeMail extends EventMail { * * @protected */ - protected getNodeMailerPayload(): Object { + protected getNodeMailerPayload() { 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: `Confirmed: ${this.calEvent.type} with ${this.calEvent.organizer.name} on ${this.getInviteeStart().format('dddd, LL')}`, + subject: `Confirmed: ${this.calEvent.type} with ${ + this.calEvent.organizer.name + } on ${this.getInviteeStart().format("dddd, LL")}`, html: this.getHtmlRepresentation(), text: this.getPlainTextRepresentation(), }; @@ -59,4 +101,4 @@ export default class EventAttendeeMail extends EventMail { protected getInviteeStart(): Dayjs { return dayjs(this.calEvent.startTime).tz(this.calEvent.attendees[0].timeZone); } -} \ No newline at end of file +} diff --git a/lib/emails/EventMail.ts b/lib/emails/EventMail.ts index de1c050732..f7194d1268 100644 --- a/lib/emails/EventMail.ts +++ b/lib/emails/EventMail.ts @@ -1,10 +1,28 @@ -import {CalendarEvent} from "../calendarClient"; -import {serverConfig} from "../serverConfig"; -import nodemailer from 'nodemailer'; +import { CalendarEvent, ConferenceData } from "../calendarClient"; +import { serverConfig } from "../serverConfig"; +import nodemailer from "nodemailer"; + +interface EntryPoint { + entryPointType?: string; + uri?: string; + label?: string; + pin?: string; + accessCode?: string; + meetingCode?: string; + passcode?: string; + password?: string; +} + +interface AdditionInformation { + conferenceData?: ConferenceData; + entryPoints?: EntryPoint[]; + hangoutLink?: string; +} export default abstract class EventMail { calEvent: CalendarEvent; uid: string; + additionInformation?: AdditionInformation; /** * An EventMail always consists of a CalendarEvent @@ -14,9 +32,10 @@ export default abstract class EventMail { * @param calEvent * @param uid */ - constructor(calEvent: CalendarEvent, uid: string) { + constructor(calEvent: CalendarEvent, uid: string, additionInformation: AdditionInformation = null) { this.calEvent = calEvent; this.uid = uid; + this.additionInformation = additionInformation; } /** @@ -43,31 +62,30 @@ export default abstract class EventMail { * @protected */ protected stripHtml(html: string): string { - return html - .replace('
', "\n") - .replace(/<[^>]+>/g, ''); + return html.replace("
", "\n").replace(/<[^>]+>/g, ""); } /** * Returns the payload object for the nodemailer. * @protected */ - protected abstract getNodeMailerPayload(): Object; + protected abstract getNodeMailerPayload(); /** * Sends the email to the event attendant and returns a Promise. */ public sendEmail(): Promise { - new Promise((resolve, reject) => nodemailer.createTransport(this.getMailerOptions().transport).sendMail( - this.getNodeMailerPayload(), - (error, info) => { - if (error) { - this.printNodeMailerError(error); - reject(new Error(error)); - } else { - resolve(info); - } - }) + new Promise((resolve, reject) => + nodemailer + .createTransport(this.getMailerOptions().transport) + .sendMail(this.getNodeMailerPayload(), (error, info) => { + if (error) { + this.printNodeMailerError(error); + reject(new Error(error)); + } else { + resolve(info); + } + }) ).catch((e) => console.error("sendEmail", e)); return new Promise((resolve) => resolve("send mail async")); } @@ -95,6 +113,8 @@ export default abstract class EventMail { return ""; } + protected abstract getLocation(): string; + /** * Prints out the desired information when an error * occured while sending the mail. @@ -109,7 +129,7 @@ export default abstract class EventMail { * @protected */ protected getRescheduleLink(): string { - return process.env.BASE_URL + '/reschedule/' + this.uid; + return process.env.BASE_URL + "/reschedule/" + this.uid; } /** @@ -118,10 +138,9 @@ export default abstract class EventMail { * @protected */ protected getCancelLink(): string { - return process.env.BASE_URL + '/cancel/' + this.uid; + return process.env.BASE_URL + "/cancel/" + this.uid; } - /** * Defines a footer that will be appended to the email. * @protected diff --git a/lib/emails/EventOrganizerMail.ts b/lib/emails/EventOrganizerMail.ts index 6d3060b6ca..cc429ed16b 100644 --- a/lib/emails/EventOrganizerMail.ts +++ b/lib/emails/EventOrganizerMail.ts @@ -1,11 +1,11 @@ -import {createEvent} from "ics"; -import dayjs, {Dayjs} from "dayjs"; +import { createEvent } from "ics"; +import dayjs, { Dayjs } from "dayjs"; import EventMail from "./EventMail"; -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 utc from "dayjs/plugin/utc"; +import timezone from "dayjs/plugin/timezone"; +import toArray from "dayjs/plugin/toArray"; +import localizedFormat from "dayjs/plugin/localizedFormat"; dayjs.extend(utc); dayjs.extend(timezone); dayjs.extend(toArray); @@ -18,14 +18,24 @@ export default class EventOrganizerMail extends EventMail { */ protected getiCalEventAsString(): string { const icsEvent = createEvent({ - start: dayjs(this.calEvent.startTime).utc().toArray().slice(0, 6).map((v, i) => i === 1 ? v + 1 : v), - startInputType: 'utc', - productId: 'calendso/ics', + start: dayjs(this.calEvent.startTime) + .utc() + .toArray() + .slice(0, 6) + .map((v, i) => (i === 1 ? v + 1 : v)), + startInputType: "utc", + productId: "calendso/ics", title: `${this.calEvent.type} with ${this.calEvent.attendees[0].name}`, - description: this.calEvent.description + this.stripHtml(this.getAdditionalBody()) + this.stripHtml(this.getAdditionalFooter()), - duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), 'minute') }, + description: + this.calEvent.description + + this.stripHtml(this.getAdditionalBody()) + + this.stripHtml(this.getAdditionalFooter()), + duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") }, organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email }, - attendees: this.calEvent.attendees.map( (attendee: any) => ({ name: attendee.name, email: attendee.email }) ), + attendees: this.calEvent.attendees.map((attendee: any) => ({ + name: attendee.name, + email: attendee.email, + })), status: "CONFIRMED", }); if (icsEvent.error) { @@ -40,7 +50,8 @@ export default class EventOrganizerMail extends EventMail { * @protected */ protected getHtmlRepresentation(): string { - return ` + return ( + `
Hi ${this.calEvent.organizer.name},

@@ -51,40 +62,71 @@ export default class EventOrganizerMail extends EventMail {
Invitee Email:
${this.calEvent.attendees[0].email}
-
` + this.getAdditionalBody() + - ( - this.calEvent.location ? ` - Location:
- ${this.calEvent.location}
-
- ` : '' - ) + +
` + + this.getAdditionalBody() + + "
" + `Invitee Time Zone:
- ${this.calEvent.attendees[0].timeZone}
-
- Additional notes:
- ${this.calEvent.description} - ` + this.getAdditionalFooter() + ` + ${this.calEvent.attendees[0].timeZone}
+
+ Additional notes:
+ ${this.calEvent.description} + ` + + this.getAdditionalFooter() + + `
- `; + ` + ); } + /** + * Adds the video call information to the mail body. + * + * @protected + */ + protected getLocation(): string { + if (this.additionInformation?.hangoutLink) { + return `Location: ${this.additionInformation?.hangoutLink}
`; + } + + if (this.additionInformation?.entryPoints && this.additionInformation?.entryPoints.length > 0) { + const locations = this.additionInformation?.entryPoints + .map((entryPoint) => { + return ` + Join by ${entryPoint.entryPointType}:
+ ${entryPoint.label}
+ `; + }) + .join("
"); + + return `Locations:
${locations}`; + } + + return this.calEvent.location ? `Location: ${this.calEvent.location}

` : ""; + } + + protected getAdditionalBody(): string { + return ` + ${this.getLocation()} + `; + } /** * Returns the payload object for the nodemailer. * * @protected */ - protected getNodeMailerPayload(): Object { + protected getNodeMailerPayload() { const organizerStart: Dayjs = dayjs(this.calEvent.startTime).tz(this.calEvent.organizer.timeZone); return { icalEvent: { - filename: 'event.ics', + filename: "event.ics", content: this.getiCalEventAsString(), }, 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: `New event: ${this.calEvent.attendees[0].name} - ${organizerStart.format("LT dddd, LL")} - ${ + this.calEvent.type + }`, html: this.getHtmlRepresentation(), text: this.getPlainTextRepresentation(), }; @@ -93,4 +135,4 @@ export default class EventOrganizerMail extends EventMail { protected printNodeMailerError(error: string): void { console.error("SEND_NEW_EVENT_NOTIFICATION_ERROR", this.calEvent.organizer.email, error); } -} \ No newline at end of file +} From 098b95ef55a176613a1b104b75f98cc8277d1835 Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 29 Jun 2021 23:43:18 +0200 Subject: [PATCH 04/33] Introduced CalEventParser to acquire rich descriptions for events in integrations --- lib/CalEventParser.ts | 108 ++++++++++++++++++++ lib/calendarClient.ts | 24 +++-- lib/emails/EventAttendeeMail.ts | 40 +++++--- lib/emails/EventAttendeeRescheduledMail.ts | 24 +++-- lib/emails/EventMail.ts | 60 +++++------ lib/emails/EventOrganizerMail.ts | 67 +++++++----- lib/emails/EventOrganizerRescheduledMail.ts | 33 +++--- lib/emails/helpers.ts | 27 +++-- 8 files changed, 268 insertions(+), 115 deletions(-) create mode 100644 lib/CalEventParser.ts diff --git a/lib/CalEventParser.ts b/lib/CalEventParser.ts new file mode 100644 index 0000000000..4668fb7741 --- /dev/null +++ b/lib/CalEventParser.ts @@ -0,0 +1,108 @@ +import { CalendarEvent } from "./calendarClient"; +import { v5 as uuidv5 } from "uuid"; +import short from "short-uuid"; +import { stripHtml } from "./emails/helpers"; + +const translator = short(); + +export default class CalEventParser { + calEvent: CalendarEvent; + + constructor(calEvent: CalendarEvent) { + this.calEvent = calEvent; + } + + /** + * Returns a link to reschedule the given booking. + */ + public getRescheduleLink(): string { + return process.env.BASE_URL + "/reschedule/" + this.getUid(); + } + + /** + * Returns a link to cancel the given booking. + */ + public getCancelLink(): string { + return process.env.BASE_URL + "/cancel/" + this.getUid(); + } + + /** + * Returns a unique identifier for the given calendar event. + */ + public getUid(): string { + return translator.fromUUID(uuidv5(JSON.stringify(this.calEvent), uuidv5.URL)); + } + + /** + * Returns a footer section with links to change the event (as HTML). + */ + public getChangeEventFooterHtml(): string { + return ` +
+
+ Need to change this event?
+ Cancel: ${this.getCancelLink()}
+ Reschedule: ${this.getRescheduleLink()} + `; + } + + /** + * Returns a footer section with links to change the event (as plain text). + */ + public getChangeEventFooter(): string { + return stripHtml(this.getChangeEventFooterHtml()); + } + + /** + * Returns an extended description with all important information (as HTML). + * + * @protected + */ + public getRichDescriptionHtml(): string { + return ( + ` +
+ Event Type:
+ ${this.calEvent.type}
+
+ Invitee Email:
+ ${this.calEvent.attendees[0].email}
+
` + + (this.calEvent.location + ? ` + Location:
+ ${this.calEvent.location}
+
+ ` + : "") + + `Invitee Time Zone:
+ ${this.calEvent.attendees[0].timeZone}
+
+ Additional notes:
+ ${this.calEvent.description} + ` + + this.getChangeEventFooterHtml() + + ` +
+ ` + ); + } + + /** + * Returns an extended description with all important information (as plain text). + * + * @protected + */ + public getRichDescription(): string { + return stripHtml(this.getRichDescriptionHtml()); + } + + /** + * Returns a calendar event with rich description. + */ + public asRichEvent(): CalendarEvent { + const eventCopy: CalendarEvent = { ...this.calEvent }; + eventCopy.description = this.getRichDescription(); + return eventCopy; + } +} diff --git a/lib/calendarClient.ts b/lib/calendarClient.ts index 4d8d7421b6..80b17de270 100644 --- a/lib/calendarClient.ts +++ b/lib/calendarClient.ts @@ -1,15 +1,12 @@ import EventOrganizerMail from "./emails/EventOrganizerMail"; import EventAttendeeMail from "./emails/EventAttendeeMail"; -import { v5 as uuidv5 } from "uuid"; -import short from "short-uuid"; import EventOrganizerRescheduledMail from "./emails/EventOrganizerRescheduledMail"; import EventAttendeeRescheduledMail from "./emails/EventAttendeeRescheduledMail"; - -const translator = short(); +import prisma from "./prisma"; +import CalEventParser from "./CalEventParser"; // eslint-disable-next-line @typescript-eslint/no-var-requires const { google } = require("googleapis"); -import prisma from "./prisma"; const googleAuth = (credential) => { const { client_secret, client_id, redirect_uris } = JSON.parse(process.env.GOOGLE_API_CREDENTIALS).web; @@ -105,12 +102,14 @@ const o365Auth = (credential) => { }; }; +// eslint-disable-next-line interface Person { name?: string; email: string; timeZone: string; } +// eslint-disable-next-line interface CalendarEvent { type: string; title: string; @@ -123,10 +122,12 @@ interface CalendarEvent { conferenceData?: ConferenceData; } +// eslint-disable-next-line interface ConferenceData { createRequest: any; } +// eslint-disable-next-line interface IntegrationCalendar { integration: string; primary: boolean; @@ -134,6 +135,7 @@ interface IntegrationCalendar { name: string; } +// eslint-disable-next-line interface CalendarApiAdapter { createEvent(event: CalendarEvent): Promise; @@ -507,9 +509,11 @@ const listCalendars = (withCredentials) => ); const createEvent = async (credential, calEvent: CalendarEvent): Promise => { - const uid: string = translator.fromUUID(uuidv5(JSON.stringify(calEvent), uuidv5.URL)); + const parser: CalEventParser = new CalEventParser(calEvent); + const uid: string = parser.getUid(); + const richEvent: CalendarEvent = parser.asRichEvent(); - const creationResult = credential ? await calendars([credential])[0].createEvent(calEvent) : null; + const creationResult = credential ? await calendars([credential])[0].createEvent(richEvent) : null; const organizerMail = new EventOrganizerMail(calEvent, uid); const attendeeMail = new EventAttendeeMail(calEvent, uid); @@ -534,10 +538,12 @@ const createEvent = async (credential, calEvent: CalendarEvent): Promise => }; const updateEvent = async (credential, uidToUpdate: string, calEvent: CalendarEvent): Promise => { - const newUid: string = translator.fromUUID(uuidv5(JSON.stringify(calEvent), uuidv5.URL)); + const parser: CalEventParser = new CalEventParser(calEvent); + const newUid: string = parser.getUid(); + const richEvent: CalendarEvent = parser.asRichEvent(); const updateResult = credential - ? await calendars([credential])[0].updateEvent(uidToUpdate, calEvent) + ? await calendars([credential])[0].updateEvent(uidToUpdate, richEvent) : null; const organizerMail = new EventOrganizerRescheduledMail(calEvent, newUid); diff --git a/lib/emails/EventAttendeeMail.ts b/lib/emails/EventAttendeeMail.ts index 55f231f939..cc4cf4603e 100644 --- a/lib/emails/EventAttendeeMail.ts +++ b/lib/emails/EventAttendeeMail.ts @@ -1,9 +1,9 @@ -import dayjs, {Dayjs} from "dayjs"; +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'; +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); @@ -15,20 +15,28 @@ export default class EventAttendeeMail extends EventMail { * @protected */ protected getHtmlRepresentation(): string { - return ` + return ( + `
Hi ${this.calEvent.attendees[0].name},

- Your ${this.calEvent.type} with ${this.calEvent.organizer.name} at ${this.getInviteeStart().format('h:mma')} - (${this.calEvent.attendees[0].timeZone}) on ${this.getInviteeStart().format('dddd, LL')} is scheduled.
-
` + this.getAdditionalBody() + ( - this.calEvent.location ? `Location: ${this.calEvent.location}

` : '' - ) + + Your ${this.calEvent.type} with ${this.calEvent.organizer.name} at ${this.getInviteeStart().format( + "h:mma" + )} + (${this.calEvent.attendees[0].timeZone}) on ${this.getInviteeStart().format( + "dddd, LL" + )} is scheduled.
+
` + + this.getAdditionalBody() + + (this.calEvent.location ? `Location: ${this.calEvent.location}

` : "") + `Additional notes:
${this.calEvent.description}
- ` + this.getAdditionalFooter() + ` + ` + + this.getAdditionalFooter() + + `
- `; + ` + ); } /** @@ -36,12 +44,14 @@ export default class EventAttendeeMail extends EventMail { * * @protected */ - protected getNodeMailerPayload(): Object { + protected getNodeMailerPayload(): Record { 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: `Confirmed: ${this.calEvent.type} with ${this.calEvent.organizer.name} on ${this.getInviteeStart().format('dddd, LL')}`, + subject: `Confirmed: ${this.calEvent.type} with ${ + this.calEvent.organizer.name + } on ${this.getInviteeStart().format("dddd, LL")}`, html: this.getHtmlRepresentation(), text: this.getPlainTextRepresentation(), }; @@ -59,4 +69,4 @@ export default class EventAttendeeMail extends EventMail { protected getInviteeStart(): Dayjs { return dayjs(this.calEvent.startTime).tz(this.calEvent.attendees[0].timeZone); } -} \ No newline at end of file +} diff --git a/lib/emails/EventAttendeeRescheduledMail.ts b/lib/emails/EventAttendeeRescheduledMail.ts index 760aa040f8..1286c7ab7f 100644 --- a/lib/emails/EventAttendeeRescheduledMail.ts +++ b/lib/emails/EventAttendeeRescheduledMail.ts @@ -7,15 +7,21 @@ export default class EventAttendeeRescheduledMail extends EventAttendeeMail { * @protected */ protected getHtmlRepresentation(): string { - return ` + return ( + `
Hi ${this.calEvent.attendees[0].name},

- Your ${this.calEvent.type} with ${this.calEvent.organizer.name} has been rescheduled to ${this.getInviteeStart().format('h:mma')} - (${this.calEvent.attendees[0].timeZone}) on ${this.getInviteeStart().format('dddd, LL')}.
- ` + this.getAdditionalFooter() + ` + Your ${this.calEvent.type} with ${ + this.calEvent.organizer.name + } has been rescheduled to ${this.getInviteeStart().format("h:mma")} + (${this.calEvent.attendees[0].timeZone}) on ${this.getInviteeStart().format("dddd, LL")}.
+ ` + + this.getAdditionalFooter() + + `
- `; + ` + ); } /** @@ -23,12 +29,14 @@ export default class EventAttendeeRescheduledMail extends EventAttendeeMail { * * @protected */ - protected getNodeMailerPayload(): Object { + protected getNodeMailerPayload(): Record { 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: `Rescheduled: ${this.calEvent.type} with ${this.calEvent.organizer.name} on ${this.getInviteeStart().format('dddd, LL')}`, + subject: `Rescheduled: ${this.calEvent.type} with ${ + this.calEvent.organizer.name + } on ${this.getInviteeStart().format("dddd, LL")}`, html: this.getHtmlRepresentation(), text: this.getPlainTextRepresentation(), }; @@ -37,4 +45,4 @@ export default class EventAttendeeRescheduledMail extends EventAttendeeMail { protected printNodeMailerError(error: string): void { console.error("SEND_RESCHEDULE_CONFIRMATION_ERROR", this.calEvent.attendees[0].email, error); } -} \ No newline at end of file +} diff --git a/lib/emails/EventMail.ts b/lib/emails/EventMail.ts index de1c050732..9ff1cabb95 100644 --- a/lib/emails/EventMail.ts +++ b/lib/emails/EventMail.ts @@ -1,9 +1,12 @@ -import {CalendarEvent} from "../calendarClient"; -import {serverConfig} from "../serverConfig"; -import nodemailer from 'nodemailer'; +import { CalendarEvent } from "../calendarClient"; +import { serverConfig } from "../serverConfig"; +import nodemailer from "nodemailer"; +import CalEventParser from "../CalEventParser"; +import { stripHtml } from "./helpers"; export default abstract class EventMail { calEvent: CalendarEvent; + parser: CalEventParser; uid: string; /** @@ -17,6 +20,7 @@ export default abstract class EventMail { constructor(calEvent: CalendarEvent, uid: string) { this.calEvent = calEvent; this.uid = uid; + this.parser = new CalEventParser(calEvent); } /** @@ -33,41 +37,30 @@ export default abstract class EventMail { * @protected */ protected getPlainTextRepresentation(): string { - return this.stripHtml(this.getHtmlRepresentation()); - } - - /** - * Strips off all HTML tags and leaves plain text. - * - * @param html - * @protected - */ - protected stripHtml(html: string): string { - return html - .replace('
', "\n") - .replace(/<[^>]+>/g, ''); + return stripHtml(this.getHtmlRepresentation()); } /** * Returns the payload object for the nodemailer. * @protected */ - protected abstract getNodeMailerPayload(): Object; + protected abstract getNodeMailerPayload(): Record; /** * Sends the email to the event attendant and returns a Promise. */ public sendEmail(): Promise { - new Promise((resolve, reject) => nodemailer.createTransport(this.getMailerOptions().transport).sendMail( - this.getNodeMailerPayload(), - (error, info) => { - if (error) { - this.printNodeMailerError(error); - reject(new Error(error)); - } else { - resolve(info); - } - }) + new Promise((resolve, reject) => + nodemailer + .createTransport(this.getMailerOptions().transport) + .sendMail(this.getNodeMailerPayload(), (error, info) => { + if (error) { + this.printNodeMailerError(error); + reject(new Error(error)); + } else { + resolve(info); + } + }) ).catch((e) => console.error("sendEmail", e)); return new Promise((resolve) => resolve("send mail async")); } @@ -109,7 +102,7 @@ export default abstract class EventMail { * @protected */ protected getRescheduleLink(): string { - return process.env.BASE_URL + '/reschedule/' + this.uid; + return this.parser.getRescheduleLink(); } /** @@ -118,21 +111,14 @@ export default abstract class EventMail { * @protected */ protected getCancelLink(): string { - return process.env.BASE_URL + '/cancel/' + this.uid; + return this.parser.getCancelLink(); } - /** * Defines a footer that will be appended to the email. * @protected */ protected getAdditionalFooter(): string { - return ` -
-
- Need to change this event?
- Cancel: ${this.getCancelLink()}
- Reschedule: ${this.getRescheduleLink()} - `; + return this.parser.getChangeEventFooterHtml(); } } diff --git a/lib/emails/EventOrganizerMail.ts b/lib/emails/EventOrganizerMail.ts index 6d3060b6ca..20409ce2eb 100644 --- a/lib/emails/EventOrganizerMail.ts +++ b/lib/emails/EventOrganizerMail.ts @@ -1,11 +1,13 @@ -import {createEvent} from "ics"; -import dayjs, {Dayjs} from "dayjs"; +import { createEvent } from "ics"; +import dayjs, { Dayjs } from "dayjs"; import EventMail from "./EventMail"; -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 utc from "dayjs/plugin/utc"; +import timezone from "dayjs/plugin/timezone"; +import toArray from "dayjs/plugin/toArray"; +import localizedFormat from "dayjs/plugin/localizedFormat"; +import { stripHtml } from "./helpers"; + dayjs.extend(utc); dayjs.extend(timezone); dayjs.extend(toArray); @@ -18,14 +20,24 @@ export default class EventOrganizerMail extends EventMail { */ protected getiCalEventAsString(): string { const icsEvent = createEvent({ - start: dayjs(this.calEvent.startTime).utc().toArray().slice(0, 6).map((v, i) => i === 1 ? v + 1 : v), - startInputType: 'utc', - productId: 'calendso/ics', + start: dayjs(this.calEvent.startTime) + .utc() + .toArray() + .slice(0, 6) + .map((v, i) => (i === 1 ? v + 1 : v)), + startInputType: "utc", + productId: "calendso/ics", title: `${this.calEvent.type} with ${this.calEvent.attendees[0].name}`, - description: this.calEvent.description + this.stripHtml(this.getAdditionalBody()) + this.stripHtml(this.getAdditionalFooter()), - duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), 'minute') }, + description: + this.calEvent.description + + stripHtml(this.getAdditionalBody()) + + stripHtml(this.getAdditionalFooter()), + duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") }, organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email }, - attendees: this.calEvent.attendees.map( (attendee: any) => ({ name: attendee.name, email: attendee.email }) ), + attendees: this.calEvent.attendees.map((attendee: any) => ({ + name: attendee.name, + email: attendee.email, + })), status: "CONFIRMED", }); if (icsEvent.error) { @@ -40,7 +52,8 @@ export default class EventOrganizerMail extends EventMail { * @protected */ protected getHtmlRepresentation(): string { - return ` + return ( + `
Hi ${this.calEvent.organizer.name},

@@ -51,22 +64,26 @@ export default class EventOrganizerMail extends EventMail {
Invitee Email:
${this.calEvent.attendees[0].email}
-
` + this.getAdditionalBody() + - ( - this.calEvent.location ? ` +
` + + this.getAdditionalBody() + + (this.calEvent.location + ? ` Location:
${this.calEvent.location}

- ` : '' - ) + + ` + : "") + `Invitee Time Zone:
${this.calEvent.attendees[0].timeZone}

Additional notes:
${this.calEvent.description} - ` + this.getAdditionalFooter() + ` + ` + + this.getAdditionalFooter() + + `
- `; + ` + ); } /** @@ -74,17 +91,19 @@ export default class EventOrganizerMail extends EventMail { * * @protected */ - protected getNodeMailerPayload(): Object { + protected getNodeMailerPayload(): Record { const organizerStart: Dayjs = dayjs(this.calEvent.startTime).tz(this.calEvent.organizer.timeZone); return { icalEvent: { - filename: 'event.ics', + filename: "event.ics", content: this.getiCalEventAsString(), }, 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: `New event: ${this.calEvent.attendees[0].name} - ${organizerStart.format("LT dddd, LL")} - ${ + this.calEvent.type + }`, html: this.getHtmlRepresentation(), text: this.getPlainTextRepresentation(), }; @@ -93,4 +112,4 @@ export default class EventOrganizerMail extends EventMail { protected printNodeMailerError(error: string): void { console.error("SEND_NEW_EVENT_NOTIFICATION_ERROR", this.calEvent.organizer.email, error); } -} \ No newline at end of file +} diff --git a/lib/emails/EventOrganizerRescheduledMail.ts b/lib/emails/EventOrganizerRescheduledMail.ts index 7e67ac4466..af9cc864de 100644 --- a/lib/emails/EventOrganizerRescheduledMail.ts +++ b/lib/emails/EventOrganizerRescheduledMail.ts @@ -1,4 +1,4 @@ -import dayjs, {Dayjs} from "dayjs"; +import dayjs, { Dayjs } from "dayjs"; import EventOrganizerMail from "./EventOrganizerMail"; export default class EventOrganizerRescheduledMail extends EventOrganizerMail { @@ -8,7 +8,8 @@ export default class EventOrganizerRescheduledMail extends EventOrganizerMail { * @protected */ protected getHtmlRepresentation(): string { - return ` + return ( + `
Hi ${this.calEvent.organizer.name},

@@ -19,22 +20,26 @@ export default class EventOrganizerRescheduledMail extends EventOrganizerMail {
Invitee Email:
${this.calEvent.attendees[0].email}
-
` + this.getAdditionalBody() + - ( - this.calEvent.location ? ` +
` + + this.getAdditionalBody() + + (this.calEvent.location + ? ` Location:
${this.calEvent.location}

- ` : '' - ) + + ` + : "") + `Invitee Time Zone:
${this.calEvent.attendees[0].timeZone}

Additional notes:
${this.calEvent.description} - ` + this.getAdditionalFooter() + ` + ` + + this.getAdditionalFooter() + + `
- `; + ` + ); } /** @@ -42,17 +47,19 @@ export default class EventOrganizerRescheduledMail extends EventOrganizerMail { * * @protected */ - protected getNodeMailerPayload(): Object { + protected getNodeMailerPayload(): Record { const organizerStart: Dayjs = dayjs(this.calEvent.startTime).tz(this.calEvent.organizer.timeZone); return { icalEvent: { - filename: 'event.ics', + filename: "event.ics", content: this.getiCalEventAsString(), }, from: `Calendso <${this.getMailerOptions().from}>`, to: this.calEvent.organizer.email, - subject: `Rescheduled event: ${this.calEvent.attendees[0].name} - ${organizerStart.format('LT dddd, LL')} - ${this.calEvent.type}`, + subject: `Rescheduled event: ${this.calEvent.attendees[0].name} - ${organizerStart.format( + "LT dddd, LL" + )} - ${this.calEvent.type}`, html: this.getHtmlRepresentation(), text: this.getPlainTextRepresentation(), }; @@ -61,4 +68,4 @@ export default class EventOrganizerRescheduledMail extends EventOrganizerMail { protected printNodeMailerError(error: string): void { console.error("SEND_RESCHEDULE_EVENT_NOTIFICATION_ERROR", this.calEvent.organizer.email, error); } -} \ No newline at end of file +} diff --git a/lib/emails/helpers.ts b/lib/emails/helpers.ts index ed5a10c479..e5218a0a06 100644 --- a/lib/emails/helpers.ts +++ b/lib/emails/helpers.ts @@ -1,4 +1,4 @@ -import {VideoCallData} from "../videoClient"; +import { VideoCallData } from "../videoClient"; export function getIntegrationName(videoCallData: VideoCallData): string { //TODO: When there are more complex integration type strings, we should consider using an extra field in the DB for that. @@ -6,15 +6,24 @@ export function getIntegrationName(videoCallData: VideoCallData): string { return nameProto.charAt(0).toUpperCase() + nameProto.slice(1); } +function extractZoom(videoCallData: VideoCallData): string { + const strId = videoCallData.id.toString(); + const part1 = strId.slice(0, 3); + const part2 = strId.slice(3, 7); + const part3 = strId.slice(7, 11); + + return part1 + " " + part2 + " " + part3; +} + export function getFormattedMeetingId(videoCallData: VideoCallData): string { - switch(videoCallData.type) { - case 'zoom_video': - const strId = videoCallData.id.toString(); - const part1 = strId.slice(0, 3); - const part2 = strId.slice(3, 7); - const part3 = strId.slice(7, 11); - return part1 + " " + part2 + " " + part3; + switch (videoCallData.type) { + case "zoom_video": + return extractZoom(videoCallData); default: return videoCallData.id.toString(); } -} \ No newline at end of file +} + +export function stripHtml(html: string): string { + return html.replace("
", "\n").replace(/<[^>]+>/g, ""); +} From f91fdd16cf15c73d7d70ec63592614a0cb18a01e Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 29 Jun 2021 23:47:50 +0200 Subject: [PATCH 05/33] Use HTML to allow line breaks in outlook --- lib/CalEventParser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/CalEventParser.ts b/lib/CalEventParser.ts index 4668fb7741..5261a12023 100644 --- a/lib/CalEventParser.ts +++ b/lib/CalEventParser.ts @@ -102,7 +102,7 @@ export default class CalEventParser { */ public asRichEvent(): CalendarEvent { const eventCopy: CalendarEvent = { ...this.calEvent }; - eventCopy.description = this.getRichDescription(); + eventCopy.description = this.getRichDescriptionHtml(); return eventCopy; } } From 7568071334f939e5383500d6bbaa30dc4565c15d Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 30 Jun 2021 00:11:16 +0200 Subject: [PATCH 06/33] removed div --- lib/CalEventParser.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/CalEventParser.ts b/lib/CalEventParser.ts index 5261a12023..3c1f246692 100644 --- a/lib/CalEventParser.ts +++ b/lib/CalEventParser.ts @@ -61,7 +61,6 @@ export default class CalEventParser { public getRichDescriptionHtml(): string { return ( ` -
Event Type:
${this.calEvent.type}

@@ -81,10 +80,7 @@ export default class CalEventParser { Additional notes:
${this.calEvent.description} ` + - this.getChangeEventFooterHtml() + - ` -
- ` + this.getChangeEventFooterHtml() ); } From ae3aa275f72df0edb3e2532bef350dfb5c094bdf Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 30 Jun 2021 00:16:25 +0200 Subject: [PATCH 07/33] fixed padding --- lib/CalEventParser.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/CalEventParser.ts b/lib/CalEventParser.ts index 3c1f246692..ced7276e2d 100644 --- a/lib/CalEventParser.ts +++ b/lib/CalEventParser.ts @@ -61,24 +61,24 @@ export default class CalEventParser { public getRichDescriptionHtml(): string { return ( ` - Event Type:
- ${this.calEvent.type}
-
- Invitee Email:
- ${this.calEvent.attendees[0].email}
-
` + + Event Type:
+ ${this.calEvent.type}
+
+ Invitee Email:
+ ${this.calEvent.attendees[0].email}
+
` + (this.calEvent.location ? ` - Location:
- ${this.calEvent.location}
-
+ Location:
+ ${this.calEvent.location}
+
` : "") + `Invitee Time Zone:
- ${this.calEvent.attendees[0].timeZone}
-
- Additional notes:
- ${this.calEvent.description} + ${this.calEvent.attendees[0].timeZone}
+
+ Additional notes:
+ ${this.calEvent.description} ` + this.getChangeEventFooterHtml() ); From e49c636a134e4a7cfef99743c80f108c4955d895 Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 30 Jun 2021 00:31:30 +0200 Subject: [PATCH 08/33] Fixed padding --- lib/CalEventParser.ts | 43 +++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/lib/CalEventParser.ts b/lib/CalEventParser.ts index ced7276e2d..d7240fbfad 100644 --- a/lib/CalEventParser.ts +++ b/lib/CalEventParser.ts @@ -38,11 +38,10 @@ export default class CalEventParser { */ public getChangeEventFooterHtml(): string { return ` -
-
- Need to change this event?
- Cancel: ${this.getCancelLink()}
- Reschedule: ${this.getRescheduleLink()} + +Need to change this event? +Cancel: ${this.getCancelLink()} +Reschedule: ${this.getRescheduleLink()} `; } @@ -59,27 +58,27 @@ export default class CalEventParser { * @protected */ public getRichDescriptionHtml(): string { + // This odd indentation is necessary because otherwise the leading tabs will be applied into the event description. return ( ` - Event Type:
- ${this.calEvent.type}
-
- Invitee Email:
- ${this.calEvent.attendees[0].email}
-
` + +Event Type: +${this.calEvent.type} + +Invitee Email: +${this.calEvent.attendees[0].email} + +` + (this.calEvent.location - ? ` - Location:
- ${this.calEvent.location}
-
- ` + ? `Location: +${this.calEvent.location} + +` : "") + - `Invitee Time Zone:
- ${this.calEvent.attendees[0].timeZone}
-
- Additional notes:
- ${this.calEvent.description} - ` + + `Invitee Time Zone: +${this.calEvent.attendees[0].timeZone} + +Additional notes: +${this.calEvent.description}` + this.getChangeEventFooterHtml() ); } From 88db124f471fa39447c011cbf8935b14fd066666 Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 30 Jun 2021 00:35:05 +0200 Subject: [PATCH 09/33] Fixed padding --- lib/emails/VideoEventOrganizerMail.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/emails/VideoEventOrganizerMail.ts b/lib/emails/VideoEventOrganizerMail.ts index 60d85237cf..1f5b2384ab 100644 --- a/lib/emails/VideoEventOrganizerMail.ts +++ b/lib/emails/VideoEventOrganizerMail.ts @@ -1,7 +1,7 @@ -import {CalendarEvent} from "../calendarClient"; +import { CalendarEvent } from "../calendarClient"; import EventOrganizerMail from "./EventOrganizerMail"; -import {VideoCallData} from "../videoClient"; -import {getFormattedMeetingId, getIntegrationName} from "./helpers"; +import { VideoCallData } from "../videoClient"; +import { getFormattedMeetingId, getIntegrationName } from "./helpers"; export default class VideoEventOrganizerMail extends EventOrganizerMail { videoCallData: VideoCallData; @@ -18,11 +18,12 @@ export default class VideoEventOrganizerMail extends EventOrganizerMail { * @protected */ protected getAdditionalBody(): string { + // This odd indentation is necessary because otherwise the leading tabs will be applied into the event description. return ` - Video call provider: ${getIntegrationName(this.videoCallData)}
- Meeting ID: ${getFormattedMeetingId(this.videoCallData)}
- Meeting Password: ${this.videoCallData.password}
- Meeting URL: ${this.videoCallData.url}
+Video call provider: ${getIntegrationName(this.videoCallData)}
+Meeting ID: ${getFormattedMeetingId(this.videoCallData)}
+Meeting Password: ${this.videoCallData.password}
+Meeting URL: ${this.videoCallData.url}
`; } -} \ No newline at end of file +} From 8accc3b7752ab31c156036beed10d431d5426b6b Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 30 Jun 2021 00:50:34 +0200 Subject: [PATCH 10/33] Fixed padding --- lib/CalEventParser.ts | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/lib/CalEventParser.ts b/lib/CalEventParser.ts index d7240fbfad..e6531b1f67 100644 --- a/lib/CalEventParser.ts +++ b/lib/CalEventParser.ts @@ -37,11 +37,8 @@ export default class CalEventParser { * Returns a footer section with links to change the event (as HTML). */ public getChangeEventFooterHtml(): string { - return ` - -Need to change this event? -Cancel: ${this.getCancelLink()} -Reschedule: ${this.getRescheduleLink()} + return `
+Need to change this event?
Cancel: ${this.getCancelLink()}
Reschedule: ${this.getRescheduleLink()} `; } @@ -61,24 +58,15 @@ Reschedule: ${this.getRescheduleLink()}Event Type: -${this.calEvent.type} - -Invitee Email: -${this.calEvent.attendees[0].email} - +Event Type:
${this.calEvent.type}
+Invitee Email:
${this.calEvent.attendees[0].email}
` + (this.calEvent.location - ? `Location: -${this.calEvent.location} - + ? `Location:
${this.calEvent.location}
` : "") + - `Invitee Time Zone: -${this.calEvent.attendees[0].timeZone} - -Additional notes: -${this.calEvent.description}` + + `Invitee Time Zone:
${this.calEvent.attendees[0].timeZone}
+Additional notes:
${this.calEvent.description}
` + this.getChangeEventFooterHtml() ); } From 313ee0376ecb9e46df4700ad636262479173cd3e Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 30 Jun 2021 00:53:37 +0200 Subject: [PATCH 11/33] Fixed padding --- lib/CalEventParser.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/CalEventParser.ts b/lib/CalEventParser.ts index e6531b1f67..a38494f2aa 100644 --- a/lib/CalEventParser.ts +++ b/lib/CalEventParser.ts @@ -38,7 +38,9 @@ export default class CalEventParser { */ public getChangeEventFooterHtml(): string { return `
-Need to change this event?
Cancel: ${this.getCancelLink()}
Reschedule: ${this.getRescheduleLink()} +Need to change this event?
+Cancel: ${this.getCancelLink()}
+Reschedule: ${this.getRescheduleLink()} `; } From e10030e18b87840e4c2707f773f6e54736e5d2d7 Mon Sep 17 00:00:00 2001 From: mihaic195 Date: Wed, 30 Jun 2021 16:48:34 +0300 Subject: [PATCH 12/33] fix api 500 and redirect to login if not authenticated when accessing /settings/team --- pages/api/teams.ts | 4 ++-- pages/settings/teams.tsx | 30 ++++++++++++++++++++++-------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/pages/api/teams.ts b/pages/api/teams.ts index 1ab69adbc1..ef900fbeef 100644 --- a/pages/api/teams.ts +++ b/pages/api/teams.ts @@ -30,8 +30,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } }); - return res.status(201).setHeader('Location', process.env.BASE_URL + '/api/teams/1').send(null); + return res.status(201).json({ message: 'Team created' }); } - res.status(404).send(null); + res.status(404).json({ message: 'Team not found' }); } diff --git a/pages/settings/teams.tsx b/pages/settings/teams.tsx index e4d1dc5f0b..127192505e 100644 --- a/pages/settings/teams.tsx +++ b/pages/settings/teams.tsx @@ -1,3 +1,4 @@ +import { GetServerSideProps } from "next"; import Head from 'next/head'; import prisma from '../../lib/prisma'; import Modal from '../../components/Modal'; @@ -18,14 +19,16 @@ export default function Teams(props) { const [invites, setInvites] = useState([]); const [showCreateTeamModal, setShowCreateTeamModal] = useState(false); - const loadTeams = () => fetch('/api/user/membership').then((res: any) => res.json()).then( + const loadData = () => fetch('/api/user/membership').then((res: any) => res.json()).then( (data) => { - setTeams(data.membership.filter((m) => m.role !== "INVITEE")); - setInvites(data.membership.filter((m) => m.role === "INVITEE")); + setTeams(data.membership && data.membership.filter((m) => m.role !== "INVITEE")); + setInvites(data.membership && data.membership.filter((m) => m.role === "INVITEE")); } ); - useEffect(() => { loadTeams(); }, []); + useEffect(() => { + loadData(); + }, []); if (loading) { return

Loading...

; @@ -40,7 +43,7 @@ export default function Teams(props) { 'Content-Type': 'application/json' } }).then(() => { - loadTeams(); + loadData(); setShowCreateTeamModal(false); }); } @@ -86,14 +89,14 @@ export default function Teams(props) {
{!!teams.length && - + } {!!invites.length &&

Open Invitations

    - {invites.map((team) => )} + {invites.map((team) => )}
}
@@ -153,4 +156,15 @@ export default function Teams(props) { ); -} \ No newline at end of file +} + +export const getServerSideProps: GetServerSideProps = async (context) => { + const session = await getSession(context); + if (!session) { + return { redirect: { permanent: false, destination: "/auth/login" } }; + } + + return { + props: { }, // will be passed to the page component as props + }; +}; From 8b00475b274f82d6964fa2af027777d5d23589a5 Mon Sep 17 00:00:00 2001 From: mihaic195 Date: Wed, 30 Jun 2021 16:52:18 +0300 Subject: [PATCH 13/33] remove unnecessary guard --- pages/settings/teams.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/settings/teams.tsx b/pages/settings/teams.tsx index 127192505e..5ad5db348e 100644 --- a/pages/settings/teams.tsx +++ b/pages/settings/teams.tsx @@ -21,8 +21,8 @@ export default function Teams(props) { const loadData = () => fetch('/api/user/membership').then((res: any) => res.json()).then( (data) => { - setTeams(data.membership && data.membership.filter((m) => m.role !== "INVITEE")); - setInvites(data.membership && data.membership.filter((m) => m.role === "INVITEE")); + setTeams(data.membership.filter((m) => m.role !== "INVITEE")); + setInvites(data.membership.filter((m) => m.role === "INVITEE")); } ); From 1122def2c05de9520521f0f356e0b24ccd404301 Mon Sep 17 00:00:00 2001 From: mihaic195 Date: Thu, 1 Jul 2021 11:47:27 +0300 Subject: [PATCH 14/33] update fetch to also handle errors --- pages/settings/teams.tsx | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/pages/settings/teams.tsx b/pages/settings/teams.tsx index 5ad5db348e..ca6b21c911 100644 --- a/pages/settings/teams.tsx +++ b/pages/settings/teams.tsx @@ -1,30 +1,37 @@ -import { GetServerSideProps } from "next"; import Head from 'next/head'; -import prisma from '../../lib/prisma'; -import Modal from '../../components/Modal'; import Shell from '../../components/Shell'; import SettingsShell from '../../components/Settings'; import { useEffect, useState } from 'react'; -import { useSession, getSession } from 'next-auth/client'; +import { useSession } from 'next-auth/client'; import { UsersIcon, } from "@heroicons/react/outline"; import TeamList from "../../components/team/TeamList"; import TeamListItem from "../../components/team/TeamListItem"; -export default function Teams(props) { - +export default function Teams() { const [session, loading] = useSession(); const [teams, setTeams] = useState([]); const [invites, setInvites] = useState([]); const [showCreateTeamModal, setShowCreateTeamModal] = useState(false); - const loadData = () => fetch('/api/user/membership').then((res: any) => res.json()).then( - (data) => { + const handleErrors = async (resp) => { + if (!resp.ok) { + const err = await resp.json(); + throw new Error(err.message); + } + return resp.json(); + } + + const loadData = () => { + fetch('/api/user/membership') + .then(handleErrors) + .then((data) => { setTeams(data.membership.filter((m) => m.role !== "INVITEE")); setInvites(data.membership.filter((m) => m.role === "INVITEE")); - } - ); + }) + .catch(console.log); + } useEffect(() => { loadData(); @@ -36,6 +43,7 @@ export default function Teams(props) { const createTeam = (e) => { e.preventDefault(); + return fetch('/api/teams', { method: 'POST', body: JSON.stringify({ name: e.target.elements['name'].value }), @@ -157,14 +165,3 @@ export default function Teams(props) { ); } - -export const getServerSideProps: GetServerSideProps = async (context) => { - const session = await getSession(context); - if (!session) { - return { redirect: { permanent: false, destination: "/auth/login" } }; - } - - return { - props: { }, // will be passed to the page component as props - }; -}; From b91d021fc88a3ff5e4a5ac7c1103db4d29a5f01a Mon Sep 17 00:00:00 2001 From: Mihai C <34626017+mihaic195@users.noreply.github.com> Date: Thu, 1 Jul 2021 12:29:55 +0300 Subject: [PATCH 15/33] Update pages/api/teams.ts Co-authored-by: Bailey Pumfleet --- pages/api/teams.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/api/teams.ts b/pages/api/teams.ts index ef900fbeef..e632966e16 100644 --- a/pages/api/teams.ts +++ b/pages/api/teams.ts @@ -33,5 +33,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) return res.status(201).json({ message: 'Team created' }); } - res.status(404).json({ message: 'Team not found' }); + res.status(404).json({ message: "Team not found" }); } From 769f36f8d5a2871a73f54aa1bbb441ff85963529 Mon Sep 17 00:00:00 2001 From: Mihai C <34626017+mihaic195@users.noreply.github.com> Date: Thu, 1 Jul 2021 12:30:10 +0300 Subject: [PATCH 16/33] Update pages/settings/teams.tsx Co-authored-by: Bailey Pumfleet --- pages/settings/teams.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/settings/teams.tsx b/pages/settings/teams.tsx index ca6b21c911..36b57984ff 100644 --- a/pages/settings/teams.tsx +++ b/pages/settings/teams.tsx @@ -24,7 +24,7 @@ export default function Teams() { } const loadData = () => { - fetch('/api/user/membership') + fetch("/api/user/membership") .then(handleErrors) .then((data) => { setTeams(data.membership.filter((m) => m.role !== "INVITEE")); From b71cc82a0ef9def80ad6615187666ab711de95d4 Mon Sep 17 00:00:00 2001 From: Mihai C <34626017+mihaic195@users.noreply.github.com> Date: Thu, 1 Jul 2021 12:30:20 +0300 Subject: [PATCH 17/33] Update pages/settings/teams.tsx Co-authored-by: Bailey Pumfleet --- pages/settings/teams.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/settings/teams.tsx b/pages/settings/teams.tsx index 36b57984ff..b0df7e31ee 100644 --- a/pages/settings/teams.tsx +++ b/pages/settings/teams.tsx @@ -26,7 +26,7 @@ export default function Teams() { const loadData = () => { fetch("/api/user/membership") .then(handleErrors) - .then((data) => { + .then((data) => { setTeams(data.membership.filter((m) => m.role !== "INVITEE")); setInvites(data.membership.filter((m) => m.role === "INVITEE")); }) From 0fb29841386363ad8570c7ad0a209111eb1707f7 Mon Sep 17 00:00:00 2001 From: Mihai C <34626017+mihaic195@users.noreply.github.com> Date: Thu, 1 Jul 2021 12:30:31 +0300 Subject: [PATCH 18/33] Update pages/settings/teams.tsx Co-authored-by: Bailey Pumfleet --- pages/settings/teams.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/settings/teams.tsx b/pages/settings/teams.tsx index b0df7e31ee..90e33d729c 100644 --- a/pages/settings/teams.tsx +++ b/pages/settings/teams.tsx @@ -30,7 +30,7 @@ export default function Teams() { setTeams(data.membership.filter((m) => m.role !== "INVITEE")); setInvites(data.membership.filter((m) => m.role === "INVITEE")); }) - .catch(console.log); + .catch(console.log); } useEffect(() => { From 4a1e8cfbf77ce74bd1fc515726ac221d22e75ec2 Mon Sep 17 00:00:00 2001 From: Mihai C <34626017+mihaic195@users.noreply.github.com> Date: Thu, 1 Jul 2021 12:30:40 +0300 Subject: [PATCH 19/33] Update pages/settings/teams.tsx Co-authored-by: Bailey Pumfleet --- pages/settings/teams.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/settings/teams.tsx b/pages/settings/teams.tsx index 90e33d729c..c50e937b5e 100644 --- a/pages/settings/teams.tsx +++ b/pages/settings/teams.tsx @@ -2,7 +2,7 @@ import Head from 'next/head'; import Shell from '../../components/Shell'; import SettingsShell from '../../components/Settings'; import { useEffect, useState } from 'react'; -import { useSession } from 'next-auth/client'; +import { useSession } from "next-auth/client"; import { UsersIcon, } from "@heroicons/react/outline"; From 9eff620ee0941e7d0ed279751f79f8406d5ba357 Mon Sep 17 00:00:00 2001 From: Mihai C <34626017+mihaic195@users.noreply.github.com> Date: Thu, 1 Jul 2021 12:30:51 +0300 Subject: [PATCH 20/33] Update pages/settings/teams.tsx Co-authored-by: Bailey Pumfleet --- pages/settings/teams.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/settings/teams.tsx b/pages/settings/teams.tsx index c50e937b5e..da622abec2 100644 --- a/pages/settings/teams.tsx +++ b/pages/settings/teams.tsx @@ -29,7 +29,7 @@ export default function Teams() { .then((data) => { setTeams(data.membership.filter((m) => m.role !== "INVITEE")); setInvites(data.membership.filter((m) => m.role === "INVITEE")); - }) + }) .catch(console.log); } From 715033f4e1510be4d7b3c49c458ff63145f5a169 Mon Sep 17 00:00:00 2001 From: Mihai C <34626017+mihaic195@users.noreply.github.com> Date: Thu, 1 Jul 2021 12:31:40 +0300 Subject: [PATCH 21/33] Update pages/settings/teams.tsx Co-authored-by: Bailey Pumfleet --- pages/settings/teams.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/settings/teams.tsx b/pages/settings/teams.tsx index da622abec2..ee026f0633 100644 --- a/pages/settings/teams.tsx +++ b/pages/settings/teams.tsx @@ -104,7 +104,7 @@ export default function Teams() { {!!invites.length &&

Open Invitations

    - {invites.map((team) => )} + {invites.map((team) => (
} From 6463b3ba37c3493dc0d0c67333b426f7465a1862 Mon Sep 17 00:00:00 2001 From: mihaic195 Date: Thu, 1 Jul 2021 12:47:12 +0300 Subject: [PATCH 22/33] fix codacy suggestions --- pages/api/teams.ts | 4 ++-- pages/settings/teams.tsx | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pages/api/teams.ts b/pages/api/teams.ts index e632966e16..1c1254fb05 100644 --- a/pages/api/teams.ts +++ b/pages/api/teams.ts @@ -21,7 +21,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }, }); - const createMembership = await prisma.membership.create({ + await prisma.membership.create({ data: { teamId: createTeam.id, userId: session.user.id, @@ -33,5 +33,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) return res.status(201).json({ message: 'Team created' }); } - res.status(404).json({ message: "Team not found" }); + res.status(404).json({ message: 'Team not found' }); } diff --git a/pages/settings/teams.tsx b/pages/settings/teams.tsx index ee026f0633..dc70c3da38 100644 --- a/pages/settings/teams.tsx +++ b/pages/settings/teams.tsx @@ -2,7 +2,7 @@ import Head from 'next/head'; import Shell from '../../components/Shell'; import SettingsShell from '../../components/Settings'; import { useEffect, useState } from 'react'; -import { useSession } from "next-auth/client"; +import { useSession } from 'next-auth/client'; import { UsersIcon, } from "@heroicons/react/outline"; @@ -26,11 +26,11 @@ export default function Teams() { const loadData = () => { fetch("/api/user/membership") .then(handleErrors) - .then((data) => { + .then((data) => { setTeams(data.membership.filter((m) => m.role !== "INVITEE")); setInvites(data.membership.filter((m) => m.role === "INVITEE")); - }) - .catch(console.log); + }) + .catch(console.log); } useEffect(() => { @@ -104,7 +104,7 @@ export default function Teams() { {!!invites.length &&

Open Invitations

    - {invites.map((team) => ( + {invites.map((team) => )}
} From 6f64df3e75f3446d3d6cefbb635c631d19d39d50 Mon Sep 17 00:00:00 2001 From: mihaic195 Date: Thu, 1 Jul 2021 12:51:51 +0300 Subject: [PATCH 23/33] lint --- pages/settings/teams.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/settings/teams.tsx b/pages/settings/teams.tsx index dc70c3da38..36b57984ff 100644 --- a/pages/settings/teams.tsx +++ b/pages/settings/teams.tsx @@ -104,7 +104,7 @@ export default function Teams() { {!!invites.length &&

Open Invitations

    - {invites.map((team) => )} + {invites.map((team) => )}
} From 2008ad263ce1c1f7d32409a069545f300ffa3334 Mon Sep 17 00:00:00 2001 From: mihaic195 Date: Fri, 2 Jul 2021 13:28:33 +0300 Subject: [PATCH 24/33] fix: ssr session on teams page --- pages/_app.tsx | 3 ++- pages/settings/teams.tsx | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/pages/_app.tsx b/pages/_app.tsx index e2964ddee3..8b0b900996 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,8 +1,9 @@ import '../styles/globals.css'; import {createTelemetryClient, TelemetryProvider} from '../lib/telemetry'; import { Provider } from 'next-auth/client'; +import type { AppProps } from "next/app" -function MyApp({ Component, pageProps }) { +function MyApp({ Component, pageProps }: AppProps) { return ( diff --git a/pages/settings/teams.tsx b/pages/settings/teams.tsx index 36b57984ff..21df9f4d10 100644 --- a/pages/settings/teams.tsx +++ b/pages/settings/teams.tsx @@ -1,8 +1,10 @@ +import { GetServerSideProps } from "next" import Head from 'next/head'; import Shell from '../../components/Shell'; import SettingsShell from '../../components/Settings'; import { useEffect, useState } from 'react'; -import { useSession } from 'next-auth/client'; +import type { Session } from "next-auth" +import { useSession, getSession } from 'next-auth/client'; import { UsersIcon, } from "@heroicons/react/outline"; @@ -165,3 +167,15 @@ export default function Teams() { ); } + +// Export the `session` prop to use sessions with Server Side Rendering +export const getServerSideProps: GetServerSideProps<{session: Session | null}> = async (context) => { + const session = await getSession(context); + if (!session) { + return { redirect: { permanent: false, destination: '/auth/login' } }; + } + + return { + props: { session } + } +} From 8500a8dd64e1ab5daa6ff7413ad7451a8e91d4a9 Mon Sep 17 00:00:00 2001 From: Mihai C <34626017+mihaic195@users.noreply.github.com> Date: Fri, 2 Jul 2021 13:31:59 +0300 Subject: [PATCH 25/33] Update pages/settings/teams.tsx Co-authored-by: Bailey Pumfleet --- pages/settings/teams.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/settings/teams.tsx b/pages/settings/teams.tsx index 21df9f4d10..e0b9ae5736 100644 --- a/pages/settings/teams.tsx +++ b/pages/settings/teams.tsx @@ -3,7 +3,7 @@ import Head from 'next/head'; import Shell from '../../components/Shell'; import SettingsShell from '../../components/Settings'; import { useEffect, useState } from 'react'; -import type { Session } from "next-auth" +import type { Session } from "next-auth"; import { useSession, getSession } from 'next-auth/client'; import { UsersIcon, From 2af9ed55a7a45acb92ca4e872169ab52289091ce Mon Sep 17 00:00:00 2001 From: Mihai C <34626017+mihaic195@users.noreply.github.com> Date: Fri, 2 Jul 2021 13:32:11 +0300 Subject: [PATCH 26/33] Update pages/settings/teams.tsx Co-authored-by: Bailey Pumfleet --- pages/settings/teams.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/settings/teams.tsx b/pages/settings/teams.tsx index e0b9ae5736..74132d38f6 100644 --- a/pages/settings/teams.tsx +++ b/pages/settings/teams.tsx @@ -1,4 +1,4 @@ -import { GetServerSideProps } from "next" +import { GetServerSideProps } from "next"; import Head from 'next/head'; import Shell from '../../components/Shell'; import SettingsShell from '../../components/Settings'; From b15ef1d8b09e75da3ecdde6a2b11d311a229b806 Mon Sep 17 00:00:00 2001 From: Mihai C <34626017+mihaic195@users.noreply.github.com> Date: Fri, 2 Jul 2021 13:32:27 +0300 Subject: [PATCH 27/33] Update pages/settings/teams.tsx Co-authored-by: Bailey Pumfleet --- pages/settings/teams.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/settings/teams.tsx b/pages/settings/teams.tsx index 74132d38f6..d74c8ddd74 100644 --- a/pages/settings/teams.tsx +++ b/pages/settings/teams.tsx @@ -169,7 +169,7 @@ export default function Teams() { } // Export the `session` prop to use sessions with Server Side Rendering -export const getServerSideProps: GetServerSideProps<{session: Session | null}> = async (context) => { +export const getServerSideProps: GetServerSideProps<{ session: Session | null }> = async (context) => { const session = await getSession(context); if (!session) { return { redirect: { permanent: false, destination: '/auth/login' } }; From dcf274456674c7e6ab7efd14c50e1a7a9b17e0b6 Mon Sep 17 00:00:00 2001 From: Mihai C <34626017+mihaic195@users.noreply.github.com> Date: Fri, 2 Jul 2021 13:32:45 +0300 Subject: [PATCH 28/33] Update pages/_app.tsx Co-authored-by: Bailey Pumfleet --- pages/_app.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/_app.tsx b/pages/_app.tsx index 8b0b900996..f521f29355 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,7 +1,7 @@ import '../styles/globals.css'; import {createTelemetryClient, TelemetryProvider} from '../lib/telemetry'; import { Provider } from 'next-auth/client'; -import type { AppProps } from "next/app" +import type { AppProps } from "next/app"; function MyApp({ Component, pageProps }: AppProps) { return ( From 47d7634638ef0f5c5798b7d5aff510bbfa6657dc Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Tue, 6 Jul 2021 16:10:22 +0000 Subject: [PATCH 29/33] Adds prisma helper function whereAndSelect --- lib/prisma.ts | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/prisma.ts b/lib/prisma.ts index 4d3f847070..b20a1b488b 100644 --- a/lib/prisma.ts +++ b/lib/prisma.ts @@ -1,9 +1,9 @@ -import { PrismaClient } from '@prisma/client'; +import { PrismaClient } from "@prisma/client"; let prisma: PrismaClient; -const globalAny:any = global; +const globalAny: any = global; -if (process.env.NODE_ENV === 'production') { +if (process.env.NODE_ENV === "production") { prisma = new PrismaClient(); } else { if (!globalAny.prisma) { @@ -12,4 +12,18 @@ if (process.env.NODE_ENV === 'production') { prisma = globalAny.prisma; } -export default prisma; \ No newline at end of file +const whereAndSelect = (modelQuery, criteria: Record, pluckedAttributes: string[]) => + modelQuery({ + where: criteria, + select: pluckedAttributes.reduce( + (select: { [string]: boolean }, attr: string) => ({ + ...select, + [attr]: true, + }), + {} + ), + }); + +export { whereAndSelect }; + +export default prisma; From e08815ce787eff0ab6d0a545c8269b840c673df2 Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Tue, 6 Jul 2021 18:20:25 +0000 Subject: [PATCH 30/33] Added support for deep selecting --- lib/prisma.ts | 23 ++++++--- test/lib/prisma.test.ts | 101 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 7 deletions(-) create mode 100644 test/lib/prisma.test.ts diff --git a/lib/prisma.ts b/lib/prisma.ts index b20a1b488b..5d75ada2f4 100644 --- a/lib/prisma.ts +++ b/lib/prisma.ts @@ -12,16 +12,25 @@ if (process.env.NODE_ENV === "production") { prisma = globalAny.prisma; } +const pluck = (select: Record, attr: string) => { + const parts = attr.split("."); + const alwaysAttr = parts[0]; + const pluckedValue = + parts.length > 1 + ? { + select: pluck(select[alwaysAttr] ? select[alwaysAttr].select : {}, parts.slice(1).join(".")), + } + : true; + return { + ...select, + [alwaysAttr]: pluckedValue, + }; +}; + const whereAndSelect = (modelQuery, criteria: Record, pluckedAttributes: string[]) => modelQuery({ where: criteria, - select: pluckedAttributes.reduce( - (select: { [string]: boolean }, attr: string) => ({ - ...select, - [attr]: true, - }), - {} - ), + select: pluckedAttributes.reduce(pluck, {}), }); export { whereAndSelect }; diff --git a/test/lib/prisma.test.ts b/test/lib/prisma.test.ts new file mode 100644 index 0000000000..45c343988f --- /dev/null +++ b/test/lib/prisma.test.ts @@ -0,0 +1,101 @@ + +import {it, expect} from '@jest/globals'; +import {whereAndSelect} from "@lib/prisma"; + +it('can decorate using whereAndSelect', async () => { + whereAndSelect( (queryObj) => { + expect(queryObj).toStrictEqual({ where: { id: 1 }, select: { example: true } }); + }, + { id: 1 }, + [ + "example" + ]) +}); + +it('can do nested selects using . seperator', async () => { + + whereAndSelect( (queryObj) => { + expect(queryObj).toStrictEqual({ + where: { + uid: 1, + }, + select: { + description: true, + attendees: { + select: { + email: true, + name: true, + }, + }, + } + }); + }, + { uid: 1 }, + [ + "description", + "attendees.email", + "attendees.name", + ]) +}) + +it('can handle nesting deeply', async () => { + whereAndSelect( (queryObj) => { + expect(queryObj).toStrictEqual({ + where: { + uid: 1, + }, + select: { + description: true, + attendees: { + select: { + email: { + select: { + nested: true + } + }, + name: true, + }, + }, + } + }); + }, + { uid: 1 }, + [ + "description", + "attendees.email.nested", + "attendees.name", + ]) +}); + +it('can handle nesting multiple', async () => { + whereAndSelect( (queryObj) => { + expect(queryObj).toStrictEqual({ + where: { + uid: 1, + }, + select: { + description: true, + attendees: { + select: { + email: true, + name: true, + }, + }, + bookings: { + select: { + id: true, + name: true, + } + } + } + }); + }, + { uid: 1 }, + [ + "description", + "attendees.email", + "attendees.name", + "bookings.id", + "bookings.name", + ]) +}); From de74d85fac59ff7ad34101e48fc10d0991906f96 Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Tue, 6 Jul 2021 18:23:33 +0000 Subject: [PATCH 31/33] Remove --experimental-vm-modules & fix Codacy --- package.json | 2 +- test/lib/prisma.test.ts | 48 ++++++++++++++++++++++++----------------- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index b0ac1af372..4b8836be4b 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "dev": "next dev", - "test": "node --experimental-vm-modules node_modules/.bin/jest", + "test": "node node_modules/.bin/jest", "build": "next build", "start": "next start", "postinstall": "prisma generate", diff --git a/test/lib/prisma.test.ts b/test/lib/prisma.test.ts index 45c343988f..b2164ffd74 100644 --- a/test/lib/prisma.test.ts +++ b/test/lib/prisma.test.ts @@ -1,20 +1,23 @@ -import {it, expect} from '@jest/globals'; -import {whereAndSelect} from "@lib/prisma"; +import { it, expect } from '@jest/globals'; +import { whereAndSelect } from "@lib/prisma"; -it('can decorate using whereAndSelect', async () => { - whereAndSelect( (queryObj) => { - expect(queryObj).toStrictEqual({ where: { id: 1 }, select: { example: true } }); +it("can decorate using whereAndSelect", async () => { + whereAndSelect( + (queryObj) => { + expect(queryObj).toStrictEqual({ where: { id: 1 }, select: { example: true } }); }, { id: 1 }, [ - "example" - ]) + "example", + ] + ); }); -it('can do nested selects using . seperator', async () => { +it("can do nested selects using . seperator", async () => { - whereAndSelect( (queryObj) => { + whereAndSelect( + (queryObj) => { expect(queryObj).toStrictEqual({ where: { uid: 1, @@ -27,7 +30,7 @@ it('can do nested selects using . seperator', async () => { name: true, }, }, - } + }, }); }, { uid: 1 }, @@ -35,11 +38,13 @@ it('can do nested selects using . seperator', async () => { "description", "attendees.email", "attendees.name", - ]) -}) + ] + ); +}); -it('can handle nesting deeply', async () => { - whereAndSelect( (queryObj) => { +it("can handle nesting deeply", async () => { + whereAndSelect( + (queryObj) => { expect(queryObj).toStrictEqual({ where: { uid: 1, @@ -50,13 +55,13 @@ it('can handle nesting deeply', async () => { select: { email: { select: { - nested: true + nested: true, } }, name: true, }, }, - } + }, }); }, { uid: 1 }, @@ -64,11 +69,13 @@ it('can handle nesting deeply', async () => { "description", "attendees.email.nested", "attendees.name", - ]) + ] + ); }); -it('can handle nesting multiple', async () => { - whereAndSelect( (queryObj) => { +it("can handle nesting multiple", async () => { + whereAndSelect( + (queryObj) => { expect(queryObj).toStrictEqual({ where: { uid: 1, @@ -97,5 +104,6 @@ it('can handle nesting multiple', async () => { "attendees.name", "bookings.id", "bookings.name", - ]) + ] + ); }); From 9371e6192c8104a49585f6e07e58a3d304136063 Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 7 Jul 2021 14:07:18 +0200 Subject: [PATCH 32/33] Use auto-generated types --- lib/calendarClient.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/calendarClient.ts b/lib/calendarClient.ts index 364e96baa8..3891feab71 100644 --- a/lib/calendarClient.ts +++ b/lib/calendarClient.ts @@ -3,6 +3,7 @@ import EventAttendeeMail from "./emails/EventAttendeeMail"; import EventOrganizerRescheduledMail from "./emails/EventOrganizerRescheduledMail"; import EventAttendeeRescheduledMail from "./emails/EventAttendeeRescheduledMail"; import prisma from "./prisma"; +import { Credential } from "@prisma/client"; import CalEventParser from "./CalEventParser"; // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -504,7 +505,7 @@ const listCalendars = (withCredentials) => results.reduce((acc, calendars) => acc.concat(calendars), []) ); -const createEvent = async (credential, calEvent: CalendarEvent): Promise => { +const createEvent = async (credential: Credential, calEvent: CalendarEvent): Promise => { const parser: CalEventParser = new CalEventParser(calEvent); const uid: string = parser.getUid(); const richEvent: CalendarEvent = parser.asRichEvent(); @@ -547,7 +548,11 @@ const createEvent = async (credential, calEvent: CalendarEvent): Promise => { +const updateEvent = async ( + credential: Credential, + uidToUpdate: string, + calEvent: CalendarEvent +): Promise => { const parser: CalEventParser = new CalEventParser(calEvent); const newUid: string = parser.getUid(); const richEvent: CalendarEvent = parser.asRichEvent(); @@ -578,7 +583,7 @@ const updateEvent = async (credential, uidToUpdate: string, calEvent: CalendarEv }; }; -const deleteEvent = (credential, uid: string): Promise => { +const deleteEvent = (credential: Credential, uid: string): Promise => { if (credential) { return calendars([credential])[0].deleteEvent(uid); } From 2f73997e720138b76112d1c216a114659d0d5568 Mon Sep 17 00:00:00 2001 From: Femi Odugbesan Date: Wed, 7 Jul 2021 11:53:33 -0500 Subject: [PATCH 33/33] safely check isAvailable (#331) fix issue where checking availability throws an error --- pages/api/book/[user].ts | 144 ++++++++++++++++++++------------------- 1 file changed, 75 insertions(+), 69 deletions(-) diff --git a/pages/api/book/[user].ts b/pages/api/book/[user].ts index 2ea6611ba2..32ae584d44 100644 --- a/pages/api/book/[user].ts +++ b/pages/api/book/[user].ts @@ -1,10 +1,10 @@ import type { NextApiRequest, NextApiResponse } from "next"; import prisma from "../../../lib/prisma"; -import { CalendarEvent, createEvent, updateEvent } from "../../../lib/calendarClient"; +import { CalendarEvent, createEvent, getBusyCalendarTimes, updateEvent } from "../../../lib/calendarClient"; import async from "async"; import { v5 as uuidv5 } from "uuid"; import short from "short-uuid"; -import { createMeeting, updateMeeting } from "../../../lib/videoClient"; +import { createMeeting, getBusyVideoTimes, updateMeeting } from "../../../lib/videoClient"; import EventAttendeeMail from "../../../lib/emails/EventAttendeeMail"; import { getEventName } from "../../../lib/event"; import { LocationType } from "../../../lib/location"; @@ -13,37 +13,39 @@ import dayjs from "dayjs"; const translator = short(); -// Commented out because unused and thus throwing an error in linter. -// const isAvailable = (busyTimes, time, length) => { -// // Check for conflicts -// let t = true; -// busyTimes.forEach((busyTime) => { -// const startTime = dayjs(busyTime.start); -// const endTime = dayjs(busyTime.end); -// -// // Check if start times are the same -// if (dayjs(time).format("HH:mm") == startTime.format("HH:mm")) { -// t = false; -// } -// -// // Check if time is between start and end times -// if (dayjs(time).isBetween(startTime, endTime)) { -// t = false; -// } -// -// // Check if slot end time is between start and end time -// if (dayjs(time).add(length, "minutes").isBetween(startTime, endTime)) { -// t = false; -// } -// -// // Check if startTime is between slot -// if (startTime.isBetween(dayjs(time), dayjs(time).add(length, "minutes"))) { -// t = false; -// } -// }); -// -// return t; -// }; +function isAvailable(busyTimes, time, length) { + // Check for conflicts + let t = true; + + if (Array.isArray(busyTimes) && busyTimes.length > 0) { + busyTimes.forEach((busyTime) => { + const startTime = dayjs(busyTime.start); + const endTime = dayjs(busyTime.end); + + // Check if start times are the same + if (dayjs(time).format("HH:mm") == startTime.format("HH:mm")) { + t = false; + } + + // Check if time is between start and end times + if (dayjs(time).isBetween(startTime, endTime)) { + t = false; + } + + // Check if slot end time is between start and end time + if (dayjs(time).add(length, "minutes").isBetween(startTime, endTime)) { + t = false; + } + + // Check if startTime is between slot + if (startTime.isBetween(dayjs(time), dayjs(time).add(length, "minutes"))) { + t = false; + } + }); + } + + return t; +} interface GetLocationRequestFromIntegrationRequest { location: string; @@ -91,46 +93,43 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }, }); - // Commented out because unused and thus throwing an error in linter. - // const selectedCalendars = await prisma.selectedCalendar.findMany({ - // where: { - // userId: currentUser.id, - // }, - // }); + const selectedCalendars = await prisma.selectedCalendar.findMany({ + where: { + userId: currentUser.id, + }, + }); + // Split credentials up into calendar credentials and video credentials let calendarCredentials = currentUser.credentials.filter((cred) => cred.type.endsWith("_calendar")); let videoCredentials = currentUser.credentials.filter((cred) => cred.type.endsWith("_video")); - // Commented out because unused and thus throwing an error in linter. - // const hasCalendarIntegrations = - // currentUser.credentials.filter((cred) => cred.type.endsWith("_calendar")).length > 0; - // const hasVideoIntegrations = - // currentUser.credentials.filter((cred) => cred.type.endsWith("_video")).length > 0; + const hasCalendarIntegrations = + currentUser.credentials.filter((cred) => cred.type.endsWith("_calendar")).length > 0; + const hasVideoIntegrations = + currentUser.credentials.filter((cred) => cred.type.endsWith("_video")).length > 0; - // Commented out because unused and thus throwing an error in linter. - // const calendarAvailability = await getBusyCalendarTimes( - // currentUser.credentials, - // dayjs(req.body.start).startOf("day").utc().format(), - // dayjs(req.body.end).endOf("day").utc().format(), - // selectedCalendars - // ); - // const videoAvailability = await getBusyVideoTimes( - // currentUser.credentials, - // dayjs(req.body.start).startOf("day").utc().format(), - // dayjs(req.body.end).endOf("day").utc().format() - // ); - // let commonAvailability = []; + const calendarAvailability = await getBusyCalendarTimes( + currentUser.credentials, + dayjs(req.body.start).startOf("day").utc().format(), + dayjs(req.body.end).endOf("day").utc().format(), + selectedCalendars + ); + const videoAvailability = await getBusyVideoTimes( + currentUser.credentials, + dayjs(req.body.start).startOf("day").utc().format(), + dayjs(req.body.end).endOf("day").utc().format() + ); + let commonAvailability = []; - // Commented out because unused and thus throwing an error in linter. - // if (hasCalendarIntegrations && hasVideoIntegrations) { - // commonAvailability = calendarAvailability.filter((availability) => - // videoAvailability.includes(availability) - // ); - // } else if (hasVideoIntegrations) { - // commonAvailability = videoAvailability; - // } else if (hasCalendarIntegrations) { - // commonAvailability = calendarAvailability; - // } + if (hasCalendarIntegrations && hasVideoIntegrations) { + commonAvailability = calendarAvailability.filter((availability) => + videoAvailability.includes(availability) + ); + } else if (hasVideoIntegrations) { + commonAvailability = videoAvailability; + } else if (hasCalendarIntegrations) { + commonAvailability = calendarAvailability; + } // Now, get the newly stored credentials (new refresh token for example). currentUser = await prisma.user.findFirst({ @@ -201,8 +200,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }, }); - // TODO isAvailable was throwing an error - const isAvailableToBeBooked = true; //isAvailable(commonAvailability, req.body.start, selectedEventType.length); + let isAvailableToBeBooked = true; + + try { + isAvailableToBeBooked = isAvailable(commonAvailability, req.body.start, selectedEventType.length); + } catch { + console.debug({ + message: "Unable set isAvailableToBeBooked. Using true. ", + }); + } if (!isAvailableToBeBooked) { return res.status(400).json({ message: `${currentUser.name} is unavailable at this time.` });