Apps Status + Updates to non-traditional calendars (#5034)
* Apps Status + Updates to non-traditional calendars * Recurring booking tweaks * Last tweaks * Fixing checks * Fixing eslint * Reverting unneeded changes, using plain text email * More unneeded changes revert * Update packages/features/bookings/lib/handleNewBooking.ts Co-authored-by: Omar López <zomars@me.com> * Fixing lint * Refactored appsStatus in emails * Update packages/emails/src/templates/OrganizerScheduledEmail.tsx Co-authored-by: Omar López <zomars@me.com>
This commit is contained in:
co-authored by
Omar López
parent
f298865d9e
commit
b6be94fd14
@@ -1,22 +1,46 @@
|
||||
import { AppsStatus } from "@calcom/types/Calendar";
|
||||
|
||||
import * as fetch from "@lib/core/http/fetch-wrapper";
|
||||
import { BookingCreateBody, BookingResponse } from "@lib/types/booking";
|
||||
|
||||
type ExtendedBookingCreateBody = BookingCreateBody & { noEmail?: boolean; recurringCount?: number };
|
||||
|
||||
const createRecurringBooking = async (data: ExtendedBookingCreateBody[]) => {
|
||||
return Promise.all(
|
||||
data.map((booking, key) => {
|
||||
// We only want to send the first occurrence of the meeting at the moment, not all at once
|
||||
if (key === 0) {
|
||||
return fetch.post<ExtendedBookingCreateBody, BookingResponse>("/api/book/event", booking);
|
||||
} else {
|
||||
return fetch.post<ExtendedBookingCreateBody, BookingResponse>("/api/book/event", {
|
||||
...booking,
|
||||
noEmail: true,
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
const createdBookings: BookingResponse[] = [];
|
||||
// Reversing to accumulate results for noEmail instances first, to then lastly, create the
|
||||
// emailed booking taking into account accumulated results to send app status accurately
|
||||
for (let key = 0; key < data.length; key++) {
|
||||
const booking = data[key];
|
||||
if (key === data.length - 1) {
|
||||
const calcAppsStatus: { [key: string]: AppsStatus } = createdBookings
|
||||
.flatMap((book) => (book.appsStatus !== undefined ? book.appsStatus : []))
|
||||
.reduce((prev, curr) => {
|
||||
if (prev[curr.type]) {
|
||||
prev[curr.type].failures += curr.failures;
|
||||
prev[curr.type].success += curr.success;
|
||||
} else {
|
||||
prev[curr.type] = curr;
|
||||
}
|
||||
return prev;
|
||||
}, {} as { [key: string]: AppsStatus });
|
||||
const appsStatus = Object.values(calcAppsStatus);
|
||||
const response = await fetch.post<
|
||||
ExtendedBookingCreateBody & { appsStatus: AppsStatus[] },
|
||||
BookingResponse
|
||||
>("/api/book/event", {
|
||||
...booking,
|
||||
appsStatus,
|
||||
});
|
||||
createdBookings.push(response);
|
||||
} else {
|
||||
const response = await fetch.post<ExtendedBookingCreateBody, BookingResponse>("/api/book/event", {
|
||||
...booking,
|
||||
noEmail: true,
|
||||
});
|
||||
createdBookings.push(response);
|
||||
}
|
||||
}
|
||||
return createdBookings;
|
||||
};
|
||||
|
||||
export default createRecurringBooking;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Attendee, Booking } from "@prisma/client";
|
||||
|
||||
import { AppsStatus } from "@calcom/types/Calendar";
|
||||
|
||||
export type BookingCreateBody = {
|
||||
email: string;
|
||||
end: string;
|
||||
@@ -33,4 +35,5 @@ export type BookingCreateBody = {
|
||||
export type BookingResponse = Booking & {
|
||||
paymentUid?: string;
|
||||
attendees: Attendee[];
|
||||
appsStatus?: AppsStatus[];
|
||||
};
|
||||
|
||||
@@ -543,6 +543,7 @@
|
||||
"link_shared": "Link shared!",
|
||||
"title": "Title",
|
||||
"description": "Description",
|
||||
"apps_status": "Apps Status",
|
||||
"quick_video_meeting": "A quick video meeting.",
|
||||
"scheduling_type": "Scheduling Type",
|
||||
"preview_team": "Preview team",
|
||||
|
||||
@@ -51,7 +51,19 @@ export default class HubspotOtherCalendarService implements Calendar {
|
||||
};
|
||||
});
|
||||
return Promise.all(
|
||||
simplePublicObjectInputs.map((contact) => hubspotClient.crm.contacts.basicApi.create(contact))
|
||||
simplePublicObjectInputs.map((contact) =>
|
||||
hubspotClient.crm.contacts.basicApi.create(contact).catch((error) => {
|
||||
// If multiple events are created, subsequent events may fail due to
|
||||
// contact was created by previous event creation, so we introduce a
|
||||
// fallback taking advantage of the error message providing the contact id
|
||||
if (error.body.message.includes("Contact already exists. Existing ID:")) {
|
||||
const split = error.body.message.split("Contact already exists. Existing ID: ");
|
||||
return { id: split[1] };
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -233,7 +245,10 @@ export default class HubspotOtherCalendarService implements Calendar {
|
||||
// Continue with meeting creation and association only when all contacts are present in HubSpot
|
||||
if (createContacts.length) {
|
||||
this.log.debug("contact:creation:ok");
|
||||
return await this.handleMeetingCreation(event, createContacts.concat(contacts));
|
||||
return await this.handleMeetingCreation(
|
||||
event,
|
||||
createContacts.concat(contacts) as SimplePublicObjectInput[]
|
||||
);
|
||||
}
|
||||
return Promise.reject("Something went wrong when creating non-existing attendees in HubSpot");
|
||||
}
|
||||
@@ -243,7 +258,7 @@ export default class HubspotOtherCalendarService implements Calendar {
|
||||
this.log.debug("contact:created", { createContacts });
|
||||
if (createContacts.length) {
|
||||
this.log.debug("contact:creation:ok");
|
||||
return await this.handleMeetingCreation(event, createContacts);
|
||||
return await this.handleMeetingCreation(event, createContacts as SimplePublicObjectInput[]);
|
||||
}
|
||||
}
|
||||
return Promise.reject("Something went wrong when searching/creating the attendees in HubSpot");
|
||||
|
||||
@@ -5,6 +5,7 @@ import cache from "memory-cache";
|
||||
|
||||
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
|
||||
import getApps from "@calcom/app-store/utils";
|
||||
import type { ExtendedCredential } from "@calcom/core/EventManager";
|
||||
import { getUid } from "@calcom/lib/CalEventParser";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { performance } from "@calcom/lib/server/perfObserver";
|
||||
@@ -184,7 +185,7 @@ export const getBusyCalendarTimes = async (
|
||||
};
|
||||
|
||||
export const createEvent = async (
|
||||
credential: Credential,
|
||||
credential: ExtendedCredential,
|
||||
calEvent: CalendarEvent
|
||||
): Promise<EventResult<NewCalendarEventType>> => {
|
||||
const uid: string = getUid(calEvent);
|
||||
@@ -217,6 +218,7 @@ export const createEvent = async (
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
appName: credential.appName,
|
||||
type: credential.type,
|
||||
success,
|
||||
uid,
|
||||
@@ -226,7 +228,7 @@ export const createEvent = async (
|
||||
};
|
||||
|
||||
export const updateEvent = async (
|
||||
credential: Credential,
|
||||
credential: ExtendedCredential,
|
||||
calEvent: CalendarEvent,
|
||||
bookingRefUid: string | null,
|
||||
externalCalendarId: string | null
|
||||
@@ -255,6 +257,7 @@ export const updateEvent = async (
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
appName: credential.appName,
|
||||
type: credential.type,
|
||||
success,
|
||||
uid,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Credential, DestinationCalendar } from "@prisma/client";
|
||||
import async from "async";
|
||||
import merge from "lodash/merge";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
import { z } from "zod";
|
||||
@@ -65,9 +64,11 @@ type EventManagerUser = {
|
||||
|
||||
type createdEventSchema = z.infer<typeof createdEventSchema>;
|
||||
|
||||
export type ExtendedCredential = Credential & { appName: string };
|
||||
|
||||
export default class EventManager {
|
||||
calendarCredentials: Credential[];
|
||||
videoCredentials: Credential[];
|
||||
calendarCredentials: ExtendedCredential[];
|
||||
videoCredentials: ExtendedCredential[];
|
||||
|
||||
/**
|
||||
* Takes an array of credentials and initializes a new instance of the EventManager.
|
||||
@@ -75,7 +76,9 @@ export default class EventManager {
|
||||
* @param user
|
||||
*/
|
||||
constructor(user: EventManagerUser) {
|
||||
const appCredentials = getApps(user.credentials).flatMap((app) => app.credentials);
|
||||
const appCredentials = getApps(user.credentials).flatMap((app) =>
|
||||
app.credentials.map((creds) => ({ ...creds, appName: app.name }))
|
||||
);
|
||||
this.calendarCredentials = appCredentials.filter((cred) => cred.type.endsWith("_calendar"));
|
||||
this.videoCredentials = appCredentials.filter((cred) => cred.type.endsWith("_video"));
|
||||
}
|
||||
@@ -306,11 +309,9 @@ export default class EventManager {
|
||||
let createdEvents: EventResult<NewCalendarEventType>[] = [];
|
||||
if (event.destinationCalendar) {
|
||||
if (event.destinationCalendar.credentialId) {
|
||||
const credential = await prisma.credential.findFirst({
|
||||
where: {
|
||||
id: event.destinationCalendar.credentialId,
|
||||
},
|
||||
});
|
||||
const credential = this.calendarCredentials.find(
|
||||
(c) => c.id === event.destinationCalendar?.credentialId
|
||||
);
|
||||
|
||||
if (credential) {
|
||||
createdEvents.push(await createEvent(credential, event));
|
||||
@@ -354,7 +355,7 @@ export default class EventManager {
|
||||
* @private
|
||||
*/
|
||||
|
||||
private getVideoCredential(event: CalendarEvent): Credential | undefined {
|
||||
private getVideoCredential(event: CalendarEvent): ExtendedCredential | undefined {
|
||||
if (!event.location) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -374,7 +375,7 @@ export default class EventManager {
|
||||
* This might happen if someone tries to use a location with a missing credential, so we fallback to Cal Video.
|
||||
* @todo remove location from event types that has missing credentials
|
||||
* */
|
||||
if (!videoCredential) videoCredential = FAKE_DAILY_CREDENTIAL;
|
||||
if (!videoCredential) videoCredential = { ...FAKE_DAILY_CREDENTIAL, appName: "FAKE" };
|
||||
|
||||
return videoCredential;
|
||||
}
|
||||
@@ -409,7 +410,7 @@ export default class EventManager {
|
||||
* @param booking
|
||||
* @private
|
||||
*/
|
||||
private updateAllCalendarEvents(
|
||||
private async updateAllCalendarEvents(
|
||||
event: CalendarEvent,
|
||||
booking: PartialBooking
|
||||
): Promise<Array<EventResult<NewCalendarEventType>>> {
|
||||
@@ -424,7 +425,7 @@ export default class EventManager {
|
||||
|
||||
if (!bookingExternalCalendarId) throw new Error("externalCalendarId");
|
||||
|
||||
const result = [];
|
||||
let result = [];
|
||||
if (calendarReference.credentialId) {
|
||||
credential = this.calendarCredentials.filter(
|
||||
(credential) => credential.id === calendarReference?.credentialId
|
||||
@@ -439,6 +440,26 @@ export default class EventManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Taking care of non-traditional calendar integrations
|
||||
result = result.concat(
|
||||
this.calendarCredentials
|
||||
.filter((cred) => cred.type.includes("other_calendar"))
|
||||
.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,
|
||||
};
|
||||
const { externalCalendarId: bookingExternalCalendarId, meetingId: bookingRefUid } =
|
||||
calendarReference;
|
||||
return await updateEvent(cred, event, bookingRefUid ?? null, bookingExternalCalendarId ?? null);
|
||||
})
|
||||
);
|
||||
|
||||
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}'`;
|
||||
@@ -446,6 +467,7 @@ export default class EventManager {
|
||||
console.error(message);
|
||||
return Promise.resolve([
|
||||
{
|
||||
appName: "none",
|
||||
type: calendarReference?.type || "calendar",
|
||||
success: false,
|
||||
uid: "",
|
||||
|
||||
@@ -4,6 +4,7 @@ import { v5 as uuidv5 } from "uuid";
|
||||
|
||||
import appStore from "@calcom/app-store";
|
||||
import { getDailyAppKeys } from "@calcom/app-store/dailyvideo/lib/getDailyAppKeys";
|
||||
import type { ExtendedCredential } from "@calcom/core/EventManager";
|
||||
import { sendBrokenIntegrationEmail } from "@calcom/emails";
|
||||
import { getUid } from "@calcom/lib/CalEventParser";
|
||||
import logger from "@calcom/lib/logger";
|
||||
@@ -34,7 +35,7 @@ const getBusyVideoTimes = (withCredentials: Credential[]) =>
|
||||
results.reduce((acc, availability) => acc.concat(availability), [] as (EventBusyDate | undefined)[])
|
||||
);
|
||||
|
||||
const createMeeting = async (credential: Credential, calEvent: CalendarEvent) => {
|
||||
const createMeeting = async (credential: ExtendedCredential, calEvent: CalendarEvent) => {
|
||||
const uid: string = getUid(calEvent);
|
||||
|
||||
if (!credential) {
|
||||
@@ -51,6 +52,7 @@ const createMeeting = async (credential: Credential, calEvent: CalendarEvent) =>
|
||||
|
||||
if (!createdMeeting) {
|
||||
return {
|
||||
appName: credential.appName,
|
||||
type: credential.type,
|
||||
success: false,
|
||||
uid,
|
||||
@@ -70,6 +72,7 @@ const createMeeting = async (credential: Credential, calEvent: CalendarEvent) =>
|
||||
}
|
||||
|
||||
return {
|
||||
appName: credential.appName,
|
||||
type: credential.type,
|
||||
success: true,
|
||||
uid,
|
||||
@@ -79,7 +82,7 @@ const createMeeting = async (credential: Credential, calEvent: CalendarEvent) =>
|
||||
};
|
||||
|
||||
const updateMeeting = async (
|
||||
credential: Credential,
|
||||
credential: ExtendedCredential,
|
||||
calEvent: CalendarEvent,
|
||||
bookingRef: PartialReference | null
|
||||
): Promise<EventResult<VideoCallData>> => {
|
||||
@@ -100,6 +103,7 @@ const updateMeeting = async (
|
||||
|
||||
if (!updatedMeeting) {
|
||||
return {
|
||||
appName: credential.appName,
|
||||
type: credential.type,
|
||||
success,
|
||||
uid,
|
||||
@@ -108,6 +112,7 @@ const updateMeeting = async (
|
||||
}
|
||||
|
||||
return {
|
||||
appName: credential.appName,
|
||||
type: credential.type,
|
||||
success,
|
||||
uid,
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { TFunction } from "next-i18next";
|
||||
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
import { Info } from "./Info";
|
||||
|
||||
export const AppsStatus = (props: { calEvent: CalendarEvent; t: TFunction }) => {
|
||||
const { t } = props;
|
||||
if (!props.calEvent.appsStatus) return null;
|
||||
return (
|
||||
<Info
|
||||
label={t("apps_status")}
|
||||
description={
|
||||
<ul style={{ lineHeight: "24px" }}>
|
||||
{props.calEvent.appsStatus.map((status) => (
|
||||
<li key={status.type} style={{ fontWeight: 400 }}>
|
||||
{status.appName}{" "}
|
||||
{status.success >= 1 && `✅ ${status.success > 1 ? `(x${status.success})` : ""}`}{" "}
|
||||
{status.failures >= 1 && `❌ ${status.failures > 1 ? `(x${status.failures})` : ""}`}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
}
|
||||
withSpacer
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -9,3 +9,4 @@ export { ManageLink } from "./ManageLink";
|
||||
export { default as RawHtml } from "./RawHtml";
|
||||
export { WhenInfo } from "./WhenInfo";
|
||||
export { WhoInfo } from "./WhoInfo";
|
||||
export { AppsStatus } from "./AppsStatus";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { TFunction } from "next-i18next";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||
import type { AppsStatus as AppsStatusType, CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||
|
||||
import {
|
||||
BaseEmailHtml,
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
ManageLink,
|
||||
WhenInfo,
|
||||
WhoInfo,
|
||||
AppsStatus,
|
||||
} from "../components";
|
||||
|
||||
export const BaseScheduledEmail = (
|
||||
@@ -18,6 +19,7 @@ export const BaseScheduledEmail = (
|
||||
calEvent: CalendarEvent;
|
||||
attendee: Person;
|
||||
timeZone: string;
|
||||
includeAppsStatus?: boolean;
|
||||
t: TFunction;
|
||||
} & Partial<React.ComponentProps<typeof BaseEmailHtml>>
|
||||
) => {
|
||||
@@ -70,6 +72,7 @@ export const BaseScheduledEmail = (
|
||||
<LocationInfo calEvent={props.calEvent} t={t} />
|
||||
<Info label={t("description")} description={props.calEvent.description} withSpacer />
|
||||
<Info label={t("additional_notes")} description={props.calEvent.additionalNotes} withSpacer />
|
||||
{props.includeAppsStatus && <AppsStatus calEvent={props.calEvent} t={t} />}
|
||||
<CustomInputs calEvent={props.calEvent} />
|
||||
</BaseEmailHtml>
|
||||
);
|
||||
|
||||
@@ -33,6 +33,7 @@ export const OrganizerScheduledEmail = (
|
||||
t={t}
|
||||
subject={t(subject)}
|
||||
title={t(title)}
|
||||
includeAppsStatus
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -44,7 +44,7 @@ import { updateWebUser as syncServicesUpdateWebUser } from "@calcom/lib/sync/Syn
|
||||
import prisma, { userSelect } from "@calcom/prisma";
|
||||
import { EventTypeMetaDataSchema, extendedBookingCreateBody } from "@calcom/prisma/zod-utils";
|
||||
import type { BufferedBusyTime } from "@calcom/types/BufferedBusyTime";
|
||||
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
|
||||
import type { AdditionalInformation, AppsStatus, CalendarEvent } from "@calcom/types/Calendar";
|
||||
import type { EventResult, PartialReference } from "@calcom/types/EventManager";
|
||||
|
||||
import sendPayload, { EventTypeInfo } from "../../webhooks/lib/sendPayload";
|
||||
@@ -248,8 +248,16 @@ async function ensureAvailableUsers(
|
||||
async function handler(req: NextApiRequest & { userId?: number }) {
|
||||
const { userId } = req;
|
||||
|
||||
const { recurringCount, noEmail, eventTypeSlug, eventTypeId, hasHashedBookingLink, language, ...reqBody } =
|
||||
extendedBookingCreateBody.parse(req.body);
|
||||
const {
|
||||
recurringCount,
|
||||
noEmail,
|
||||
eventTypeSlug,
|
||||
eventTypeId,
|
||||
hasHashedBookingLink,
|
||||
language,
|
||||
appsStatus: reqAppsStatus,
|
||||
...reqBody
|
||||
} = extendedBookingCreateBody.parse(req.body);
|
||||
|
||||
// handle dynamic user
|
||||
const dynamicUserList = Array.isArray(reqBody.user)
|
||||
@@ -689,7 +697,7 @@ async function handler(req: NextApiRequest & { userId?: number }) {
|
||||
let referencesToCreate: PartialReference[] = [];
|
||||
|
||||
type Booking = Prisma.PromiseReturnType<typeof createBooking>;
|
||||
let booking: Booking | null = null;
|
||||
let booking: (Booking & { appsStatus?: AppsStatus[] }) | null = null;
|
||||
try {
|
||||
booking = await createBooking();
|
||||
// Sync Services
|
||||
@@ -713,6 +721,39 @@ async function handler(req: NextApiRequest & { userId?: number }) {
|
||||
const credentials = await refreshCredentials(organizerUser.credentials);
|
||||
const eventManager = new EventManager({ ...organizerUser, credentials });
|
||||
|
||||
function handleAppsStatus(
|
||||
results: EventResult<AdditionalInformation>[],
|
||||
booking: (Booking & { appsStatus?: AppsStatus[] }) | null
|
||||
) {
|
||||
// Taking care of apps status
|
||||
const resultStatus: AppsStatus[] = results.map((app) => ({
|
||||
appName: app.appName,
|
||||
type: app.type,
|
||||
success: app.success ? 1 : 0,
|
||||
failures: !app.success ? 1 : 0,
|
||||
}));
|
||||
|
||||
if (reqAppsStatus === undefined) {
|
||||
if (booking !== null) {
|
||||
booking.appsStatus = resultStatus;
|
||||
}
|
||||
evt.appsStatus = resultStatus;
|
||||
return;
|
||||
}
|
||||
// From down here we can assume reqAppsStatus is not undefined anymore
|
||||
// Other status exist, so this is the last booking of a series,
|
||||
// proceeding to prepare the info for the event
|
||||
const calcAppsStatus = reqAppsStatus.concat(resultStatus).reduce((prev, curr) => {
|
||||
if (prev[curr.type]) {
|
||||
prev[curr.type].success += curr.success;
|
||||
} else {
|
||||
prev[curr.type] = curr;
|
||||
}
|
||||
return prev;
|
||||
}, {} as { [key: string]: AppsStatus });
|
||||
evt.appsStatus = Object.values(calcAppsStatus);
|
||||
}
|
||||
|
||||
if (originalRescheduledBooking?.uid) {
|
||||
// Use EventManager to conditionally use all needed integrations.
|
||||
const updateManager = await eventManager.reschedule(
|
||||
@@ -746,6 +787,7 @@ async function handler(req: NextApiRequest & { userId?: number }) {
|
||||
metadata.hangoutLink = updatedEvent.hangoutLink;
|
||||
metadata.conferenceData = updatedEvent.conferenceData;
|
||||
metadata.entryPoints = updatedEvent.entryPoints;
|
||||
handleAppsStatus(results, booking);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -785,6 +827,7 @@ async function handler(req: NextApiRequest & { userId?: number }) {
|
||||
metadata.hangoutLink = results[0].createdEvent?.hangoutLink;
|
||||
metadata.conferenceData = results[0].createdEvent?.conferenceData;
|
||||
metadata.entryPoints = results[0].createdEvent?.entryPoints;
|
||||
handleAppsStatus(results, booking);
|
||||
}
|
||||
if (noEmail !== true) {
|
||||
await sendScheduledEmails({
|
||||
|
||||
@@ -73,6 +73,20 @@ ${calEvent.customInputs[key]}
|
||||
return customInputsString;
|
||||
};
|
||||
|
||||
export const getAppsStatus = (calEvent: CalendarEvent) => {
|
||||
if (!calEvent.appsStatus) {
|
||||
return "";
|
||||
}
|
||||
return `\n${calEvent.attendees[0].language.translate("apps_status")}
|
||||
\n${calEvent.appsStatus.map(
|
||||
(app) =>
|
||||
`\t${app.appName} ${app.success >= 1 && `✅ ${app.success > 1 ? `(x${app.success})` : ""}`} ${
|
||||
app.failures >= 1 && `❌ ${app.failures > 1 ? `(x${app.failures})` : ""}`
|
||||
}`
|
||||
)}
|
||||
`;
|
||||
};
|
||||
|
||||
export const getDescription = (calEvent: CalendarEvent) => {
|
||||
if (!calEvent.description) {
|
||||
return "";
|
||||
@@ -132,6 +146,7 @@ ${getLocation(calEvent)}
|
||||
${getDescription(calEvent)}
|
||||
${getAdditionalNotes(calEvent)}
|
||||
${getCustomInputs(calEvent)}
|
||||
${getAppsStatus(calEvent)}
|
||||
${
|
||||
// TODO: Only the original attendee can make changes to the event
|
||||
// Guests cannot
|
||||
|
||||
@@ -147,6 +147,16 @@ export const extendedBookingCreateBody = bookingCreateBodySchema.merge(
|
||||
recurringCount: z.number().optional(),
|
||||
rescheduleReason: z.string().optional(),
|
||||
smsReminderNumber: z.string().optional(),
|
||||
appsStatus: z
|
||||
.array(
|
||||
z.object({
|
||||
appName: z.string(),
|
||||
success: z.number(),
|
||||
failures: z.number(),
|
||||
type: z.string(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
Vendored
+8
@@ -114,6 +114,13 @@ export interface BookingLimit {
|
||||
PER_YEAR?: number | undefined;
|
||||
}
|
||||
|
||||
export type AppsStatus = {
|
||||
appName: string;
|
||||
type: typeof App["type"];
|
||||
success: number;
|
||||
failures: number;
|
||||
};
|
||||
|
||||
// If modifying this interface, probably should update builders/calendarEvent files
|
||||
export interface CalendarEvent {
|
||||
type: string;
|
||||
@@ -143,6 +150,7 @@ export interface CalendarEvent {
|
||||
recurrence?: string;
|
||||
recurringEvent?: RecurringEvent | null;
|
||||
eventTypeId?: number | null;
|
||||
appsStatus?: AppsStatus[];
|
||||
seatsShowAttendees?: boolean | null;
|
||||
}
|
||||
|
||||
|
||||
Vendored
+1
@@ -16,6 +16,7 @@ export interface PartialReference {
|
||||
|
||||
export interface EventResult<T> {
|
||||
type: string;
|
||||
appName: string;
|
||||
success: boolean;
|
||||
uid: string;
|
||||
createdEvent?: T;
|
||||
|
||||
Reference in New Issue
Block a user