From dff08dc5b939a57ddeea8a1f4902a3c8b8ae0a00 Mon Sep 17 00:00:00 2001 From: alannnc Date: Sat, 5 Nov 2022 11:58:35 -0700 Subject: [PATCH] fix/google-calendar-removes-attendees-when-seats-5336 (#5366) * Adding missing code that copied booking attendees to event attendees * Clean up code * Fix types * Fix types * Doing deep clone for sending seats email --- .../v2/eventtype/EventAdvancedTab.tsx | 7 ++-- .../googlecalendar/lib/CalendarService.ts | 9 +++-- packages/core/EventManager.ts | 38 +++++++++++++------ .../features/bookings/lib/handleNewBooking.ts | 23 ++++++++--- .../components/TeamAvailabilityScreen.tsx | 2 +- .../components/v2/TeamAvailabilityScreen.tsx | 2 +- .../components/NewScheduleButton.tsx | 2 +- .../features/webhooks/pages/webhooks-view.tsx | 2 +- packages/ui/v2/core/apps/AppCard.tsx | 2 +- 9 files changed, 57 insertions(+), 30 deletions(-) diff --git a/apps/web/components/v2/eventtype/EventAdvancedTab.tsx b/apps/web/components/v2/eventtype/EventAdvancedTab.tsx index a6c882cac0..d73716c243 100644 --- a/apps/web/components/v2/eventtype/EventAdvancedTab.tsx +++ b/apps/web/components/v2/eventtype/EventAdvancedTab.tsx @@ -54,7 +54,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick {/** * Only display calendar selector if user has connected calendars AND if it's not - * a team event. Since we don't have logic to handle each attende calendar (for now). + * a team event. Since we don't have logic to handle each attendee calendar (for now). * This will fallback to each user selected destination calendar. */} {!!connectedCalendarsQuery.data?.connectedCalendars.length && !team && ( @@ -292,7 +292,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick { - // Enabling seats will disable guests and requiring confimation until fully supported + // Enabling seats will disable guests and requiring confirmation until fully supported if (e) { formMethods.setValue("disableGuests", true); formMethods.setValue("requiresConfirmation", false); @@ -316,9 +316,10 @@ export const EventAdvancedTab = ({ eventType, team }: Pick{t("seats")}} onChange={(e) => { - onChange(Number(e.target.value)); + onChange(Math.abs(Number(e.target.value))); }} />
diff --git a/packages/app-store/googlecalendar/lib/CalendarService.ts b/packages/app-store/googlecalendar/lib/CalendarService.ts index f6f8a1316d..1b81ca52f7 100644 --- a/packages/app-store/googlecalendar/lib/CalendarService.ts +++ b/packages/app-store/googlecalendar/lib/CalendarService.ts @@ -160,6 +160,10 @@ export default class GoogleCalendarService implements Calendar { async updateEvent(uid: string, event: CalendarEvent, externalCalendarId: string): Promise { return new Promise(async (resolve, reject) => { const myGoogleAuth = await this.auth.getToken(); + const eventAttendees = event.attendees.map(({ id, ...rest }) => ({ + ...rest, + responseStatus: "accepted", + })); const payload: calendar_v3.Schema$Event = { summary: event.title, description: getRichDescription(event), @@ -179,10 +183,7 @@ export default class GoogleCalendarService implements Calendar { responseStatus: "accepted", }, // eslint-disable-next-line - ...event.attendees.map(({ id, ...rest }) => ({ - ...rest, - responseStatus: "accepted", - })), + ...eventAttendees, ], reminders: { useDefault: true, diff --git a/packages/core/EventManager.ts b/packages/core/EventManager.ts index 67f5c5858b..8479718d94 100644 --- a/packages/core/EventManager.ts +++ b/packages/core/EventManager.ts @@ -7,8 +7,14 @@ import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApi import { getEventLocationTypeFromApp } from "@calcom/app-store/locations"; import getApps from "@calcom/app-store/utils"; import prisma from "@calcom/prisma"; +import { Attendee } from "@calcom/prisma/client"; import { createdEventSchema } from "@calcom/prisma/zod-utils"; -import type { AdditionalInformation, CalendarEvent, NewCalendarEventType } from "@calcom/types/Calendar"; +import type { + AdditionalInformation, + CalendarEvent, + NewCalendarEventType, + Person, +} from "@calcom/types/Calendar"; import { CredentialPayload, CredentialWithAppName } from "@calcom/types/Credential"; import type { Event } from "@calcom/types/Event"; import type { @@ -294,6 +300,7 @@ export default class EventManager { } public async updateCalendarAttendees(event: CalendarEvent, booking: PartialBooking) { + // @NOTE: This function is only used for updating attendees on a calendar event. Can we remove this? await this.updateAllCalendarEvents(event, booking); } @@ -430,11 +437,14 @@ export default class EventManager { try { // Bookings should only have one calendar reference calendarReference = booking.references.filter((reference) => reference.type.includes("_calendar"))[0]; - if (!calendarReference) throw new Error("bookingRef"); - + if (!calendarReference) { + throw new Error("bookingRef"); + } const { uid: bookingRefUid, externalCalendarId: bookingExternalCalendarId } = calendarReference; - if (!bookingExternalCalendarId) throw new Error("externalCalendarId"); + if (!bookingExternalCalendarId) { + throw new Error("externalCalendarId"); + } let result = []; if (calendarReference.credentialId) { @@ -458,13 +468,15 @@ export default class EventManager { .map(async (cred) => { const calendarReference = booking.references.find((ref) => ref.type === cred.type); if (!calendarReference) - return { - appName: cred.appName, - type: cred.type, - success: false, - uid: "", - originalEvent: event, - }; + if (!calendarReference) { + return { + appName: cred.appName, + type: cred.type, + success: false, + uid: "", + originalEvent: event, + }; + } const { externalCalendarId: bookingExternalCalendarId, meetingId: bookingRefUid } = calendarReference; return await updateEvent(cred, event, bookingRefUid ?? null, bookingExternalCalendarId ?? null); @@ -474,7 +486,9 @@ export default class EventManager { return Promise.all(result); } catch (error) { let message = `Tried to 'updateAllCalendarEvents' but there was no '{thing}' for '${credential?.type}', userId: '${credential?.userId}', bookingId: '${booking?.id}'`; - if (error instanceof Error) message = message.replace("{thing}", error.message); + if (error instanceof Error) { + message = message.replace("{thing}", error.message); + } console.error(message); return Promise.resolve([ { diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 80d477957f..d0c0fa4262 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -7,6 +7,7 @@ import { WebhookTriggerEvents, } from "@prisma/client"; import async from "async"; +import { cloneDeep } from "lodash"; import type { NextApiRequest } from "next"; import { RRule } from "rrule"; import short from "short-uuid"; @@ -482,8 +483,9 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) { // For seats, if the booking already exists then we want to add the new attendee to the existing booking if (reqBody.bookingUid) { - if (!eventType.seatsPerTimeSlot) + if (!eventType.seatsPerTimeSlot) { throw new HttpError({ statusCode: 404, message: "Event type does not have seats" }); + } const booking = await prisma.booking.findUnique({ where: { @@ -505,7 +507,9 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) { }, }, }); - if (!booking) throw new HttpError({ statusCode: 404, message: "Booking not found" }); + if (!booking) { + throw new HttpError({ statusCode: 404, message: "Booking not found" }); + } // Need to add translation for attendees to pass type checks. Since these values are never written to the db we can just use the new attendee language const bookingAttendees = booking.attendees.map((attendee) => { @@ -514,11 +518,13 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) { evt = { ...evt, attendees: [...bookingAttendees, invitee[0]] }; - if (eventType.seatsPerTimeSlot <= booking.attendees.length) + if (eventType.seatsPerTimeSlot <= booking.attendees.length) { throw new HttpError({ statusCode: 409, message: "Booking seats are full" }); + } - if (booking.attendees.some((attendee) => attendee.email === invitee[0].email)) + if (booking.attendees.find((attendee) => attendee.email === invitee[0].email)) { throw new HttpError({ statusCode: 409, message: "Already signed up for time slot" }); + } await prisma.booking.update({ where: { @@ -537,8 +543,13 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) { }); const newSeat = booking.attendees.length !== 0; - - await sendScheduledSeatsEmails(evt, invitee[0], newSeat, !!eventType.seatsShowAttendees); + /** + * Remember objects are passed into functions as references + * so if you modify it in a inner function it will be modified in the outer function + * deep cloning evt to avoid this + */ + const copyEvent = cloneDeep(evt); + await sendScheduledSeatsEmails(copyEvent, invitee[0], newSeat, !!eventType.seatsShowAttendees); const credentials = await refreshCredentials(organizerUser.credentials); const eventManager = new EventManager({ ...organizerUser, credentials }); diff --git a/packages/features/ee/teams/components/TeamAvailabilityScreen.tsx b/packages/features/ee/teams/components/TeamAvailabilityScreen.tsx index c5e7f70bbe..ea75cd1f5e 100644 --- a/packages/features/ee/teams/components/TeamAvailabilityScreen.tsx +++ b/packages/features/ee/teams/components/TeamAvailabilityScreen.tsx @@ -5,9 +5,9 @@ import { FixedSizeList as List } from "react-window"; import dayjs from "@calcom/dayjs"; import { CAL_URL } from "@calcom/lib/constants"; import { inferQueryOutput, trpc } from "@calcom/trpc/react"; +import { Avatar } from "@calcom/ui/components/avatar"; import Select from "@calcom/ui/form/Select"; import TimezoneSelect, { ITimezone } from "@calcom/ui/form/TimezoneSelect"; -import { Avatar } from "@calcom/ui/components/avatar"; import DatePicker from "@calcom/ui/v2/core/form/DatePicker"; import TeamAvailabilityTimes from "./TeamAvailabilityTimes"; diff --git a/packages/features/ee/teams/components/v2/TeamAvailabilityScreen.tsx b/packages/features/ee/teams/components/v2/TeamAvailabilityScreen.tsx index c5e7f70bbe..ea75cd1f5e 100644 --- a/packages/features/ee/teams/components/v2/TeamAvailabilityScreen.tsx +++ b/packages/features/ee/teams/components/v2/TeamAvailabilityScreen.tsx @@ -5,9 +5,9 @@ import { FixedSizeList as List } from "react-window"; import dayjs from "@calcom/dayjs"; import { CAL_URL } from "@calcom/lib/constants"; import { inferQueryOutput, trpc } from "@calcom/trpc/react"; +import { Avatar } from "@calcom/ui/components/avatar"; import Select from "@calcom/ui/form/Select"; import TimezoneSelect, { ITimezone } from "@calcom/ui/form/TimezoneSelect"; -import { Avatar } from "@calcom/ui/components/avatar"; import DatePicker from "@calcom/ui/v2/core/form/DatePicker"; import TeamAvailabilityTimes from "./TeamAvailabilityTimes"; diff --git a/packages/features/schedules/components/NewScheduleButton.tsx b/packages/features/schedules/components/NewScheduleButton.tsx index c7ef6c6152..c6df6cff18 100644 --- a/packages/features/schedules/components/NewScheduleButton.tsx +++ b/packages/features/schedules/components/NewScheduleButton.tsx @@ -6,8 +6,8 @@ import { HttpError } from "@calcom/lib/http-error"; import { trpc } from "@calcom/trpc/react"; import { Dialog, DialogClose, DialogContent, DialogTrigger } from "@calcom/ui/Dialog"; import { Icon } from "@calcom/ui/Icon"; -import { Form } from "@calcom/ui/form/fields"; import { Button } from "@calcom/ui/components/button"; +import { Form } from "@calcom/ui/form/fields"; import showToast from "@calcom/ui/v2/core/notifications"; export function NewScheduleButton({ name = "new-schedule" }: { name?: string }) { diff --git a/packages/features/webhooks/pages/webhooks-view.tsx b/packages/features/webhooks/pages/webhooks-view.tsx index 96a7ef378c..afcfdff33d 100644 --- a/packages/features/webhooks/pages/webhooks-view.tsx +++ b/packages/features/webhooks/pages/webhooks-view.tsx @@ -5,8 +5,8 @@ import { WEBAPP_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; import { Icon } from "@calcom/ui"; -import { EmptyScreen, SkeletonText } from "@calcom/ui/v2"; import { Button } from "@calcom/ui/components/button"; +import { EmptyScreen, SkeletonText } from "@calcom/ui/v2"; import Meta from "@calcom/ui/v2/core/Meta"; import { getLayout } from "@calcom/ui/v2/core/layouts/SettingsLayout"; diff --git a/packages/ui/v2/core/apps/AppCard.tsx b/packages/ui/v2/core/apps/AppCard.tsx index 903251d456..7594f3571d 100644 --- a/packages/ui/v2/core/apps/AppCard.tsx +++ b/packages/ui/v2/core/apps/AppCard.tsx @@ -5,8 +5,8 @@ import { InstallAppButton } from "@calcom/app-store/components"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { App } from "@calcom/types/App"; import { Icon } from "@calcom/ui/Icon"; -import { showToast } from "@calcom/ui/v2"; import { Button } from "@calcom/ui/components/button"; +import { showToast } from "@calcom/ui/v2"; interface AppCardProps { app: App;