Merge branch 'main' into teams-stripe-checkout-form

This commit is contained in:
Peer Richelsen 2022-11-06 15:47:10 +00:00 committed by GitHub
commit e59dbb9ed0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 234 additions and 79 deletions

View File

@ -24,14 +24,7 @@ export function AvailableEventLocations({ locations }: { locations: Props["event
alt={`${eventLocationType.label} icon`} alt={`${eventLocationType.label} icon`}
/> />
<Tooltip content={locationKeyToString(location)}> <Tooltip content={locationKeyToString(location)}>
<a <p className="truncate">{locationKeyToString(location)}</p>
target="_blank"
href={locationKeyToString(location) ?? "/"}
className="truncate"
key={location.type}
rel="noreferrer">
{locationKeyToString(location)}
</a>
</Tooltip> </Tooltip>
</div> </div>
); );

View File

@ -40,6 +40,7 @@ import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { Icon } from "@calcom/ui/Icon"; import { Icon } from "@calcom/ui/Icon";
import { Tooltip } from "@calcom/ui/Tooltip"; import { Tooltip } from "@calcom/ui/Tooltip";
import AddressInput from "@calcom/ui/form/AddressInputLazy";
import { Button } from "@calcom/ui/components"; import { Button } from "@calcom/ui/components";
import PhoneInput from "@calcom/ui/form/PhoneInputLazy"; import PhoneInput from "@calcom/ui/form/PhoneInputLazy";
import { EmailInput, Form } from "@calcom/ui/form/fields"; import { EmailInput, Form } from "@calcom/ui/form/fields";
@ -68,6 +69,8 @@ type BookingFormValues = {
notes?: string; notes?: string;
locationType?: EventLocationType["type"]; locationType?: EventLocationType["type"];
guests?: string[]; guests?: string[];
address?: string;
attendeeAddress?: string;
phone?: string; phone?: string;
hostPhoneNumber?: string; // Maybe come up with a better way to name this to distingish between two types of phone numbers hostPhoneNumber?: string; // Maybe come up with a better way to name this to distingish between two types of phone numbers
customInputs?: { customInputs?: {
@ -269,6 +272,7 @@ const BookingPage = ({
.refine((val) => isValidPhoneNumber(val)) .refine((val) => isValidPhoneNumber(val))
.optional() .optional()
.nullable(), .nullable(),
attendeeAddress: z.string().optional().nullable(),
smsReminderNumber: z smsReminderNumber: z
.string() .string()
.refine((val) => isValidPhoneNumber(val)) .refine((val) => isValidPhoneNumber(val))
@ -297,10 +301,10 @@ const BookingPage = ({
const selectedLocation = getEventLocationType(selectedLocationType); const selectedLocation = getEventLocationType(selectedLocationType);
const AttendeeInput = const AttendeeInput =
selectedLocation?.attendeeInputType === "text" selectedLocation?.attendeeInputType === "phone"
? "input"
: selectedLocation?.attendeeInputType === "phone"
? PhoneInput ? PhoneInput
: selectedLocation?.attendeeInputType === "attendeeAddress"
? AddressInput
: null; : null;
// Calculate the booking date(s) // Calculate the booking date(s)
@ -356,6 +360,7 @@ const BookingPage = ({
location: getEventLocationValue(locations, { location: getEventLocationValue(locations, {
type: booking.locationType ? booking.locationType : selectedLocationType || "", type: booking.locationType ? booking.locationType : selectedLocationType || "",
phone: booking.phone, phone: booking.phone,
attendeeAddress: booking.attendeeAddress,
}), }),
metadata, metadata,
customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({ customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({
@ -386,6 +391,7 @@ const BookingPage = ({
location: getEventLocationValue(locations, { location: getEventLocationValue(locations, {
type: (booking.locationType ? booking.locationType : selectedLocationType) || "", type: (booking.locationType ? booking.locationType : selectedLocationType) || "",
phone: booking.phone, phone: booking.phone,
attendeeAddress: booking.attendeeAddress,
}), }),
metadata, metadata,
customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({ customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({
@ -648,16 +654,39 @@ const BookingPage = ({
{AttendeeInput && ( {AttendeeInput && (
<div className="mb-4"> <div className="mb-4">
<label <label
htmlFor="phone" htmlFor={
selectedLocationType === LocationType.Phone
? "phone"
: selectedLocationType === LocationType.AttendeeInPerson
? "attendeeAddress"
: ""
}
className="block text-sm font-medium text-gray-700 dark:text-white"> className="block text-sm font-medium text-gray-700 dark:text-white">
{t("phone_number")} {selectedLocationType === LocationType.Phone
? t("phone_number")
: selectedLocationType === LocationType.AttendeeInPerson
? t("Address")
: ""}
</label> </label>
<div className="mt-1"> <div className="mt-1">
<AttendeeInput<BookingFormValues> <AttendeeInput<BookingFormValues>
control={bookingForm.control} control={bookingForm.control}
name="phone" bookingForm={bookingForm}
name={
selectedLocationType === LocationType.Phone
? "phone"
: selectedLocationType === LocationType.AttendeeInPerson
? "attendeeAddress"
: ""
}
placeholder={t(selectedLocation?.attendeeInputPlaceholder || "")} placeholder={t(selectedLocation?.attendeeInputPlaceholder || "")}
id="phone" id={
selectedLocationType === LocationType.Phone
? "phone"
: selectedLocationType === LocationType.AttendeeInPerson
? "attendeeAddress"
: ""
}
required required
disabled={disableInput} disabled={disableInput}
/> />

View File

@ -293,7 +293,10 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
defaultValue={selection} defaultValue={selection}
options={ options={
booking booking
? locationOptions.filter((location) => location.value !== "phone") ? locationOptions.filter(
(location) =>
location.value !== "phone" && location.value !== "attendeeInPerson"
)
: locationOptions : locationOptions
} }
isSearchable isSearchable

View File

@ -232,6 +232,12 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
const { locationType: newLocation, displayLocationPublicly } = values; const { locationType: newLocation, displayLocationPublicly } = values;
let details = {}; let details = {};
if (newLocation === LocationType.AttendeeInPerson) {
details = {
address: values.locationAddress,
};
}
if (newLocation === LocationType.InPerson) { if (newLocation === LocationType.InPerson) {
details = { details = {
address: values.locationAddress, address: values.locationAddress,

View File

@ -54,7 +54,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
<div className="flex flex-col space-y-8"> <div className="flex flex-col space-y-8">
{/** {/**
* Only display calendar selector if user has connected calendars AND if it's not * Only display calendar selector if user has connected calendars AND if it's not
* a team event. Since we don't have logic to handle each attende calendar (for now). * a team event. Since we don't have logic to handle each attendee calendar (for now).
* This will fallback to each user selected destination calendar. * This will fallback to each user selected destination calendar.
*/} */}
{!!connectedCalendarsQuery.data?.connectedCalendars.length && !team && ( {!!connectedCalendarsQuery.data?.connectedCalendars.length && !team && (
@ -292,7 +292,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
description={t("offer_seats_description")} description={t("offer_seats_description")}
checked={value} checked={value}
onCheckedChange={(e) => { onCheckedChange={(e) => {
// Enabling seats will disable guests and requiring confimation until fully supported // Enabling seats will disable guests and requiring confirmation until fully supported
if (e) { if (e) {
formMethods.setValue("disableGuests", true); formMethods.setValue("disableGuests", true);
formMethods.setValue("requiresConfirmation", false); formMethods.setValue("requiresConfirmation", false);
@ -316,9 +316,10 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
label={t("number_of_seats")} label={t("number_of_seats")}
type="number" type="number"
defaultValue={value || 2} defaultValue={value || 2}
min={1}
addOnSuffix={<>{t("seats")}</>} addOnSuffix={<>{t("seats")}</>}
onChange={(e) => { onChange={(e) => {
onChange(Number(e.target.value)); onChange(Math.abs(Number(e.target.value)));
}} }}
/> />
<div className="mt-2"> <div className="mt-2">

View File

@ -1,7 +1,7 @@
import { TooltipProvider } from "@radix-ui/react-tooltip"; import { TooltipProvider } from "@radix-ui/react-tooltip";
import { SessionProvider } from "next-auth/react"; import { SessionProvider } from "next-auth/react";
import { EventCollectionProvider } from "next-collect/client"; import { EventCollectionProvider } from "next-collect/client";
import { appWithTranslation } from "next-i18next"; import { appWithTranslation, SSRConfig } from "next-i18next";
import { ThemeProvider } from "next-themes"; import { ThemeProvider } from "next-themes";
import type { AppProps as NextAppProps, AppProps as NextJsAppProps } from "next/app"; import type { AppProps as NextAppProps, AppProps as NextJsAppProps } from "next/app";
import { NextRouter } from "next/router"; import { NextRouter } from "next/router";
@ -14,9 +14,9 @@ import { MetaProvider } from "@calcom/ui/v2/core/Meta";
import usePublicPage from "@lib/hooks/usePublicPage"; import usePublicPage from "@lib/hooks/usePublicPage";
const I18nextAdapter = appWithTranslation<NextJsAppProps & { children: React.ReactNode }>(({ children }) => ( const I18nextAdapter = appWithTranslation<NextJsAppProps<SSRConfig> & { children: React.ReactNode }>(
<>{children}</> ({ children }) => <>{children}</>
)); );
// Workaround for https://github.com/vercel/next.js/issues/8592 // Workaround for https://github.com/vercel/next.js/issues/8592
export type AppProps = Omit<NextAppProps, "Component"> & { export type AppProps = Omit<NextAppProps, "Component"> & {

View File

@ -56,6 +56,7 @@ export type FormValues = {
locations: { locations: {
type: EventLocationType["type"]; type: EventLocationType["type"];
address?: string; address?: string;
attendeeAddress?: string;
link?: string; link?: string;
hostPhoneNumber?: string; hostPhoneNumber?: string;
displayLocationPublicly?: boolean; displayLocationPublicly?: boolean;

View File

@ -369,19 +369,8 @@ export default function Success(props: SuccessProps) {
<p className="text-bookinglight">{bookingInfo.user.email}</p> <p className="text-bookinglight">{bookingInfo.user.email}</p>
</div> </div>
)} )}
{!eventType.seatsShowAttendees {bookingInfo?.attendees.map((attendee, index) => (
? bookingInfo?.attendees <div key={attendee.name} className="mb-3 last:mb-0">
.filter((attendee) => attendee.email === email)
.map((attendee) => (
<div key={attendee.name} className="mb-3">
<p>{attendee.name}</p>
<p className="text-bookinglight">{attendee.email}</p>
</div>
))
: bookingInfo?.attendees.map((attendee, index) => (
<div
key={attendee.name}
className={index === bookingInfo.attendees.length - 1 ? "" : "mb-3"}>
<p>{attendee.name}</p> <p>{attendee.name}</p>
<p className="text-bookinglight">{attendee.email}</p> <p className="text-bookinglight">{attendee.email}</p>
</div> </div>
@ -786,6 +775,28 @@ const schema = z.object({
bookingId: strToNumber, bookingId: strToNumber,
}); });
const handleSeatsEventTypeOnBooking = (
eventType: {
seatsPerTimeSlot?: boolean | null;
seatsShowAttendees: boolean | null;
[x: string | number | symbol]: unknown;
},
booking: Partial<
Prisma.BookingGetPayload<{ include: { attendees: { select: { name: true; email: true } } } }>
>,
email: string
) => {
if (eventType?.seatsPerTimeSlot !== null) {
// @TODO: right now bookings with seats doesn't save every description that its entered by every user
delete booking.description;
}
if (!eventType.seatsShowAttendees) {
const attendee = booking?.attendees?.find((a) => a.email === email);
booking["attendees"] = attendee ? [attendee] : [];
}
return;
};
export async function getServerSideProps(context: GetServerSidePropsContext) { export async function getServerSideProps(context: GetServerSidePropsContext) {
const ssr = await ssrInit(context); const ssr = await ssrInit(context);
const parsedQuery = schema.safeParse(context.query); const parsedQuery = schema.safeParse(context.query);
@ -884,6 +895,10 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
}, },
}, },
}); });
if (bookingInfo !== null && email) {
handleSeatsEventTypeOnBooking(eventType, bookingInfo, email);
}
let recurringBookings = null; let recurringBookings = null;
if (recurringEventIdQuery) { if (recurringEventIdQuery) {
// We need to get the dates for the bookings to be able to show them in the UI // We need to get the dates for the bookings to be able to show them in the UI

View File

@ -123,6 +123,13 @@ operators.between.label = "Between";
delete operators.proximity; delete operators.proximity;
delete operators.is_null; delete operators.is_null;
delete operators.is_not_null; delete operators.is_not_null;
/**
* Not supported with JSONLogic. Implement them and add these back -> https://github.com/jwadhams/json-logic-js/issues/81
*/
delete operators.starts_with;
delete operators.ends_with;
const config = { const config = {
conjunctions: BasicConfig.conjunctions, conjunctions: BasicConfig.conjunctions,
operators, operators,

View File

@ -160,6 +160,10 @@ export default class GoogleCalendarService implements Calendar {
async updateEvent(uid: string, event: CalendarEvent, externalCalendarId: string): Promise<any> { async updateEvent(uid: string, event: CalendarEvent, externalCalendarId: string): Promise<any> {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
const myGoogleAuth = await this.auth.getToken(); const myGoogleAuth = await this.auth.getToken();
const eventAttendees = event.attendees.map(({ id, ...rest }) => ({
...rest,
responseStatus: "accepted",
}));
const payload: calendar_v3.Schema$Event = { const payload: calendar_v3.Schema$Event = {
summary: event.title, summary: event.title,
description: getRichDescription(event), description: getRichDescription(event),
@ -179,10 +183,7 @@ export default class GoogleCalendarService implements Calendar {
responseStatus: "accepted", responseStatus: "accepted",
}, },
// eslint-disable-next-line // eslint-disable-next-line
...event.attendees.map(({ id, ...rest }) => ({ ...eventAttendees,
...rest,
responseStatus: "accepted",
})),
], ],
reminders: { reminders: {
useDefault: true, useDefault: true,

View File

@ -16,8 +16,8 @@ export type DefaultEventLocationType = {
iconUrl: string; iconUrl: string;
// HACK: `variable` and `defaultValueVariable` are required due to legacy reason where different locations were stored in different places. // HACK: `variable` and `defaultValueVariable` are required due to legacy reason where different locations were stored in different places.
variable: "locationType" | "locationAddress" | "locationLink" | "locationPhoneNumber" | "phone"; variable: "locationType" | "locationAddress" | "address" | "locationLink" | "locationPhoneNumber" | "phone";
defaultValueVariable: "address" | "link" | "hostPhoneNumber" | "phone"; defaultValueVariable: "address" | "attendeeAddress" | "link" | "hostPhoneNumber" | "phone";
} & ( } & (
| { | {
organizerInputType: "phone" | "text" | null; organizerInputType: "phone" | "text" | null;
@ -26,7 +26,7 @@ export type DefaultEventLocationType = {
attendeeInputPlaceholder?: null; attendeeInputPlaceholder?: null;
} }
| { | {
attendeeInputType: "phone" | "text" | null; attendeeInputType: "phone" | "attendeeAddress" | null;
attendeeInputPlaceholder: string; attendeeInputPlaceholder: string;
organizerInputType?: null; organizerInputType?: null;
organizerInputPlaceholder?: null; organizerInputPlaceholder?: null;
@ -40,6 +40,13 @@ export type EventLocationType = DefaultEventLocationType | EventLocationTypeFrom
export const DailyLocationType = "integrations:daily"; export const DailyLocationType = "integrations:daily";
export enum DefaultEventLocationTypeEnum { export enum DefaultEventLocationTypeEnum {
/**
* Booker Address
*/
AttendeeInPerson = "attendeeInPerson",
/**
* Organizer Address
*/
InPerson = "inPerson", InPerson = "inPerson",
/** /**
* Booker Phone * Booker Phone
@ -53,10 +60,22 @@ export enum DefaultEventLocationTypeEnum {
} }
export const defaultLocations: DefaultEventLocationType[] = [ export const defaultLocations: DefaultEventLocationType[] = [
{
default: true,
type: DefaultEventLocationTypeEnum.AttendeeInPerson,
label: "In Person (Attendee Address)",
variable: "address",
organizerInputType: null,
messageForOrganizer: "Cal will ask your invitee to enter an address before scheduling.",
attendeeInputType: "attendeeAddress",
attendeeInputPlaceholder: `Enter Address`,
defaultValueVariable: "attendeeAddress",
iconUrl: "/map-pin.svg",
},
{ {
default: true, default: true,
type: DefaultEventLocationTypeEnum.InPerson, type: DefaultEventLocationTypeEnum.InPerson,
label: "In Person", label: "In Person (Organizer Address)",
organizerInputType: "text", organizerInputType: "text",
messageForOrganizer: "Provide an Address or Place", messageForOrganizer: "Provide an Address or Place",
// HACK: // HACK:
@ -103,7 +122,7 @@ export const defaultLocations: DefaultEventLocationType[] = [
export type LocationObject = { export type LocationObject = {
type: string; type: string;
displayLocationPublicly?: boolean; displayLocationPublicly?: boolean;
} & Partial<Record<"address" | "link" | "hostPhoneNumber" | "phone", string>>; } & Partial<Record<"address" | "attendeeAddress" | "link" | "hostPhoneNumber" | "phone", string>>;
// integrations:jitsi | 919999999999 | Delhi | https://manual.meeting.link | Around Video // integrations:jitsi | 919999999999 | Delhi | https://manual.meeting.link | Around Video
export type BookingLocationValue = string; export type BookingLocationValue = string;

View File

@ -326,6 +326,7 @@ const ZoomVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =>
const handleZoomResponse = async (response: Response, credentialId: Credential["id"]) => { const handleZoomResponse = async (response: Response, credentialId: Credential["id"]) => {
let _response = response.clone(); let _response = response.clone();
const responseClone = response.clone();
if (_response.headers.get("content-encoding") === "gzip") { if (_response.headers.get("content-encoding") === "gzip") {
const responseString = await response.text(); const responseString = await response.text();
_response = JSON.parse(responseString); _response = JSON.parse(responseString);
@ -339,7 +340,7 @@ const handleZoomResponse = async (response: Response, credentialId: Credential["
throw Error(response.statusText); throw Error(response.statusText);
} }
return response.json(); return responseClone.json();
}; };
const invalidateCredential = async (credentialId: Credential["id"]) => { const invalidateCredential = async (credentialId: Credential["id"]) => {

View File

@ -7,8 +7,14 @@ import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApi
import { getEventLocationTypeFromApp } from "@calcom/app-store/locations"; import { getEventLocationTypeFromApp } from "@calcom/app-store/locations";
import getApps from "@calcom/app-store/utils"; import getApps from "@calcom/app-store/utils";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { Attendee } from "@calcom/prisma/client";
import { createdEventSchema } from "@calcom/prisma/zod-utils"; import { createdEventSchema } from "@calcom/prisma/zod-utils";
import type { AdditionalInformation, CalendarEvent, NewCalendarEventType } from "@calcom/types/Calendar"; import type {
AdditionalInformation,
CalendarEvent,
NewCalendarEventType,
Person,
} from "@calcom/types/Calendar";
import { CredentialPayload, CredentialWithAppName } from "@calcom/types/Credential"; import { CredentialPayload, CredentialWithAppName } from "@calcom/types/Credential";
import type { Event } from "@calcom/types/Event"; import type { Event } from "@calcom/types/Event";
import type { import type {
@ -294,6 +300,7 @@ export default class EventManager {
} }
public async updateCalendarAttendees(event: CalendarEvent, booking: PartialBooking) { public async updateCalendarAttendees(event: CalendarEvent, booking: PartialBooking) {
// @NOTE: This function is only used for updating attendees on a calendar event. Can we remove this?
await this.updateAllCalendarEvents(event, booking); await this.updateAllCalendarEvents(event, booking);
} }
@ -430,11 +437,14 @@ export default class EventManager {
try { try {
// Bookings should only have one calendar reference // Bookings should only have one calendar reference
calendarReference = booking.references.filter((reference) => reference.type.includes("_calendar"))[0]; calendarReference = booking.references.filter((reference) => reference.type.includes("_calendar"))[0];
if (!calendarReference) throw new Error("bookingRef"); if (!calendarReference) {
throw new Error("bookingRef");
}
const { uid: bookingRefUid, externalCalendarId: bookingExternalCalendarId } = calendarReference; const { uid: bookingRefUid, externalCalendarId: bookingExternalCalendarId } = calendarReference;
if (!bookingExternalCalendarId) throw new Error("externalCalendarId"); if (!bookingExternalCalendarId) {
throw new Error("externalCalendarId");
}
let result = []; let result = [];
if (calendarReference.credentialId) { if (calendarReference.credentialId) {
@ -458,6 +468,7 @@ export default class EventManager {
.map(async (cred) => { .map(async (cred) => {
const calendarReference = booking.references.find((ref) => ref.type === cred.type); const calendarReference = booking.references.find((ref) => ref.type === cred.type);
if (!calendarReference) if (!calendarReference)
if (!calendarReference) {
return { return {
appName: cred.appName, appName: cred.appName,
type: cred.type, type: cred.type,
@ -465,6 +476,7 @@ export default class EventManager {
uid: "", uid: "",
originalEvent: event, originalEvent: event,
}; };
}
const { externalCalendarId: bookingExternalCalendarId, meetingId: bookingRefUid } = const { externalCalendarId: bookingExternalCalendarId, meetingId: bookingRefUid } =
calendarReference; calendarReference;
return await updateEvent(cred, event, bookingRefUid ?? null, bookingExternalCalendarId ?? null); return await updateEvent(cred, event, bookingRefUid ?? null, bookingExternalCalendarId ?? null);
@ -474,7 +486,9 @@ export default class EventManager {
return Promise.all(result); return Promise.all(result);
} catch (error) { } catch (error) {
let message = `Tried to 'updateAllCalendarEvents' but there was no '{thing}' for '${credential?.type}', userId: '${credential?.userId}', bookingId: '${booking?.id}'`; let message = `Tried to 'updateAllCalendarEvents' but there was no '{thing}' for '${credential?.type}', userId: '${credential?.userId}', bookingId: '${booking?.id}'`;
if (error instanceof Error) message = message.replace("{thing}", error.message); if (error instanceof Error) {
message = message.replace("{thing}", error.message);
}
console.error(message); console.error(message);
return Promise.resolve([ return Promise.resolve([
{ {

View File

@ -47,7 +47,7 @@
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"postcss": "^8.4.18", "postcss": "^8.4.18",
"typescript": "^4.7.4", "typescript": "^4.7.4",
"vite": "^3.1.0", "vite": "^2.9.15",
"tailwindcss": "^3.2.1" "tailwindcss": "^3.2.1"
} }
} }

View File

@ -47,7 +47,7 @@
"eslint": "^8.22.0", "eslint": "^8.22.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"typescript": "^4.7.4", "typescript": "^4.7.4",
"vite": "^3.1.0" "vite": "^2.9.15"
}, },
"dependencies": { "dependencies": {
"@calcom/embed-core": "*", "@calcom/embed-core": "*",

View File

@ -7,6 +7,7 @@ import {
WebhookTriggerEvents, WebhookTriggerEvents,
} from "@prisma/client"; } from "@prisma/client";
import async from "async"; import async from "async";
import { cloneDeep } from "lodash";
import type { NextApiRequest } from "next"; import type { NextApiRequest } from "next";
import { RRule } from "rrule"; import { RRule } from "rrule";
import short from "short-uuid"; import short from "short-uuid";
@ -482,8 +483,9 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) {
// For seats, if the booking already exists then we want to add the new attendee to the existing booking // For seats, if the booking already exists then we want to add the new attendee to the existing booking
if (reqBody.bookingUid) { if (reqBody.bookingUid) {
if (!eventType.seatsPerTimeSlot) if (!eventType.seatsPerTimeSlot) {
throw new HttpError({ statusCode: 404, message: "Event type does not have seats" }); throw new HttpError({ statusCode: 404, message: "Event type does not have seats" });
}
const booking = await prisma.booking.findUnique({ const booking = await prisma.booking.findUnique({
where: { where: {
@ -505,7 +507,9 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) {
}, },
}, },
}); });
if (!booking) throw new HttpError({ statusCode: 404, message: "Booking not found" }); if (!booking) {
throw new HttpError({ statusCode: 404, message: "Booking not found" });
}
// Need to add translation for attendees to pass type checks. Since these values are never written to the db we can just use the new attendee language // Need to add translation for attendees to pass type checks. Since these values are never written to the db we can just use the new attendee language
const bookingAttendees = booking.attendees.map((attendee) => { const bookingAttendees = booking.attendees.map((attendee) => {
@ -514,11 +518,13 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) {
evt = { ...evt, attendees: [...bookingAttendees, invitee[0]] }; evt = { ...evt, attendees: [...bookingAttendees, invitee[0]] };
if (eventType.seatsPerTimeSlot <= booking.attendees.length) if (eventType.seatsPerTimeSlot <= booking.attendees.length) {
throw new HttpError({ statusCode: 409, message: "Booking seats are full" }); throw new HttpError({ statusCode: 409, message: "Booking seats are full" });
}
if (booking.attendees.some((attendee) => attendee.email === invitee[0].email)) if (booking.attendees.find((attendee) => attendee.email === invitee[0].email)) {
throw new HttpError({ statusCode: 409, message: "Already signed up for time slot" }); throw new HttpError({ statusCode: 409, message: "Already signed up for time slot" });
}
await prisma.booking.update({ await prisma.booking.update({
where: { where: {
@ -537,8 +543,13 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) {
}); });
const newSeat = booking.attendees.length !== 0; const newSeat = booking.attendees.length !== 0;
/**
await sendScheduledSeatsEmails(evt, invitee[0], newSeat, !!eventType.seatsShowAttendees); * Remember objects are passed into functions as references
* so if you modify it in a inner function it will be modified in the outer function
* deep cloning evt to avoid this
*/
const copyEvent = cloneDeep(evt);
await sendScheduledSeatsEmails(copyEvent, invitee[0], newSeat, !!eventType.seatsShowAttendees);
const credentials = await refreshCredentials(organizerUser.credentials); const credentials = await refreshCredentials(organizerUser.credentials);
const eventManager = new EventManager({ ...organizerUser, credentials }); const eventManager = new EventManager({ ...organizerUser, credentials });

View File

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import { CAL_URL } from "./constants"; import { CAL_URL, LOGO } from "./constants";
// Ensures tw prop is typed. // Ensures tw prop is typed.
declare module "react" { declare module "react" {
@ -119,7 +119,7 @@ export const Meeting = ({ title, users = [], profile }: MeetingImageProps) => {
<Wrapper variant="dark"> <Wrapper variant="dark">
<div tw="h-full flex flex-col justify-start"> <div tw="h-full flex flex-col justify-start">
<div tw="flex items-center justify-center" style={{ fontFamily: "cal", fontWeight: 300 }}> <div tw="flex items-center justify-center" style={{ fontFamily: "cal", fontWeight: 300 }}>
<img src={`${CAL_URL}/cal-logo-word-black.svg`} width="350" alt="Logo" /> <img src={`${CAL_URL}/${LOGO}`} width="350" alt="Logo" />
{avatars.length > 0 && <div tw="font-bold text-black text-[92px] mx-8 bottom-2">/</div>} {avatars.length > 0 && <div tw="font-bold text-black text-[92px] mx-8 bottom-2">/</div>}
<div tw="flex flex-row"> <div tw="flex flex-row">
{avatars.slice(0, 3).map((avatar) => ( {avatars.slice(0, 3).map((avatar) => (
@ -189,12 +189,7 @@ const VisualBlur = ({ visualSlug }: { visualSlug: string }) => {
export const App = ({ name, description, slug }: AppImageProps) => ( export const App = ({ name, description, slug }: AppImageProps) => (
<Wrapper> <Wrapper>
<img <img src={`${CAL_URL}/${LOGO}`} width="150" alt="Logo" tw="absolute right-[48px] top-[48px]" />
src={`${CAL_URL}/cal-logo-word-black.svg`}
width="150"
alt="Logo"
tw="absolute right-[48px] top-[48px]"
/>
<VisualBlur visualSlug={slug} /> <VisualBlur visualSlug={slug} />

View File

@ -152,7 +152,7 @@ const loggedInViewerRouter = createProtectedRouter()
locale: user.locale, locale: user.locale,
timeFormat: user.timeFormat, timeFormat: user.timeFormat,
timeZone: user.timeZone, timeZone: user.timeZone,
avatar: user.avatar, avatar: `${CAL_URL}/${user.username}/avatar.png`,
createdDate: user.createdDate, createdDate: user.createdDate,
trialEndsAt: user.trialEndsAt, trialEndsAt: user.trialEndsAt,
completedOnboarding: user.completedOnboarding, completedOnboarding: user.completedOnboarding,

View File

@ -0,0 +1,51 @@
import { UseFormReturn } from "react-hook-form";
import { Props } from "react-phone-number-input/react-hook-form";
import { EventLocationType } from "@calcom/app-store/locations";
import { Icon } from "../Icon";
type BookingFormValues = {
name: string;
email: string;
notes?: string;
locationType?: EventLocationType["type"];
guests?: string[];
address?: string;
attendeeAddress?: string;
phone?: string;
hostPhoneNumber?: string; // Maybe come up with a better way to name this to distingish between two types of phone numbers
customInputs?: {
[key: string]: string | boolean;
};
rescheduleReason?: string;
smsReminderNumber?: string;
};
export type AddressInputProps<FormValues> = Props<
{
value: string;
id: string;
placeholder: string;
required: boolean;
bookingForm: UseFormReturn<BookingFormValues>;
},
FormValues
>;
function AddressInput<FormValues>({ bookingForm, name, className, ...rest }: AddressInputProps<FormValues>) {
return (
<div className="relative ">
<Icon.FiMapPin color="#D2D2D2" className="absolute top-1/2 left-0.5 ml-3 h-6 -translate-y-1/2" />
<input
{...rest}
{...bookingForm.register("attendeeAddress")}
name={name}
color="#D2D2D2"
className={`${className} border-1 focus-within:border-brand dark:bg-darkgray-100 dark:border-darkgray-300 block h-10 w-full rounded-md border border-gray-300 py-px pl-10 text-sm outline-none ring-black focus-within:ring-1 disabled:text-gray-500 disabled:opacity-50 dark:text-white dark:placeholder-gray-500 dark:selection:bg-green-500 disabled:dark:text-gray-500`}
/>
</div>
);
}
export default AddressInput;

View File

@ -0,0 +1,8 @@
import dynamic from "next/dynamic";
/** These are like 40kb that not every user needs */
const AddressInput = dynamic(
() => import("./AddressInput")
) as unknown as typeof import("./AddressInput").default;
export default AddressInput;