diff --git a/apps/ai/package.json b/apps/ai/package.json index 45931e9a31..6a5de24ec4 100644 --- a/apps/ai/package.json +++ b/apps/ai/package.json @@ -9,12 +9,11 @@ "langchain": "^0.0.131", "mailparser": "^3.6.5", "next": "^13.4.6", - "zod": "^3.20.2" + "supports-color": "8.1.1", + "zod": "^3.22.2" }, "devDependencies": { - "@types/mailparser": "^3.4.0", - "@types/node": "^20.5.1", - "typescript": "^4.9.4" + "@types/mailparser": "^3.4.0" }, "scripts": { "build": "next build", diff --git a/apps/ai/src/env.mjs b/apps/ai/src/env.mjs index ba482f6bea..6081733698 100644 --- a/apps/ai/src/env.mjs +++ b/apps/ai/src/env.mjs @@ -23,6 +23,7 @@ export const env = createEnv({ NODE_ENV: process.env.NODE_ENV, OPENAI_API_KEY: process.env.OPENAI_API_KEY, SENDGRID_API_KEY: process.env.SENDGRID_API_KEY, + DATABASE_URL: process.env.DATABASE_URL, }, /** @@ -37,5 +38,6 @@ export const env = createEnv({ NODE_ENV: z.enum(["development", "test", "production"]), OPENAI_API_KEY: z.string().min(1), SENDGRID_API_KEY: z.string().min(1), + DATABASE_URL: z.string().url(), }, }); diff --git a/apps/api/package.json b/apps/api/package.json index 9ddd6e1f44..2fee124d19 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -40,6 +40,6 @@ "typescript": "^4.9.4", "tzdata": "^1.0.30", "uuid": "^8.3.2", - "zod": "^3.20.2" + "zod": "^3.22.2" } } diff --git a/apps/storybook/styles/storybook-styles.css b/apps/storybook/styles/storybook-styles.css index 2f188d1668..8893aebc15 100644 --- a/apps/storybook/styles/storybook-styles.css +++ b/apps/storybook/styles/storybook-styles.css @@ -248,10 +248,10 @@ --cal-bg-inverted: #f3f4f6; /* background -> components*/ - --cal-bg-info: #dee9fc; - --cal-bg-success: #e2fbe8; - --cal-bg-attention: #fceed8; - --cal-bg-error: #f9e3e2; + --cal-bg-info: #263fa9; + --cal-bg-success: #306339; + --cal-bg-attention: #8e3b1f; + --cal-bg-error: #8c2822; --cal-bg-dark-error: #752522; /* Borders */ @@ -269,10 +269,10 @@ --cal-text-inverted: #101010; /* Content/Text -> components */ - --cal-text-info: #253985; - --cal-text-success: #285231; - --cal-text-attention: #73321b; - --cal-text-error: #752522; + --cal-text-info: #dee9fc; + --cal-text-success: #e2fbe8; + --cal-text-attention: #fceed8; + --cal-text-error: #f9e3e2; /* Brand shenanigans -> These will be computed for the users theme at runtime. diff --git a/apps/web/next.config.js b/apps/web/next.config.js index df25515720..1858a06b46 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -14,7 +14,8 @@ const { if (!process.env.NEXTAUTH_SECRET) throw new Error("Please set NEXTAUTH_SECRET"); if (!process.env.CALENDSO_ENCRYPTION_KEY) throw new Error("Please set CALENDSO_ENCRYPTION_KEY"); - +const isOrganizationsEnabled = + process.env.ORGANIZATIONS_ENABLED === "1" || process.env.ORGANIZATIONS_ENABLED === "true"; // To be able to use the version in the app without having to import package.json process.env.NEXT_PUBLIC_CALCOM_VERSION = version; @@ -226,7 +227,7 @@ const nextConfig = { async rewrites() { const beforeFiles = [ // These rewrites are other than booking pages rewrites and so that they aren't redirected to org pages ensure that they happen in beforeFiles - ...(process.env.ORGANIZATIONS_ENABLED + ...(isOrganizationsEnabled ? [ { ...matcherConfigRootPath, @@ -333,44 +334,46 @@ const nextConfig = { }, ], }, - ...[ - { - ...matcherConfigRootPath, - headers: [ + ...(isOrganizationsEnabled + ? [ { - key: "X-Cal-Org-path", - value: "/team/:orgSlug", + ...matcherConfigRootPath, + headers: [ + { + key: "X-Cal-Org-path", + value: "/team/:orgSlug", + }, + ], }, - ], - }, - { - ...matcherConfigUserRoute, - headers: [ { - key: "X-Cal-Org-path", - value: "/org/:orgSlug/:user", + ...matcherConfigUserRoute, + headers: [ + { + key: "X-Cal-Org-path", + value: "/org/:orgSlug/:user", + }, + ], }, - ], - }, - { - ...matcherConfigUserTypeRoute, - headers: [ { - key: "X-Cal-Org-path", - value: "/org/:orgSlug/:user/:type", + ...matcherConfigUserTypeRoute, + headers: [ + { + key: "X-Cal-Org-path", + value: "/org/:orgSlug/:user/:type", + }, + ], }, - ], - }, - { - ...matcherConfigUserTypeEmbedRoute, - headers: [ { - key: "X-Cal-Org-path", - value: "/org/:orgSlug/:user/:type/embed", + ...matcherConfigUserTypeEmbedRoute, + headers: [ + { + key: "X-Cal-Org-path", + value: "/org/:orgSlug/:user/:type/embed", + }, + ], }, - ], - }, - ], + ] + : []), ]; }, async redirects() { @@ -447,6 +450,13 @@ const nextConfig = { }, { source: "/support", + missing: [ + { + type: "header", + key: "host", + value: orgHostPath, + }, + ], destination: "/event-types?openIntercom=true", permanent: true, }, @@ -463,7 +473,7 @@ const nextConfig = { // OAuth callbacks when sent to localhost:3000(w would be expected) should be redirected to corresponding to WEBAPP_URL ...(process.env.NODE_ENV === "development" && // Safer to enable the redirect only when the user is opting to test out organizations - process.env.ORGANIZATIONS_ENABLED && + isOrganizationsEnabled && // Prevent infinite redirect by checking that we aren't already on localhost process.env.NEXT_PUBLIC_WEBAPP_URL !== "http://localhost:3000" ? [ diff --git a/apps/web/package.json b/apps/web/package.json index fc39783df1..992551c296 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@calcom/web", - "version": "3.2.6", + "version": "3.2.7.1", "private": true, "scripts": { "analyze": "ANALYZE=true next build", @@ -129,7 +129,7 @@ "turndown": "^7.1.1", "uuid": "^8.3.2", "web3": "^1.7.5", - "zod": "^3.20.2" + "zod": "^3.22.2" }, "devDependencies": { "@babel/core": "^7.19.6", diff --git a/apps/web/pages/api/cron/bookingReminder.ts b/apps/web/pages/api/cron/bookingReminder.ts index ab511b5a42..571b4850dd 100644 --- a/apps/web/pages/api/cron/bookingReminder.ts +++ b/apps/web/pages/api/cron/bookingReminder.ts @@ -104,7 +104,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }); const attendeesList = await Promise.all(attendeesListPromises); - + const selectedDestinationCalendar = booking.destinationCalendar || user.destinationCalendar; const evt: CalendarEvent = { type: booking.title, title: booking.title, @@ -127,7 +127,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) attendees: attendeesList, uid: booking.uid, recurringEvent: parseRecurringEvent(booking.eventType?.recurringEvent), - destinationCalendar: booking.destinationCalendar || user.destinationCalendar, + destinationCalendar: selectedDestinationCalendar ? [selectedDestinationCalendar] : [], }; await sendOrganizerRequestReminderEmail(evt); diff --git a/apps/web/pages/availability/[schedule].tsx b/apps/web/pages/availability/[schedule].tsx index 2999798088..d4dd83c381 100644 --- a/apps/web/pages/availability/[schedule].tsx +++ b/apps/web/pages/availability/[schedule].tsx @@ -6,6 +6,7 @@ import dayjs from "@calcom/dayjs"; import { DateOverrideInputDialog, DateOverrideList } from "@calcom/features/schedules"; import Schedule from "@calcom/features/schedules/components/Schedule"; import Shell from "@calcom/features/shell/Shell"; +import { classNames } from "@calcom/lib"; import { availabilityAsString } from "@calcom/lib/availability"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { HttpError } from "@calcom/lib/http-error"; @@ -17,11 +18,6 @@ import { ConfirmationDialogContent, Dialog, DialogTrigger, - Dropdown, - DropdownItem, - DropdownMenuContent, - DropdownMenuSeparator, - DropdownMenuTrigger, Form, Label, showToast, @@ -32,7 +28,7 @@ import { Tooltip, VerticalDivider, } from "@calcom/ui"; -import { Info, MoreHorizontal, Plus, Trash } from "@calcom/ui/components/icon"; +import { Info, MoreVertical, ArrowLeft, Plus, Trash } from "@calcom/ui/components/icon"; import PageWrapper from "@components/PageWrapper"; import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader"; @@ -95,7 +91,7 @@ export default function Availability() { const scheduleId = searchParams?.get("schedule") ? Number(searchParams.get("schedule")) : -1; const fromEventType = searchParams?.get("fromEventType"); const { timeFormat } = me.data || { timeFormat: null }; - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [openSidebar, setOpenSidebar] = useState(false); const { data: schedule, isLoading } = trpc.viewer.availability.schedule.get.useQuery( { scheduleId }, { @@ -225,33 +221,60 @@ export default function Availability() { - - - + + moveEventType(index, -1)} arrowDirection="up" /> )} {!(lastItem && lastItem.id === eventType.id) && ( - + moveEventType(index, 1)} arrowDirection="down" /> )}
@@ -887,7 +878,7 @@ const Main = ({ {isMobile ? ( ) : ( -
+
${subdomainRegExp})\\..*`; +exports.orgHostPath = `^(?${subdomainRegExp})\\.(?!vercel\.app).*`; let beforeRewriteExcludePages = pages.concat(otherNonExistingRoutePrefixes); exports.orgUserRoutePath = `/:user((?!${beforeRewriteExcludePages.join("|")}|_next|public)[a-zA-Z0-9\-_]+)`; diff --git a/apps/web/playwright/booking-pages.e2e.ts b/apps/web/playwright/booking-pages.e2e.ts index 3ca81bc2c7..9ebd29bb2d 100644 --- a/apps/web/playwright/booking-pages.e2e.ts +++ b/apps/web/playwright/booking-pages.e2e.ts @@ -268,3 +268,60 @@ test.describe("prefill", () => { }); }); }); + +test.describe("Booking on different layouts", () => { + test.beforeEach(async ({ page, users }) => { + const user = await users.create(); + await page.goto(`/${user.username}`); + }); + + test("Book on week layout", async ({ page }) => { + // Click first event type + await page.click('[data-testid="event-type-link"]'); + + await page.click('[data-testid="toggle-group-item-week_view"]'); + + await page.click('[data-testid="incrementMonth"]'); + + await page.locator('[data-testid="calendar-empty-cell"]').nth(0).click(); + + // Fill what is this meeting about? name email and notes + await page.locator('[name="name"]').fill("Test name"); + await page.locator('[name="email"]').fill(`${randomString(4)}@example.com`); + await page.locator('[name="notes"]').fill("Test notes"); + + await page.click('[data-testid="confirm-book-button"]'); + + await page.waitForURL((url) => { + return url.pathname.startsWith("/booking"); + }); + + // expect page to be booking page + await expect(page.locator("[data-testid=success-page]")).toBeVisible(); + }); + + test("Book on column layout", async ({ page }) => { + // Click first event type + await page.click('[data-testid="event-type-link"]'); + + await page.click('[data-testid="toggle-group-item-column_view"]'); + + await page.click('[data-testid="incrementMonth"]'); + + await page.locator('[data-testid="time"]').nth(0).click(); + + // Fill what is this meeting about? name email and notes + await page.locator('[name="name"]').fill("Test name"); + await page.locator('[name="email"]').fill(`${randomString(4)}@example.com`); + await page.locator('[name="notes"]').fill("Test notes"); + + await page.click('[data-testid="confirm-book-button"]'); + + await page.waitForURL((url) => { + return url.pathname.startsWith("/booking"); + }); + + // expect page to be booking page + await expect(page.locator("[data-testid=success-page]")).toBeVisible(); + }); +}); diff --git a/apps/web/playwright/webhook.e2e.ts b/apps/web/playwright/webhook.e2e.ts index a157578d5e..a56f5a425b 100644 --- a/apps/web/playwright/webhook.e2e.ts +++ b/apps/web/playwright/webhook.e2e.ts @@ -246,7 +246,7 @@ test.describe("BOOKING_REJECTED", async () => { }, ], location: "[redacted/dynamic]", - destinationCalendar: null, + destinationCalendar: [], // hideCalendarNotes: false, requiresConfirmation: "[redacted/dynamic]", eventTypeId: "[redacted/dynamic]", diff --git a/apps/web/public/static/locales/de/common.json b/apps/web/public/static/locales/de/common.json index 495255112c..8c645173ef 100644 --- a/apps/web/public/static/locales/de/common.json +++ b/apps/web/public/static/locales/de/common.json @@ -529,7 +529,7 @@ "location": "Ort", "address": "Adresse", "enter_address": "Geben Sie eine Adresse ein", - "in_person_attendee_address": "In Person (Adresse von Ihnen)", + "in_person_attendee_address": "Vor Ort (Adresse von Ihnen)", "yes": "Ja", "no": "Nein", "additional_notes": "Zusätzliche Notizen", @@ -539,7 +539,7 @@ "booking_confirmation": "Bestätigen Sie {{eventTypeTitle}} mit {{profileName}}", "booking_reschedule_confirmation": "Planen Sie Ihr {{eventTypeTitle}} mit {{profileName}} um", "in_person_meeting": "Vor-Ort-Termin", - "in_person": "Persönlich (Organisator-Adresse)", + "in_person": "Vor Ort (Organisator-Adresse)", "link_meeting": "Termin verknüpfen", "phone_number": "Telefonnummer", "attendee_phone_number": "Telefonnummer", diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 31489764ea..de68506adf 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -266,6 +266,7 @@ "nearly_there_instructions": "Last thing, a brief description about you and a photo really helps you get bookings and let people know who they’re booking with.", "set_availability_instructions": "Define ranges of time when you are available on a recurring basis. You can create more of these later and assign them to different calendars.", "set_availability": "Set your availability", + "availability_settings": "Availability Settings", "continue_without_calendar": "Continue without calendar", "connect_your_calendar": "Connect your calendar", "connect_your_video_app": "Connect your video apps", @@ -1107,6 +1108,7 @@ "email_attendee_action": "send email to attendees", "sms_attendee_action": "Send SMS to attendee", "sms_number_action": "send SMS to a specific number", + "send_reminder_sms": "Easily send meeting reminders via SMS to your attendees", "whatsapp_number_action": "send WhatsApp message to a specific number", "whatsapp_attendee_action": "send WhatsApp message to attendee", "workflows": "Workflows", diff --git a/apps/web/public/static/locales/fr/common.json b/apps/web/public/static/locales/fr/common.json index 8aa634a6a7..ddb90d612d 100644 --- a/apps/web/public/static/locales/fr/common.json +++ b/apps/web/public/static/locales/fr/common.json @@ -266,6 +266,7 @@ "nearly_there_instructions": "Pour finir, une brève description de vous et une photo vous aideront vraiment à obtenir des réservations et à faire savoir aux gens avec qui ils prennent rendez-vous.", "set_availability_instructions": "Définissez des plages de temps pendant lesquelles vous êtes disponible de manière récurrente. Vous pourrez en créer d'autres ultérieurement et les assigner à différents calendriers.", "set_availability": "Définissez vos disponibilités", + "availability_settings": "Paramètres de disponibilité", "continue_without_calendar": "Continuer sans calendrier", "connect_your_calendar": "Connectez votre calendrier", "connect_your_video_app": "Connectez vos applications vidéo", @@ -1107,6 +1108,7 @@ "email_attendee_action": "envoyer un e-mail aux participants", "sms_attendee_action": "Envoyer un SMS au participant", "sms_number_action": "envoyer un SMS à un numéro spécifique", + "send_reminder_sms": "Envoyez facilement des rappels de rendez-vous par SMS à vos participants", "whatsapp_number_action": "envoyer un message WhatsApp à un numéro spécifique", "whatsapp_attendee_action": "envoyer un message WhatsApp au participant", "workflows": "Workflows", diff --git a/apps/web/styles/globals.css b/apps/web/styles/globals.css index cd8dbcdb6b..0fc2b6eedf 100644 --- a/apps/web/styles/globals.css +++ b/apps/web/styles/globals.css @@ -58,10 +58,10 @@ --cal-bg-inverted: #f3f4f6; /* background -> components*/ - --cal-bg-info: #dee9fc; - --cal-bg-success: #e2fbe8; - --cal-bg-attention: #fceed8; - --cal-bg-error: #f9e3e2; + --cal-bg-info: #263fa9; + --cal-bg-success: #306339; + --cal-bg-attention: #8e3b1f; + --cal-bg-error: #8c2822; --cal-bg-dark-error: #752522; /* Borders */ @@ -80,10 +80,10 @@ --cal-text-inverted: #101010; /* Content/Text -> components */ - --cal-text-info: #253985; - --cal-text-success: #285231; - --cal-text-attention: #73321b; - --cal-text-error: #752522; + --cal-text-info: #dee9fc; + --cal-text-success: #e2fbe8; + --cal-text-attention: #fceed8; + --cal-text-error: #f9e3e2; /* Brand shenanigans -> These will be computed for the users theme at runtime. diff --git a/apps/web/test/lib/next-config.test.ts b/apps/web/test/lib/next-config.test.ts index 0237cdc0f4..efa875c56d 100644 --- a/apps/web/test/lib/next-config.test.ts +++ b/apps/web/test/lib/next-config.test.ts @@ -31,7 +31,7 @@ beforeAll(async () => { describe("next.config.js - Org Rewrite", () => { const orgHostRegExp = (subdomainRegExp: string) => // RegExp copied from pagesAndRewritePaths.js orgHostPath. Do make the change there as well. - new RegExp(`^(?${subdomainRegExp})\\..*`); + new RegExp(`^(?${subdomainRegExp})\\.(?!vercel\.app).*`); describe("Host matching based on NEXT_PUBLIC_WEBAPP_URL", () => { it("https://app.cal.com", () => { @@ -87,6 +87,11 @@ describe("next.config.js - Org Rewrite", () => { ?.orgSlug ).toEqual("some-other"); }); + it("Should ignore Vercel preview URLs", () => { + const subdomainRegExp = getSubdomainRegExp("https://cal-xxxxxxxx-cal.vercel.app"); + expect(orgHostRegExp(subdomainRegExp).exec("https://cal-xxxxxxxx-cal.vercel.app")).toMatchInlineSnapshot('null') + expect(orgHostRegExp(subdomainRegExp).exec("cal-xxxxxxxx-cal.vercel.app")).toMatchInlineSnapshot('null') + }); }); describe("Rewrite", () => { diff --git a/packages/app-store/googlecalendar/lib/CalendarService.ts b/packages/app-store/googlecalendar/lib/CalendarService.ts index b746233a43..755c7bcc40 100644 --- a/packages/app-store/googlecalendar/lib/CalendarService.ts +++ b/packages/app-store/googlecalendar/lib/CalendarService.ts @@ -84,7 +84,7 @@ export default class GoogleCalendarService implements Calendar { }; }; - async createEvent(calEventRaw: CalendarEvent): Promise { + async createEvent(calEventRaw: CalendarEvent, credentialId: number): Promise { const eventAttendees = calEventRaw.attendees.map(({ id: _id, ...rest }) => ({ ...rest, responseStatus: "accepted", @@ -97,6 +97,10 @@ export default class GoogleCalendarService implements Calendar { responseStatus: "accepted", })) || []; return new Promise(async (resolve, reject) => { + const [mainHostDestinationCalendar] = + calEventRaw?.destinationCalendar && calEventRaw?.destinationCalendar.length > 0 + ? calEventRaw.destinationCalendar + : []; const myGoogleAuth = await this.auth.getToken(); const payload: calendar_v3.Schema$Event = { summary: calEventRaw.title, @@ -115,8 +119,8 @@ export default class GoogleCalendarService implements Calendar { id: String(calEventRaw.organizer.id), responseStatus: "accepted", organizer: true, - email: calEventRaw.destinationCalendar?.externalId - ? calEventRaw.destinationCalendar.externalId + email: mainHostDestinationCalendar?.externalId + ? mainHostDestinationCalendar.externalId : calEventRaw.organizer.email, }, ...eventAttendees, @@ -138,13 +142,16 @@ export default class GoogleCalendarService implements Calendar { const calendar = google.calendar({ version: "v3", }); - const selectedCalendar = calEventRaw.destinationCalendar?.externalId - ? calEventRaw.destinationCalendar.externalId - : "primary"; + // Find in calEventRaw.destinationCalendar the one with the same credentialId + + const selectedCalendar = calEventRaw.destinationCalendar?.find( + (cal) => cal.credentialId === credentialId + )?.externalId; + calendar.events.insert( { auth: myGoogleAuth, - calendarId: selectedCalendar, + calendarId: selectedCalendar || "primary", requestBody: payload, conferenceDataVersion: 1, sendUpdates: "none", @@ -188,6 +195,8 @@ export default class GoogleCalendarService implements Calendar { async updateEvent(uid: string, event: CalendarEvent, externalCalendarId: string): Promise { return new Promise(async (resolve, reject) => { + const [mainHostDestinationCalendar] = + event?.destinationCalendar && event?.destinationCalendar.length > 0 ? event.destinationCalendar : []; const myGoogleAuth = await this.auth.getToken(); const eventAttendees = event.attendees.map(({ ...rest }) => ({ ...rest, @@ -216,8 +225,8 @@ export default class GoogleCalendarService implements Calendar { id: String(event.organizer.id), organizer: true, responseStatus: "accepted", - email: event.destinationCalendar?.externalId - ? event.destinationCalendar.externalId + email: mainHostDestinationCalendar?.externalId + ? mainHostDestinationCalendar.externalId : event.organizer.email, }, ...(eventAttendees as any), @@ -244,7 +253,7 @@ export default class GoogleCalendarService implements Calendar { const selectedCalendar = externalCalendarId ? externalCalendarId - : event.destinationCalendar?.externalId; + : event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId; calendar.events.update( { @@ -303,7 +312,9 @@ export default class GoogleCalendarService implements Calendar { }); const defaultCalendarId = "primary"; - const calendarId = externalCalendarId ? externalCalendarId : event.destinationCalendar?.externalId; + const calendarId = externalCalendarId + ? externalCalendarId + : event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId; calendar.events.delete( { diff --git a/packages/app-store/larkcalendar/lib/CalendarService.ts b/packages/app-store/larkcalendar/lib/CalendarService.ts index 61774563e1..f2979b5952 100644 --- a/packages/app-store/larkcalendar/lib/CalendarService.ts +++ b/packages/app-store/larkcalendar/lib/CalendarService.ts @@ -125,7 +125,8 @@ export default class LarkCalendarService implements Calendar { async createEvent(event: CalendarEvent): Promise { let eventId = ""; let eventRespData; - const calendarId = event.destinationCalendar?.externalId; + const [mainHostDestinationCalendar] = event.destinationCalendar ?? []; + const calendarId = mainHostDestinationCalendar?.externalId; if (!calendarId) { throw new Error("no calendar id"); } @@ -160,7 +161,8 @@ export default class LarkCalendarService implements Calendar { } private createAttendees = async (event: CalendarEvent, eventId: string) => { - const calendarId = event.destinationCalendar?.externalId; + const [mainHostDestinationCalendar] = event.destinationCalendar ?? []; + const calendarId = mainHostDestinationCalendar?.externalId; if (!calendarId) { this.log.error("no calendar id provided in createAttendees"); throw new Error("no calendar id provided in createAttendees"); @@ -187,7 +189,8 @@ export default class LarkCalendarService implements Calendar { async updateEvent(uid: string, event: CalendarEvent, externalCalendarId?: string) { const eventId = uid; let eventRespData; - const calendarId = externalCalendarId || event.destinationCalendar?.externalId; + const [mainHostDestinationCalendar] = event.destinationCalendar ?? []; + const calendarId = externalCalendarId || mainHostDestinationCalendar?.externalId; if (!calendarId) { this.log.error("no calendar id provided in updateEvent"); throw new Error("no calendar id provided in updateEvent"); @@ -231,7 +234,8 @@ export default class LarkCalendarService implements Calendar { * @returns */ async deleteEvent(uid: string, event: CalendarEvent, externalCalendarId?: string) { - const calendarId = externalCalendarId || event.destinationCalendar?.externalId; + const [mainHostDestinationCalendar] = event.destinationCalendar ?? []; + const calendarId = externalCalendarId || mainHostDestinationCalendar?.externalId; if (!calendarId) { this.log.error("no calendar id provided in deleteEvent"); throw new Error("no calendar id provided in deleteEvent"); diff --git a/packages/app-store/office365calendar/lib/CalendarService.ts b/packages/app-store/office365calendar/lib/CalendarService.ts index c4faa5f8ec..5283eed719 100644 --- a/packages/app-store/office365calendar/lib/CalendarService.ts +++ b/packages/app-store/office365calendar/lib/CalendarService.ts @@ -70,9 +70,10 @@ export default class Office365CalendarService implements Calendar { } async createEvent(event: CalendarEvent): Promise { + const [mainHostDestinationCalendar] = event.destinationCalendar ?? []; try { - const eventsUrl = event.destinationCalendar?.externalId - ? `/me/calendars/${event.destinationCalendar?.externalId}/events` + const eventsUrl = mainHostDestinationCalendar?.externalId + ? `/me/calendars/${mainHostDestinationCalendar?.externalId}/events` : "/me/calendar/events"; const response = await this.fetcher(eventsUrl, { diff --git a/packages/app-store/paypal/api/capture.ts b/packages/app-store/paypal/api/capture.ts index 14c577f14c..980a7ff315 100644 --- a/packages/app-store/paypal/api/capture.ts +++ b/packages/app-store/paypal/api/capture.ts @@ -4,7 +4,6 @@ import z from "zod"; import Paypal from "@calcom/app-store/paypal/lib/Paypal"; import { findPaymentCredentials } from "@calcom/features/ee/payments/api/paypal-webhook"; import { IS_PRODUCTION } from "@calcom/lib/constants"; -import { getErrorFromUnknown } from "@calcom/lib/errors"; import prisma from "@calcom/prisma"; export default async function handler(req: NextApiRequest, res: NextApiResponse) { @@ -78,12 +77,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } return; } catch (_err) { - const err = getErrorFromUnknown(_err); - - res.status(200).send({ - message: err.message, - stack: IS_PRODUCTION ? undefined : err.stack, - }); res.redirect(`/booking/${req.query.bookingUid}?paypalPaymentStatus=failed`); } } diff --git a/packages/app-store/paypal/lib/Paypal.ts b/packages/app-store/paypal/lib/Paypal.ts index 4645061887..b136299500 100644 --- a/packages/app-store/paypal/lib/Paypal.ts +++ b/packages/app-store/paypal/lib/Paypal.ts @@ -19,7 +19,7 @@ class Paypal { } private fetcher = async (endpoint: string, init?: RequestInit | undefined) => { - this.getAccessToken(); + await this.getAccessToken(); return fetch(`${this.url}${endpoint}`, { method: "get", ...init, @@ -173,7 +173,7 @@ class Paypal { } } catch (error) { console.error(error); - return false; + throw error; } return false; } diff --git a/packages/app-store/plausible/components/EventTypeAppCardInterface.tsx b/packages/app-store/plausible/components/EventTypeAppCardInterface.tsx index 2b24f7c4fb..fc176fc533 100644 --- a/packages/app-store/plausible/components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/plausible/components/EventTypeAppCardInterface.tsx @@ -41,25 +41,6 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ }} />
- { - setAppData("PLAUSIBLE_URL", e.target.value); - }} - /> - { - setAppData("trackingId", e.target.value); - }} - /> ); }; diff --git a/packages/app-store/routing-forms/package.json b/packages/app-store/routing-forms/package.json index 208f0385f0..95b864ae3a 100644 --- a/packages/app-store/routing-forms/package.json +++ b/packages/app-store/routing-forms/package.json @@ -7,7 +7,7 @@ "description": "It would allow a booker to connect with the right person or choose the right event, faster. It would work by taking inputs from the booker and using that data to route to the correct booker/event as configured by Cal user ", "dependencies": { "@calcom/lib": "*", - "dotenv": "^16.0.1", + "dotenv": "^16.3.1", "json-logic-js": "^2.0.2", "react-awesome-query-builder": "^5.1.2" }, diff --git a/packages/app-store/routing-forms/pages/forms/[...appPages].tsx b/packages/app-store/routing-forms/pages/forms/[...appPages].tsx index 9f7a7dca42..6c08123ad5 100644 --- a/packages/app-store/routing-forms/pages/forms/[...appPages].tsx +++ b/packages/app-store/routing-forms/pages/forms/[...appPages].tsx @@ -1,4 +1,5 @@ // TODO: i18n +import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useEffect } from "react"; import { useFormContext } from "react-hook-form"; @@ -30,6 +31,7 @@ import { List, ListLinkItem, Tooltip, + ArrowButton, } from "@calcom/ui"; import { BarChart, @@ -83,6 +85,20 @@ export default function RoutingForms({ const { hasPaidPlan } = useHasPaidPlan(); const routerQuery = useRouterQuery(); const hookForm = useFormContext(); + const utils = trpc.useContext(); + const [parent] = useAutoAnimate(); + + const mutation = trpc.viewer.routingFormOrder.useMutation({ + onError: async (err) => { + console.error(err.message); + await utils.viewer.appRoutingForms.forms.cancel(); + await utils.viewer.appRoutingForms.invalidate(); + }, + onSettled: () => { + utils.viewer.appRoutingForms.invalidate(); + }, + }); + useEffect(() => { hookForm.reset({}); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -128,6 +144,29 @@ export default function RoutingForms({ }, ]; + async function moveRoutingForm(index: number, increment: 1 | -1) { + const types = forms?.map((type) => { + return type.form; + }); + + if (types?.length) { + const newList = [...types]; + + const type = types[index]; + const tmp = types[index + increment]; + if (tmp) { + newList[index] = tmp; + newList[index + increment] = type; + } + + await utils.viewer.appRoutingForms.forms.cancel(); + + mutation.mutate({ + ids: newList?.map((type) => type.id), + }); + } + } + return (
- - {forms?.map(({ form, readOnly }) => { + + {forms?.map(({ form, readOnly }, index) => { if (!form) { return null; } @@ -187,116 +226,129 @@ export default function RoutingForms({ form.routes = form.routes || []; const fields = form.fields || []; const userRoutes = form.routes.filter((route) => !isFallbackRoute(route)); + const firstItem = forms[0].form; + const lastItem = forms[forms.length - 1].form; + return ( - - {form.team?.name && ( -
- - {form.team.name} - -
- )} - - - +
+ {!(firstItem && firstItem.id === form.id) && ( + moveRoutingForm(index, -1)} arrowDirection="up" /> + )} + + {!(lastItem && lastItem.id === form.id) && ( + moveRoutingForm(index, 1)} arrowDirection="down" /> + )} + + {form.team?.name && ( +
+ + {form.team.name} + +
+ )} + + + + + - - - - - {t("edit")} - - - {t("download_responses")} - - - {t("duplicate")} - - {typeformApp?.isInstalled ? ( + action="embed" + color="secondary" + variant="icon" + StartIcon={Code} + tooltip={t("embed")} + /> + - {t("Copy Typeform Redirect Url")} + className="!flex" + StartIcon={Edit}> + {t("edit")} - ) : null} - - {t("delete")} - - - - - }> -
- - {fields.length} {fields.length === 1 ? "field" : "fields"} - - - {userRoutes.length} {userRoutes.length === 1 ? "route" : "routes"} - - - {form._count.responses}{" "} - {form._count.responses === 1 ? "response" : "responses"} - -
-
+ + {t("download_responses")} + + + {t("duplicate")} + + {typeformApp?.isInstalled ? ( + + {t("Copy Typeform Redirect Url")} + + ) : null} + + {t("delete")} + + + + + }> +
+ + {fields.length} {fields.length === 1 ? "field" : "fields"} + + + {userRoutes.length} {userRoutes.length === 1 ? "route" : "routes"} + + + {form._count.responses}{" "} + {form._count.responses === 1 ? "response" : "responses"} + +
+ +
); })}
diff --git a/packages/app-store/routing-forms/playwright/tests/basic.e2e.ts b/packages/app-store/routing-forms/playwright/tests/basic.e2e.ts index aad31dc55b..5878906607 100644 --- a/packages/app-store/routing-forms/playwright/tests/basic.e2e.ts +++ b/packages/app-store/routing-forms/playwright/tests/basic.e2e.ts @@ -21,7 +21,7 @@ test.describe("Routing Forms", () => { await page.waitForSelector('[data-testid="routing-forms-list"]'); // Ensure that it's visible in forms list - expect(await page.locator('[data-testid="routing-forms-list"] > li').count()).toBe(1); + expect(await page.locator('[data-testid="routing-forms-list"] > div').count()).toBe(1); await gotoRoutingLink({ page, formId }); await expect(page.locator("text=Test Form Name")).toBeVisible(); diff --git a/packages/app-store/routing-forms/trpc/formMutation.handler.ts b/packages/app-store/routing-forms/trpc/formMutation.handler.ts index dffd2486bf..951a107638 100644 --- a/packages/app-store/routing-forms/trpc/formMutation.handler.ts +++ b/packages/app-store/routing-forms/trpc/formMutation.handler.ts @@ -59,6 +59,7 @@ export const formMutationHandler = async ({ ctx, input }: FormMutationHandlerOpt fields: true, settings: true, teamId: true, + position: true, }, }); diff --git a/packages/app-store/routing-forms/trpc/forms.handler.ts b/packages/app-store/routing-forms/trpc/forms.handler.ts index a01cdca8b0..2cff781e76 100644 --- a/packages/app-store/routing-forms/trpc/forms.handler.ts +++ b/packages/app-store/routing-forms/trpc/forms.handler.ts @@ -26,9 +26,14 @@ export const formsHandler = async ({ ctx, input }: FormsHandlerOptions) => { const forms = await prisma.app_RoutingForms_Form.findMany({ where, - orderBy: { - createdAt: "desc", - }, + orderBy: [ + { + position: "desc", + }, + { + createdAt: "asc", + }, + ], include: { team: { include: { diff --git a/packages/app-store/stripepayment/lib/PaymentService.ts b/packages/app-store/stripepayment/lib/PaymentService.ts index 6211e77504..de021a7bcb 100644 --- a/packages/app-store/stripepayment/lib/PaymentService.ts +++ b/packages/app-store/stripepayment/lib/PaymentService.ts @@ -50,7 +50,8 @@ export class PaymentService implements IAbstractPaymentService { payment: Pick, bookingId: Booking["id"], bookerEmail: string, - paymentOption: PaymentOption + paymentOption: PaymentOption, + eventTitle?: string ) { try { // Ensure that the payment service can support the passed payment option @@ -78,6 +79,12 @@ export class PaymentService implements IAbstractPaymentService { currency: this.credentials.default_currency, payment_method_types: ["card"], customer: customer.id, + metadata: { + identifier: "cal.com", + bookingId, + bookerEmail, + eventName: eventTitle || "", + }, }; const paymentIntent = await this.stripe.paymentIntents.create(params, { diff --git a/packages/app-store/stripepayment/package.json b/packages/app-store/stripepayment/package.json index 0b09e18e31..95c3e878e3 100644 --- a/packages/app-store/stripepayment/package.json +++ b/packages/app-store/stripepayment/package.json @@ -21,7 +21,7 @@ "@stripe/stripe-js": "^1.35.0", "stripe": "^9.16.0", "uuid": "^8.3.2", - "zod": "^3.20.2" + "zod": "^3.22.2" }, "devDependencies": { "@calcom/types": "*", diff --git a/packages/core/CalendarManager.ts b/packages/core/CalendarManager.ts index b256eb1cfc..e3a995dbe6 100644 --- a/packages/core/CalendarManager.ts +++ b/packages/core/CalendarManager.ts @@ -217,7 +217,8 @@ export const getBusyCalendarTimes = async ( export const createEvent = async ( credential: CredentialWithAppName, - calEvent: CalendarEvent + calEvent: CalendarEvent, + externalId?: string ): Promise> => { const uid: string = getUid(calEvent); const calendar = await getCalendar(credential); @@ -226,29 +227,31 @@ export const createEvent = async ( // Check if the disabledNotes flag is set to true if (calEvent.hideCalendarNotes) { - calEvent.additionalNotes = "Notes have been hidden by the organiser"; // TODO: i18n this string? + calEvent.additionalNotes = "Notes have been hidden by the organizer"; // TODO: i18n this string? } // TODO: Surface success/error messages coming from apps to improve end user visibility const creationResult = calendar - ? await calendar.createEvent(calEvent).catch(async (error: { code: number; calError: string }) => { - success = false; - /** - * There is a time when selectedCalendar externalId doesn't match witch certain credential - * so google returns 404. - * */ - if (error?.code === 404) { + ? await calendar + .createEvent(calEvent, credential.id) + .catch(async (error: { code: number; calError: string }) => { + success = false; + /** + * There is a time when selectedCalendar externalId doesn't match witch certain credential + * so google returns 404. + * */ + if (error?.code === 404) { + return undefined; + } + if (error?.calError) { + calError = error.calError; + } + log.error("createEvent failed", JSON.stringify(error), calEvent); + // @TODO: This code will be off till we can investigate an error with it + //https://github.com/calcom/cal.com/issues/3949 + // await sendBrokenIntegrationEmail(calEvent, "calendar"); return undefined; - } - if (error?.calError) { - calError = error.calError; - } - log.error("createEvent failed", JSON.stringify(error), calEvent); - // @TODO: This code will be off till we can investigate an error with it - //https://github.com/calcom/cal.com/issues/3949 - // await sendBrokenIntegrationEmail(calEvent, "calendar"); - return undefined; - }) + }) : undefined; return { @@ -261,6 +264,8 @@ export const createEvent = async ( originalEvent: calEvent, calError, calWarnings: creationResult?.additionalInfo?.calWarnings || [], + externalId, + credentialId: credential.id, }; }; diff --git a/packages/core/EventManager.ts b/packages/core/EventManager.ts index 72aa75eb68..cc8d5c55b9 100644 --- a/packages/core/EventManager.ts +++ b/packages/core/EventManager.ts @@ -114,7 +114,9 @@ export default class EventManager { } // Fallback to Cal Video if Google Meet is selected w/o a Google Cal - if (evt.location === MeetLocationType && evt.destinationCalendar?.integration !== "google_calendar") { + // @NOTE: destinationCalendar it's an array now so as a fallback we will only check the first one + const [mainHostDestinationCalendar] = evt.destinationCalendar ?? []; + if (evt.location === MeetLocationType && mainHostDestinationCalendar.integration !== "google_calendar") { evt["location"] = "integrations:daily"; } const isDedicated = evt.location ? isDedicatedIntegration(evt.location) : null; @@ -164,8 +166,8 @@ export default class EventManager { meetingId: createdEventObj ? createdEventObj.id : result.createdEvent?.id?.toString(), meetingPassword: createdEventObj ? createdEventObj.password : result.createdEvent?.password, meetingUrl: createdEventObj ? createdEventObj.onlineMeetingUrl : result.createdEvent?.url, - externalCalendarId: isCalendarType ? evt.destinationCalendar?.externalId : undefined, - credentialId: isCalendarType ? evt.destinationCalendar?.credentialId : result.credentialId, + externalCalendarId: isCalendarType ? result.externalId : undefined, + credentialId: isCalendarType ? result.credentialId : undefined, }; }); @@ -203,8 +205,8 @@ export default class EventManager { meetingId: result.createdEvent?.id?.toString(), meetingPassword: result.createdEvent?.password, meetingUrl: result.createdEvent?.url, - externalCalendarId: evt.destinationCalendar?.externalId, - credentialId: result.credentialId ?? evt.destinationCalendar?.credentialId, + externalCalendarId: result.externalId, + credentialId: result.credentialId ?? undefined, }; }); @@ -332,29 +334,52 @@ export default class EventManager { * @private */ private async createAllCalendarEvents(event: CalendarEvent) { - /** Can I use destinationCalendar here? */ - /* How can I link a DC to a cred? */ - let createdEvents: EventResult[] = []; - if (event.destinationCalendar) { - if (event.destinationCalendar.credentialId) { - const credential = this.calendarCredentials.find( - (c) => c.id === event.destinationCalendar?.credentialId - ); - - if (credential) { - const createdEvent = await createEvent(credential, event); - if (createdEvent) { - createdEvents.push(createdEvent); + if (event.destinationCalendar && event.destinationCalendar.length > 0) { + for (const destination of event.destinationCalendar) { + if (destination.credentialId) { + let credential = this.calendarCredentials.find((c) => c.id === destination.credentialId); + if (!credential) { + // Fetch credential from DB + const credentialFromDB = await prisma.credential.findUnique({ + include: { + app: { + select: { + slug: true, + }, + }, + }, + where: { + id: destination.credentialId, + }, + }); + if (credentialFromDB && credentialFromDB.app?.slug) { + credential = { + appName: credentialFromDB?.app.slug ?? "", + id: credentialFromDB.id, + type: credentialFromDB.type, + key: credentialFromDB.key, + userId: credentialFromDB.userId, + teamId: credentialFromDB.teamId, + invalid: credentialFromDB.invalid, + appId: credentialFromDB.appId, + }; + } } + if (credential) { + const createdEvent = await createEvent(credential, event, destination.externalId); + if (createdEvent) { + createdEvents.push(createdEvent); + } + } + } else { + const destinationCalendarCredentials = this.calendarCredentials.filter( + (c) => c.type === destination.integration + ); + createdEvents = createdEvents.concat( + await Promise.all(destinationCalendarCredentials.map(async (c) => await createEvent(c, event))) + ); } - } else { - const destinationCalendarCredentials = this.calendarCredentials.filter( - (c) => c.type === event.destinationCalendar?.integration - ); - createdEvents = createdEvents.concat( - await Promise.all(destinationCalendarCredentials.map(async (c) => await createEvent(c, event))) - ); } } else { /** @@ -451,7 +476,7 @@ export default class EventManager { booking: PartialBooking, newBookingId?: number ): Promise>> { - let calendarReference: PartialReference | undefined = undefined, + let calendarReference: PartialReference[] | undefined = undefined, credential; try { // If a newBookingId is given, update that calendar event @@ -468,33 +493,62 @@ export default class EventManager { } calendarReference = newBooking?.references.length - ? newBooking.references.find((reference) => reference.type.includes("_calendar")) - : booking.references.find((reference) => reference.type.includes("_calendar")); + ? newBooking.references.filter((reference) => reference.type.includes("_calendar")) + : booking.references.filter((reference) => reference.type.includes("_calendar")); - if (!calendarReference) { + if (calendarReference.length === 0) { return []; } - const { uid: bookingRefUid, externalCalendarId: bookingExternalCalendarId } = calendarReference; - let calenderExternalId: string | null = null; - if (bookingExternalCalendarId) { - calenderExternalId = bookingExternalCalendarId; - } - + // process all calendar references let result = []; - if (calendarReference.credentialId) { - credential = this.calendarCredentials.filter( - (credential) => credential.id === calendarReference?.credentialId - )[0]; - result.push(updateEvent(credential, event, bookingRefUid, calenderExternalId)); - } else { - const credentials = this.calendarCredentials.filter( - (credential) => credential.type === calendarReference?.type - ); - for (const credential of credentials) { + for (const reference of calendarReference) { + const { uid: bookingRefUid, externalCalendarId: bookingExternalCalendarId } = reference; + let calenderExternalId: string | null = null; + if (bookingExternalCalendarId) { + calenderExternalId = bookingExternalCalendarId; + } + + if (reference.credentialId) { + credential = this.calendarCredentials.filter( + (credential) => credential.id === reference?.credentialId + )[0]; + if (!credential) { + // Fetch credential from DB + const credentialFromDB = await prisma.credential.findUnique({ + include: { + app: { + select: { + slug: true, + }, + }, + }, + where: { + id: reference.credentialId, + }, + }); + if (credentialFromDB && credentialFromDB.app?.slug) { + credential = { + appName: credentialFromDB?.app.slug ?? "", + id: credentialFromDB.id, + type: credentialFromDB.type, + key: credentialFromDB.key, + userId: credentialFromDB.userId, + teamId: credentialFromDB.teamId, + invalid: credentialFromDB.invalid, + appId: credentialFromDB.appId, + }; + } + } result.push(updateEvent(credential, event, bookingRefUid, calenderExternalId)); + } else { + const credentials = this.calendarCredentials.filter( + (credential) => credential.type === reference?.type + ); + for (const credential of credentials) { + result.push(updateEvent(credential, event, bookingRefUid, calenderExternalId)); + } } } - // If we are merging two calendar events we should delete the old calendar event if (newBookingId) { const oldCalendarEvent = booking.references.find((reference) => reference.type.includes("_calendar")); @@ -516,17 +570,17 @@ export default class EventManager { .filter((cred) => cred.type.includes("other_calendar")) .map(async (cred) => { const calendarReference = booking.references.find((ref) => ref.type === cred.type); - if (!calendarReference) - if (!calendarReference) { - return { - appName: cred.appName, - type: cred.type, - success: false, - uid: "", - originalEvent: event, - credentialId: cred.id, - }; - } + + if (!calendarReference) { + return { + appName: cred.appName, + type: cred.type, + success: false, + uid: "", + originalEvent: event, + credentialId: cred.id, + }; + } const { externalCalendarId: bookingExternalCalendarId, meetingId: bookingRefUid } = calendarReference; return await updateEvent(cred, event, bookingRefUid ?? null, bookingExternalCalendarId ?? null); @@ -539,17 +593,19 @@ export default class EventManager { if (error instanceof Error) { message = message.replace("{thing}", error.message); } - console.error(message); - return Promise.resolve([ - { - appName: "none", - type: calendarReference?.type || "calendar", - success: false, - uid: "", - originalEvent: event, - credentialId: 0, - }, - ]); + + return Promise.resolve( + calendarReference?.map((reference) => { + return { + appName: "none", + type: reference?.type || "calendar", + success: false, + uid: "", + originalEvent: event, + credentialId: 0, + }; + }) ?? ([] as Array>) + ); } } diff --git a/packages/core/builders/CalendarEvent/builder.ts b/packages/core/builders/CalendarEvent/builder.ts index 8aa4b3cb7c..80a75ba2b1 100644 --- a/packages/core/builders/CalendarEvent/builder.ts +++ b/packages/core/builders/CalendarEvent/builder.ts @@ -1,4 +1,5 @@ -import { Prisma, Booking } from "@prisma/client"; +import type { Booking } from "@prisma/client"; +import { Prisma } from "@prisma/client"; import short from "short-uuid"; import { v5 as uuidv5 } from "uuid"; diff --git a/packages/core/builders/CalendarEvent/class.ts b/packages/core/builders/CalendarEvent/class.ts index 880fde5287..2f069c3425 100644 --- a/packages/core/builders/CalendarEvent/class.ts +++ b/packages/core/builders/CalendarEvent/class.ts @@ -23,7 +23,7 @@ class CalendarEventClass implements CalendarEvent { uid?: string | null; videoCallData?: VideoCallData; paymentInfo?: any; - destinationCalendar?: DestinationCalendar | null; + destinationCalendar?: DestinationCalendar[] | null; cancellationReason?: string | null; rejectionReason?: string | null; hideCalendarNotes?: boolean; diff --git a/packages/emails/src/templates/BrokenIntegrationEmail.tsx b/packages/emails/src/templates/BrokenIntegrationEmail.tsx index 99f7fe72ce..afb7edd31d 100644 --- a/packages/emails/src/templates/BrokenIntegrationEmail.tsx +++ b/packages/emails/src/templates/BrokenIntegrationEmail.tsx @@ -85,8 +85,9 @@ export const BrokenIntegrationEmail = ( if (type === "calendar") { // The calendar name is stored as name_calendar - let calendar = calEvent.destinationCalendar - ? calEvent.destinationCalendar?.integration.split("_") + const [mainHostDestinationCalendar] = calEvent.destinationCalendar ?? []; + let calendar = mainHostDestinationCalendar + ? mainHostDestinationCalendar?.integration.split("_") : "calendar"; if (Array.isArray(calendar)) { diff --git a/packages/features/auth/lib/next-auth-options.ts b/packages/features/auth/lib/next-auth-options.ts index 7fd3ffbcaf..3c7ab80d80 100644 --- a/packages/features/auth/lib/next-auth-options.ts +++ b/packages/features/auth/lib/next-auth-options.ts @@ -387,7 +387,7 @@ export const AUTH_OPTIONS: AuthOptions = { if (trigger === "update") { return { ...token, - locale: session?.locale ?? token.locale, + locale: session?.locale ?? token.locale ?? "en", name: session?.name ?? token.name, username: session?.username ?? token.username, email: session?.email ?? token.email, diff --git a/packages/features/bookings/Booker/components/DatePicker.tsx b/packages/features/bookings/Booker/components/DatePicker.tsx index d3970073db..82eea45a4a 100644 --- a/packages/features/bookings/Booker/components/DatePicker.tsx +++ b/packages/features/bookings/Booker/components/DatePicker.tsx @@ -1,5 +1,6 @@ import { shallow } from "zustand/shallow"; +import type { Dayjs } from "@calcom/dayjs"; import dayjs from "@calcom/dayjs"; import { default as DatePickerComponent } from "@calcom/features/calendars/DatePicker"; import { useNonEmptyScheduleDays } from "@calcom/features/schedules"; @@ -23,8 +24,13 @@ export const DatePicker = () => { return ( setSelectedDate(date ? date.format("YYYY-MM-DD") : date)} - onMonthChange={(date) => setMonth(date.format("YYYY-MM"))} + onChange={(date: Dayjs | null) => { + setSelectedDate(date === null ? date : date.format("YYYY-MM-DD")); + }} + onMonthChange={(date: Dayjs) => { + setMonth(date.format("YYYY-MM")); + setSelectedDate(date.format("YYYY-MM-DD")); + }} includedDates={nonEmptyScheduleDays} locale={i18n.language} browsingDate={month ? dayjs(month) : undefined} diff --git a/packages/features/bookings/Booker/store.ts b/packages/features/bookings/Booker/store.ts index 85c1cd5f31..ef5bf203d5 100644 --- a/packages/features/bookings/Booker/store.ts +++ b/packages/features/bookings/Booker/store.ts @@ -154,14 +154,19 @@ export const useBookerStore = create((set, get) => ({ }, selectedDate: getQueryParam("date") || null, setSelectedDate: (selectedDate: string | null) => { + // unset selected date + if (!selectedDate) { + removeQueryParam("date"); + return; + } + const currentSelection = dayjs(get().selectedDate); const newSelection = dayjs(selectedDate); set({ selectedDate }); updateQueryParam("date", selectedDate ?? ""); // Setting month make sure small calendar in fullscreen layouts also updates. - // If selectedDate is null, prevents setting month to Invalid-Date - if (selectedDate && newSelection.month() !== currentSelection.month()) { + if (newSelection.month() !== currentSelection.month()) { set({ month: newSelection.format("YYYY-MM") }); updateQueryParam("month", newSelection.format("YYYY-MM")); } @@ -194,6 +199,7 @@ export const useBookerStore = create((set, get) => ({ setMonth: (month: string | null) => { set({ month, selectedTimeslot: null }); updateQueryParam("month", month ?? ""); + get().setSelectedDate(null); }, isTeamEvent: false, seatedEventData: { diff --git a/packages/features/bookings/components/event-meta/Members.tsx b/packages/features/bookings/components/event-meta/Members.tsx index aaf10154b7..1712666534 100644 --- a/packages/features/bookings/components/event-meta/Members.tsx +++ b/packages/features/bookings/components/event-meta/Members.tsx @@ -1,3 +1,5 @@ +import { usePathname } from "next/navigation"; + import { getOrgFullDomain } from "@calcom/features/ee/organizations/lib/orgDomains"; import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants"; import { SchedulingType } from "@calcom/prisma/enums"; @@ -26,6 +28,7 @@ type Avatar = { type AvatarWithRequiredImage = Avatar & { image: string }; export const EventMembers = ({ schedulingType, users, profile, entity }: EventMembersProps) => { + const pathname = usePathname(); const showMembers = schedulingType !== SchedulingType.ROUND_ROBIN; const shownUsers = showMembers ? users : []; @@ -57,7 +60,9 @@ export const EventMembers = ({ schedulingType, users, profile, entity }: EventMe title: `${profile.name || profile.username}`, image: "logo" in profile && profile.logo ? `${profile.logo}` : undefined, alt: profile.name || undefined, - href: profile.username ? `${CAL_URL}/${profile.username}` : undefined, + href: profile.username + ? `${CAL_URL}` + (pathname.indexOf("/team/") !== -1 ? "/team" : "") + `/${profile.username}` + : undefined, }); const uniqueAvatars = avatars diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index a007e7f74b..e5acb48fad 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -248,7 +248,11 @@ async function handler(req: CustomRequest) { ? parseRecurringEvent(bookingToDelete.eventType?.recurringEvent) : undefined, location: bookingToDelete?.location, - destinationCalendar: bookingToDelete?.destinationCalendar || bookingToDelete?.user.destinationCalendar, + destinationCalendar: bookingToDelete?.destinationCalendar + ? [bookingToDelete?.destinationCalendar] + : bookingToDelete?.user.destinationCalendar + ? [bookingToDelete?.user.destinationCalendar] + : [], cancellationReason: cancellationReason, ...(teamMembers && { team: { name: "", members: teamMembers } }), seatsPerTimeSlot: bookingToDelete.eventType?.seatsPerTimeSlot, @@ -411,58 +415,71 @@ async function handler(req: CustomRequest) { const apiDeletes = []; - const bookingCalendarReference = bookingToDelete.references.find((reference) => + const bookingCalendarReference = bookingToDelete.references.filter((reference) => reference.type.includes("_calendar") ); - if (bookingCalendarReference) { - const { credentialId, uid, externalCalendarId } = bookingCalendarReference; - // If the booking calendar reference contains a credentialId - if (credentialId) { - // Find the correct calendar credential under user credentials - const calendarCredential = bookingToDelete.user.credentials.find( - (credential) => credential.id === credentialId - ); - if (calendarCredential) { - const calendar = await getCalendar(calendarCredential); - if ( - bookingToDelete.eventType?.recurringEvent && - bookingToDelete.recurringEventId && - allRemainingBookings - ) { - const promises = bookingToDelete.user.credentials - .filter((credential) => credential.type.endsWith("_calendar")) - .map(async (credential) => { - const calendar = await getCalendar(credential); - for (const updBooking of updatedBookings) { - const bookingRef = updBooking.references.find((ref) => ref.type.includes("_calendar")); - if (bookingRef) { - const { uid, externalCalendarId } = bookingRef; - const deletedEvent = await calendar?.deleteEvent(uid, evt, externalCalendarId); - apiDeletes.push(deletedEvent); - } - } - }); - try { - await Promise.all(promises); - } catch (error) { - if (error instanceof Error) { - logger.error(error.message); - } + if (bookingCalendarReference.length > 0) { + for (const reference of bookingCalendarReference) { + const { credentialId, uid, externalCalendarId } = reference; + // If the booking calendar reference contains a credentialId + if (credentialId) { + // Find the correct calendar credential under user credentials + let calendarCredential = bookingToDelete.user.credentials.find( + (credential) => credential.id === credentialId + ); + if (!calendarCredential) { + // get credential from DB + const foundCalendarCredential = await prisma.credential.findUnique({ + where: { + id: credentialId, + }, + }); + if (foundCalendarCredential) { + calendarCredential = foundCalendarCredential; } - } else { + } + if (calendarCredential) { + const calendar = await getCalendar(calendarCredential); + if ( + bookingToDelete.eventType?.recurringEvent && + bookingToDelete.recurringEventId && + allRemainingBookings + ) { + const promises = bookingToDelete.user.credentials + .filter((credential) => credential.type.endsWith("_calendar")) + .map(async (credential) => { + const calendar = await getCalendar(credential); + for (const updBooking of updatedBookings) { + const bookingRef = updBooking.references.find((ref) => ref.type.includes("_calendar")); + if (bookingRef) { + const { uid, externalCalendarId } = bookingRef; + const deletedEvent = await calendar?.deleteEvent(uid, evt, externalCalendarId); + apiDeletes.push(deletedEvent); + } + } + }); + try { + await Promise.all(promises); + } catch (error) { + if (error instanceof Error) { + logger.error(error.message); + } + } + } else { + apiDeletes.push(calendar?.deleteEvent(uid, evt, externalCalendarId) as Promise); + } + } + } else { + // For bookings made before the refactor we go through the old behavior of running through each calendar credential + const calendarCredentials = bookingToDelete.user.credentials.filter((credential) => + credential.type.endsWith("_calendar") + ); + for (const credential of calendarCredentials) { + const calendar = await getCalendar(credential); apiDeletes.push(calendar?.deleteEvent(uid, evt, externalCalendarId) as Promise); } } - } else { - // For bookings made before the refactor we go through the old behaviour of running through each calendar credential - const calendarCredentials = bookingToDelete.user.credentials.filter((credential) => - credential.type.endsWith("_calendar") - ); - for (const credential of calendarCredentials) { - const calendar = await getCalendar(credential); - apiDeletes.push(calendar?.deleteEvent(uid, evt, externalCalendarId) as Promise); - } } } @@ -508,7 +525,11 @@ async function handler(req: CustomRequest) { attendees: attendeesList, location: bookingToDelete.location ?? "", uid: bookingToDelete.uid ?? "", - destinationCalendar: bookingToDelete?.destinationCalendar || bookingToDelete?.user.destinationCalendar, + destinationCalendar: bookingToDelete?.destinationCalendar + ? [bookingToDelete?.destinationCalendar] + : bookingToDelete?.user.destinationCalendar + ? [bookingToDelete?.user.destinationCalendar] + : [], }; const successPayment = bookingToDelete.payment.find((payment) => payment.success); diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index af39a15eac..74c92e46ec 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -1,4 +1,4 @@ -import type { App, Attendee, Credential, EventTypeCustomInput } from "@prisma/client"; +import type { App, Attendee, Credential, EventTypeCustomInput, DestinationCalendar } from "@prisma/client"; import { Prisma } from "@prisma/client"; import async from "async"; import { isValidPhoneNumber } from "libphonenumber-js"; @@ -367,7 +367,7 @@ async function ensureAvailableUsers( ) { const availableUsers: IsFixedAwareUser[] = []; - const orginalBookingDuration = input.originalRescheduledBooking + const originalBookingDuration = input.originalRescheduledBooking ? dayjs(input.originalRescheduledBooking.endTime).diff( dayjs(input.originalRescheduledBooking.startTime), "minutes" @@ -380,7 +380,7 @@ async function ensureAvailableUsers( { userId: user.id, eventTypeId: eventType.id, - duration: orginalBookingDuration, + duration: originalBookingDuration, ...input, }, { @@ -686,8 +686,7 @@ async function handler( if (!eventType) throw new HttpError({ statusCode: 404, message: "eventType.notFound" }); const isTeamEventType = - eventType.schedulingType === SchedulingType.COLLECTIVE || - eventType.schedulingType === SchedulingType.ROUND_ROBIN; + !!eventType.schedulingType && ["COLLECTIVE", "ROUND_ROBIN"].includes(eventType.schedulingType); const paymentAppData = getPaymentAppData(eventType); @@ -722,31 +721,46 @@ async function handler( throw new HttpError({ statusCode: 400, message: error.message }); } - const loadUsers = async () => - !eventTypeId - ? await prisma.user.findMany({ + const loadUsers = async () => { + try { + if (!eventTypeId) { + if (!Array.isArray(dynamicUserList) || dynamicUserList.length === 0) { + throw new Error("dynamicUserList is not properly defined or empty."); + } + + const users = await prisma.user.findMany({ where: { - username: { - in: dynamicUserList, - }, + username: { in: dynamicUserList }, }, select: { ...userSelect.select, - credentials: true, // Don't leak to client + credentials: true, metadata: true, - organization: { - select: { - slug: true, - }, - }, }, - }) - : eventType.hosts?.length - ? eventType.hosts.map(({ user, isFixed }) => ({ + }); + + return users; + } else { + const hosts = eventType.hosts || []; + + if (!Array.isArray(hosts)) { + throw new Error("eventType.hosts is not properly defined."); + } + + const users = hosts.map(({ user, isFixed }) => ({ ...user, isFixed, - })) - : eventType.users || []; + })); + + return users.length ? users : eventType.users; + } + } catch (error) { + if (error instanceof HttpError || error instanceof Prisma.PrismaClientKnownRequestError) { + throw new HttpError({ statusCode: 400, message: error.message }); + } + throw new HttpError({ statusCode: 500, message: "Unable to load users" }); + } + }; // loadUsers allows type inferring let users: (Awaited>[number] & { isFixed?: boolean; @@ -970,20 +984,26 @@ async function handler( : getLocationValueForDB(locationBodyString, eventType.locations); const customInputs = getCustomInputsResponses(reqBody, eventType.customInputs); - const teamMemberPromises = - users.length > 1 - ? users.slice(1).map(async function (user) { - return { - email: user.email || "", - name: user.name || "", - timeZone: user.timeZone, - language: { - translate: await getTranslation(user.locale ?? "en", "common"), - locale: user.locale ?? "en", - }, - }; - }) - : []; + const teamDestinationCalendars: DestinationCalendar[] = []; + + // Organizer or user owner of this event type it's not listed as a team member. + const teamMemberPromises = users.slice(1).map(async (user) => { + // push to teamDestinationCalendars if it's a team event but collective only + if (isTeamEventType && eventType.schedulingType === "COLLECTIVE" && user.destinationCalendar) { + teamDestinationCalendars.push(user.destinationCalendar); + } + return { + email: user.email ?? "", + name: user.name ?? "", + firstName: "", + lastName: "", + timeZone: user.timeZone, + language: { + translate: await getTranslation(user.locale ?? "en", "common"), + locale: user.locale ?? "en", + }, + }; + }); const teamMembers = await Promise.all(teamMemberPromises); @@ -1040,16 +1060,24 @@ async function handler( attendees: attendeesList, location: bookingLocation, // Will be processed by the EventManager later. conferenceCredentialId, - /** For team events & dynamic collective events, we will need to handle each member destinationCalendar eventually */ - destinationCalendar: eventType.destinationCalendar || organizerUser.destinationCalendar, + destinationCalendar: eventType.destinationCalendar + ? [eventType.destinationCalendar] + : organizerUser.destinationCalendar + ? [organizerUser.destinationCalendar] + : null, hideCalendarNotes: eventType.hideCalendarNotes, requiresConfirmation: requiresConfirmation ?? false, eventTypeId: eventType.id, // if seats are not enabled we should default true seatsShowAttendees: eventType.seatsPerTimeSlot ? eventType.seatsShowAttendees : true, seatsPerTimeSlot: eventType.seatsPerTimeSlot, + schedulingType: eventType.schedulingType, }; + if (isTeamEventType && eventType.schedulingType === "COLLECTIVE") { + evt.destinationCalendar?.push(...teamDestinationCalendars); + } + /* Used for seats bookings to update evt object with video data */ const addVideoCallDataToEvt = (bookingReferences: BookingReference[]) => { const videoCallReference = bookingReferences.find((reference) => reference.type.includes("_video")); @@ -1843,11 +1871,12 @@ async function handler( id: organizerUser.id, }, }, - destinationCalendar: evt.destinationCalendar - ? { - connect: { id: evt.destinationCalendar.id }, - } - : undefined, + destinationCalendar: + evt.destinationCalendar && evt.destinationCalendar.length > 0 + ? { + connect: { id: evt.destinationCalendar[0].id }, + } + : undefined, }; if (reqBody.recurringEventId) { diff --git a/packages/features/calendars/DatePicker.tsx b/packages/features/calendars/DatePicker.tsx index e4bfda0d1a..b206b36ee6 100644 --- a/packages/features/calendars/DatePicker.tsx +++ b/packages/features/calendars/DatePicker.tsx @@ -30,7 +30,7 @@ export type DatePickerProps = { /** Defaults to [], which dates are not bookable. Array of valid dates like: ["2022-04-23", "2022-04-24"] */ excludedDates?: string[]; /** defaults to all, which dates are bookable (inverse of excludedDates) */ - includedDates?: string[] | null; + includedDates?: string[]; /** allows adding classes to the container */ className?: string; /** Shows a small loading spinner next to the month name */ @@ -100,6 +100,40 @@ const NoAvailabilityOverlay = ({ ); }; +/** + * Takes care of selecting a valid date in the month if the selected date is not available in the month + */ +const useHandleInitialDateSelection = ({ + daysToRenderForTheMonth, + selected, + onChange, +}: { + daysToRenderForTheMonth: { day: Dayjs | null; disabled: boolean }[]; + selected: Dayjs | Dayjs[] | null | undefined; + onChange: (date: Dayjs | null) => void; +}) => { + // Let's not do something for now in case of multiple selected dates as behaviour is unclear and it's not needed at the moment + if (selected instanceof Array) { + return; + } + const firstAvailableDateOfTheMonth = daysToRenderForTheMonth.find((day) => !day.disabled)?.day; + + const isSelectedDateAvailable = selected + ? daysToRenderForTheMonth.some(({ day, disabled }) => { + if (day && yyyymmdd(day) === yyyymmdd(selected) && !disabled) return true; + }) + : false; + + if (!isSelectedDateAvailable && firstAvailableDateOfTheMonth) { + // If selected date not available in the month, select the first available date of the month + onChange(firstAvailableDateOfTheMonth); + } + + if (!firstAvailableDateOfTheMonth) { + onChange(null); + } +}; + const Days = ({ minDate = dayjs.utc(), excludedDates = [], @@ -121,7 +155,7 @@ const Days = ({ // Create placeholder elements for empty days in first week const weekdayOfFirst = browsingDate.date(1).day(); const currentDate = minDate.utcOffset(browsingDate.utcOffset()); - const availableDates = (includedDates: string[] | undefined | null) => { + const availableDates = (includedDates: string[] | undefined) => { const dates = []; const lastDateOfMonth = browsingDate.date(daysInMonth(browsingDate)); for ( @@ -148,21 +182,6 @@ const Days = ({ days.push(date); } - const daysToRenderForTheMonth = days.map((day) => { - if (!day) return { day: null, disabled: true }; - return { - day: day, - disabled: - (includedDates && !includedDates.includes(yyyymmdd(day))) || excludedDates.includes(yyyymmdd(day)), - }; - }); - - useHandleInitialDateSelection({ - daysToRenderForTheMonth, - selected, - onChange: props.onChange, - }); - const [selectedDatesAndTimes] = useBookerStore((state) => [state.selectedDatesAndTimes], shallow); const isActive = (day: dayjs.Dayjs) => { @@ -190,6 +209,21 @@ const Days = ({ return false; }; + const daysToRenderForTheMonth = days.map((day) => { + if (!day) return { day: null, disabled: true }; + return { + day: day, + disabled: + (includedDates && !includedDates.includes(yyyymmdd(day))) || excludedDates.includes(yyyymmdd(day)), + }; + }); + + useHandleInitialDateSelection({ + daysToRenderForTheMonth, + selected, + onChange: props.onChange, + }); + return ( <> {daysToRenderForTheMonth.map(({ day, disabled }, idx) => ( @@ -305,41 +339,4 @@ const DatePicker = ({ ); }; -/** - * Takes care of selecting a valid date in the month if the selected date is not available in the month - */ -const useHandleInitialDateSelection = ({ - daysToRenderForTheMonth, - selected, - onChange, -}: { - daysToRenderForTheMonth: { day: Dayjs | null; disabled: boolean }[]; - selected: Dayjs | Dayjs[] | null | undefined; - onChange: (date: Dayjs | null) => void; -}) => { - // Let's not do something for now in case of multiple selected dates as behaviour is unclear and it's not needed at the moment - if (selected instanceof Array) { - return; - } - const firstAvailableDateOfTheMonth = daysToRenderForTheMonth.find((day) => !day.disabled)?.day; - - const isSelectedDateAvailable = selected - ? daysToRenderForTheMonth.some(({ day, disabled }) => { - if (day && yyyymmdd(day) === yyyymmdd(selected) && !disabled) return true; - }) - : false; - - if (firstAvailableDateOfTheMonth) { - // If selected date not available in the month, select the first available date of the month - if (!isSelectedDateAvailable) { - onChange(firstAvailableDateOfTheMonth); - } - } else { - // No date is available and if we were asked to select something inform that it couldn't be selected. This would actually help in not showing the timeslots section(with No Time Available) when no date in the month is available - if (selected) { - onChange(null); - } - } -}; - export default DatePicker; diff --git a/packages/features/calendars/weeklyview/components/event/Empty.tsx b/packages/features/calendars/weeklyview/components/event/Empty.tsx index cc93b959c0..386bc7d6eb 100644 --- a/packages/features/calendars/weeklyview/components/event/Empty.tsx +++ b/packages/features/calendars/weeklyview/components/event/Empty.tsx @@ -96,6 +96,7 @@ function Cell({ isDisabled, topOffsetMinutes, timeSlot }: CellProps) { )} data-disabled={isDisabled} data-slot={timeSlot.toISOString()} + data-testid="calendar-empty-cell" style={{ height: `calc(${hoverEventDuration}*var(--one-minute-height))`, overflow: "visible", diff --git a/packages/features/ee/organizations/pages/settings/admin/AdminOrgPage.tsx b/packages/features/ee/organizations/pages/settings/admin/AdminOrgPage.tsx index 1289151db6..c111ba3040 100644 --- a/packages/features/ee/organizations/pages/settings/admin/AdminOrgPage.tsx +++ b/packages/features/ee/organizations/pages/settings/admin/AdminOrgPage.tsx @@ -59,7 +59,9 @@ function AdminOrgTable() {
- {org.members[0].user.email} + + {org.members.length ? org.members[0].user.email : "No members"} +
diff --git a/packages/features/ee/package.json b/packages/features/ee/package.json index 7244d62522..33aae3a963 100644 --- a/packages/features/ee/package.json +++ b/packages/features/ee/package.json @@ -14,7 +14,7 @@ "@sendgrid/mail": "^7.6.2", "libphonenumber-js": "^1.10.12", "twilio": "^3.80.1", - "zod": "^3.20.2" + "zod": "^3.22.2" }, "devDependencies": { "@calcom/tsconfig": "*" diff --git a/packages/features/ee/payments/api/paypal-webhook.ts b/packages/features/ee/payments/api/paypal-webhook.ts index 493626200b..06708fe0fd 100644 --- a/packages/features/ee/payments/api/paypal-webhook.ts +++ b/packages/features/ee/payments/api/paypal-webhook.ts @@ -149,7 +149,11 @@ export async function handlePaymentSuccess( }, attendees: attendeesList, uid: booking.uid, - destinationCalendar: booking.destinationCalendar || user.destinationCalendar, + destinationCalendar: booking.destinationCalendar + ? [booking.destinationCalendar] + : user.destinationCalendar + ? [user.destinationCalendar] + : [], recurringEvent: parseRecurringEvent(eventTypeRaw?.recurringEvent), }; diff --git a/packages/features/ee/payments/api/webhook.ts b/packages/features/ee/payments/api/webhook.ts index deb8f6e993..c6b563a071 100644 --- a/packages/features/ee/payments/api/webhook.ts +++ b/packages/features/ee/payments/api/webhook.ts @@ -98,7 +98,7 @@ async function getBooking(bookingId: number) { }); const attendeesList = await Promise.all(attendeesListPromises); - + const selectedDestinationCalendar = booking.destinationCalendar || user.destinationCalendar; const evt: CalendarEvent = { type: booking.title, title: booking.title, @@ -116,7 +116,7 @@ async function getBooking(bookingId: number) { }, attendees: attendeesList, uid: booking.uid, - destinationCalendar: booking.destinationCalendar || user.destinationCalendar, + destinationCalendar: selectedDestinationCalendar ? [selectedDestinationCalendar] : [], recurringEvent: parseRecurringEvent(eventType?.recurringEvent), }; @@ -204,7 +204,7 @@ async function handlePaymentSuccess(event: Stripe.Event) { }); const attendeesList = await Promise.all(attendeesListPromises); - + const selectedDestinationCalendar = booking.destinationCalendar || user.destinationCalendar; const evt: CalendarEvent = { type: booking.title, title: booking.title, @@ -226,7 +226,7 @@ async function handlePaymentSuccess(event: Stripe.Event) { attendees: attendeesList, location: booking.location, uid: booking.uid, - destinationCalendar: booking.destinationCalendar || user.destinationCalendar, + destinationCalendar: selectedDestinationCalendar ? [selectedDestinationCalendar] : [], recurringEvent: parseRecurringEvent(eventTypeRaw?.recurringEvent), }; diff --git a/packages/features/ee/teams/components/MemberInvitationModal.tsx b/packages/features/ee/teams/components/MemberInvitationModal.tsx index f6fad884f0..b72ace316f 100644 --- a/packages/features/ee/teams/components/MemberInvitationModal.tsx +++ b/packages/features/ee/teams/components/MemberInvitationModal.tsx @@ -32,6 +32,7 @@ import { GoogleWorkspaceInviteButton } from "./GoogleWorkspaceInviteButton"; type MemberInvitationModalProps = { isOpen: boolean; + justEmailInvites?: boolean; onExit: () => void; orgMembers?: RouterOutputs["viewer"]["organizations"]["getMembers"]; onSubmit: (values: NewMemberForm, resetFields: () => void) => void; @@ -206,7 +207,7 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps) render={({ field: { onChange }, fieldState: { error } }) => ( <> {props.text}
diff --git a/packages/features/ee/teams/components/TeamsListing.tsx b/packages/features/ee/teams/components/TeamsListing.tsx index 12613a90a8..6560880f9e 100644 --- a/packages/features/ee/teams/components/TeamsListing.tsx +++ b/packages/features/ee/teams/components/TeamsListing.tsx @@ -65,7 +65,7 @@ export function TeamsListing() { { icon: , title: t("sms_attendee_action"), - description: t("make_it_easy_to_book"), + description: t("send_reminder_sms"), }, { icon:
{selectedDate ? (
- - -
setSelectTime((prev) => !prev)}> -

{t("select_time")}

{" "} - <> - {!selectedDate || !selectTime ? : } - -
- {selectTime && selectedDate ? ( -
- -
- ) : null} -
-
+ {selectTime && selectedDate ? ( +
+ +
+ ) : null}
) : null}
diff --git a/packages/features/schedules/components/DateOverrideInputDialog.tsx b/packages/features/schedules/components/DateOverrideInputDialog.tsx index f908d35c99..b4f50a19eb 100644 --- a/packages/features/schedules/components/DateOverrideInputDialog.tsx +++ b/packages/features/schedules/components/DateOverrideInputDialog.tsx @@ -51,11 +51,7 @@ const DateOverrideForm = ({ const [selectedDates, setSelectedDates] = useState(value ? [dayjs.utc(value[0].start)] : []); - const onDateChange = (newDate: Dayjs | null) => { - // If no date is selected, do nothing - if (!newDate) { - return; - } + const onDateChange = (newDate: Dayjs) => { // If clicking on a selected date unselect it if (selectedDates.some((date) => yyyymmdd(date) === yyyymmdd(newDate))) { setSelectedDates(selectedDates.filter((date) => yyyymmdd(date) !== yyyymmdd(newDate))); @@ -154,7 +150,9 @@ const DateOverrideForm = ({ excludedDates={excludedDates} weekStart={0} selected={selectedDates} - onChange={(day) => onDateChange(day)} + onChange={(day) => { + if (day) onDateChange(day); + }} onMonthChange={(newMonth) => { setBrowsingDate(newMonth); }} diff --git a/packages/features/shell/Shell.tsx b/packages/features/shell/Shell.tsx index 0b4e5b881a..6a6b353749 100644 --- a/packages/features/shell/Shell.tsx +++ b/packages/features/shell/Shell.tsx @@ -369,7 +369,7 @@ function UserDropdown({ small }: UserDropdownProps) { {item.icon && (