save additional inputs as json + view details of booking (#2796)

* move custom inputs from description to own json object

* show custom inputs on success page

* fix type error

* add custom inputs to email and webhook

* add custom inputs to all emails

* add values for custom inputs when rescheduling

* add custom input everywhere description is shown

* fix bug with boolean value

* fix issues with null values

* disable custom inputs and add notes for organizer

* don't show custom input with empty string

* don't show custom inputs with empty string in calender event and email

* add link to booking details page

* redirect to success page to see booking details

* add functionality to cancel and reschedule booking

* fix bookings that require confirmation

* clean code

* fix infinite lopp in useEffect of success page

* show web conference details message when integration as location

* improve design of cancelling event

* clean code

* disable darkmode for organizer on booking details page

* fix dark mode for cancelling booking

* fix build error

* Fixes infinite loop

* Fixes infinite loop

* Fixes infinite loop

* Update all Yarn dependencies (2022-05-16) (#2769)

* Update all Yarn dependencies (2022-05-16)

* Upgrade dependencies

* Removes deprecated packages

* Upgrades deps

* Updates submodules

* Update yarn.lock

* Linting

* Linting

* Update website

* Build fixes

* TODO: fix this

* Module resolving

* Type fixes

* Intercom fixes on SSG

* Fixes infinite loop

* Upgrades to React 18

* Type fixes

* Locks node version to 14

* Upgrades daily-js

* Readds missing types

* Upgrades playwright

* Noop when intercom is not installed

* Update website

* Removed yarn.lock in favor of monorepo

Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>

* Create ci.yml

* Update ci.yml

* Reintroduces typescript-eslint

Buckle up!

* Type fixes

* Update ci.yml

* Update api

* Update admin

* Reusable inferSSRProps

* Linting

* Linting

* Prisma fixes

* Update ci.yml

* Cache testing

* Update e2e.yml

* Update DatePicker.tsx

* Update e2e.yml

* Revert "Linting"

This reverts commit adf817766e.

* Revert "Linting"

This reverts commit 1b59dacd64.

* Linting

* Update e2e.yml

* Ci updates

* Add team Id to hash url (#2803)

* Fix missing tabs - Embed (#2804)

* Fix missing tabs

* Fix Eslint error

* Fix Eslint errors

* Add import statement (#2812)

* Add import statement

* Update apps/docs/next.config.js

Co-authored-by: Omar López <zomars@me.com>

* Show success page if booking was deleted on calendar (#2808)

* Add exception to 410

* Fix type error

* Add GoogelCalError type

* only show invite link for app.cal.dev (#2807)

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Omar López <zomars@me.com>

* fix: update eslint config to test .ts and .js separately (#2805)

* fix: update eslint config

* fix: update ts ignore

* fix: update eslint config

* Update TeamAvailabilityScreen.tsx

* Type fixes

* Update useIntercom.ts

Co-authored-by: Omar López <zomars@me.com>

* fix: sync api to latest commit (#2810)

Co-authored-by: Agusti Fernandez Pardo <git@agusti.me>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>

* Embed React improvements (#2782)

* Add off support. Add getApi export.

* Add publish command

* Add embed-snippet in prod deps

* Update README

* Update package.json

Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>

* Consolidates test-results

* Type fixes

* Abstracts minimal booking select

* Type fixes

* Update listBookings.ts

* Update common.json

* Update bookingReminder.ts

* Consolidates isOutOfBounds

* Update webhookResponse-chromium.txt

* Update TableActions.tsx

* Type fixes

* Update BookingPage.tsx

* Update webhookResponse-chromium.txt

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: iamkun <kunhello@outlook.com>
Co-authored-by: Agusti Fernandez Pardo <me@agusti.me>
Co-authored-by: Agusti Fernandez Pardo <git@agusti.me>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
Carina Wollendorfer 2022-05-18 23:05:49 +02:00 committed by Joe Au-Yeung
parent 8455945761
commit e7f1a829fd
46 changed files with 521 additions and 207 deletions

View File

@ -1,8 +1,15 @@
import { BanIcon, CheckIcon, ClockIcon, XIcon, PencilAltIcon } from "@heroicons/react/outline"; import {
import { PaperAirplaneIcon } from "@heroicons/react/outline"; BanIcon,
CheckIcon,
ClockIcon,
PaperAirplaneIcon,
PencilAltIcon,
XIcon,
} from "@heroicons/react/outline";
import { RefreshIcon } from "@heroicons/react/solid"; import { RefreshIcon } from "@heroicons/react/solid";
import { BookingStatus } from "@prisma/client"; import { BookingStatus } from "@prisma/client";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useRouter } from "next/router";
import { useState } from "react"; import { useState } from "react";
import { useMutation } from "react-query"; import { useMutation } from "react-query";
import { Frequency as RRuleFrequency } from "rrule"; import { Frequency as RRuleFrequency } from "rrule";
@ -17,7 +24,7 @@ import { TextArea } from "@calcom/ui/form/fields";
import { HttpError } from "@lib/core/http/error"; import { HttpError } from "@lib/core/http/error";
import useMeQuery from "@lib/hooks/useMeQuery"; import useMeQuery from "@lib/hooks/useMeQuery";
import { parseRecurringDates } from "@lib/parseDate"; import { parseRecurringDates } from "@lib/parseDate";
import { inferQueryOutput, trpc, inferQueryInput } from "@lib/trpc"; import { inferQueryInput, inferQueryOutput, trpc } from "@lib/trpc";
import { RescheduleDialog } from "@components/dialog/RescheduleDialog"; import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
import TableActions, { ActionType } from "@components/ui/TableActions"; import TableActions, { ActionType } from "@components/ui/TableActions";
@ -37,6 +44,7 @@ function BookingListItem(booking: BookingItemProps) {
const user = query.data; const user = query.data;
const { t, i18n } = useLocale(); const { t, i18n } = useLocale();
const utils = trpc.useContext(); const utils = trpc.useContext();
const router = useRouter();
const [rejectionReason, setRejectionReason] = useState<string>(""); const [rejectionReason, setRejectionReason] = useState<string>("");
const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false); const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false);
const mutation = useMutation( const mutation = useMutation(
@ -81,7 +89,10 @@ function BookingListItem(booking: BookingItemProps) {
booking.listingStatus === "upcoming" && booking.recurringEventId !== null booking.listingStatus === "upcoming" && booking.recurringEventId !== null
? t("reject_all") ? t("reject_all")
: t("reject"), : t("reject"),
onClick: () => setRejectionDialogIsOpen(true), onClick: (e) => {
e.stopPropagation();
setRejectionDialogIsOpen(true);
},
icon: BanIcon, icon: BanIcon,
disabled: mutation.isLoading, disabled: mutation.isLoading,
}, },
@ -91,7 +102,10 @@ function BookingListItem(booking: BookingItemProps) {
booking.listingStatus === "upcoming" && booking.recurringEventId !== null booking.listingStatus === "upcoming" && booking.recurringEventId !== null
? t("confirm_all") ? t("confirm_all")
: t("confirm"), : t("confirm"),
onClick: () => mutation.mutate(true), onClick: (e) => {
e.stopPropagation();
mutation.mutate(true);
},
icon: CheckIcon, icon: CheckIcon,
disabled: mutation.isLoading, disabled: mutation.isLoading,
color: "primary", color: "primary",
@ -120,7 +134,10 @@ function BookingListItem(booking: BookingItemProps) {
id: "reschedule_request", id: "reschedule_request",
icon: ClockIcon, icon: ClockIcon,
label: t("send_reschedule_request"), label: t("send_reschedule_request"),
onClick: () => setIsOpenRescheduleDialog(true), onClick: (e) => {
e.stopPropagation();
setIsOpenRescheduleDialog(true);
},
}, },
], ],
}, },
@ -150,6 +167,7 @@ function BookingListItem(booking: BookingItemProps) {
i18n i18n
); );
} }
return ( return (
<> <>
<RescheduleDialog <RescheduleDialog
@ -191,7 +209,30 @@ function BookingListItem(booking: BookingItemProps) {
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<tr className="flex"> <tr
className="flex cursor-pointer hover:bg-neutral-50"
onClick={() =>
router.push({
pathname: "/success",
query: {
date: booking.startTime,
type: booking.eventType.id,
eventSlug: booking.eventType.slug,
user: user?.username || "",
name: booking.attendees[0].name,
email: booking.attendees[0].email,
location: booking.location
? booking.location.includes("integration")
? (t("web_conferencing_details_to_follow") as string)
: booking.location
: "",
eventName: booking.eventType.eventName || "",
bookingId: booking.id,
recur: booking.recurringEventId,
reschedule: booking.confirmed,
},
})
}>
<td className="hidden whitespace-nowrap py-4 align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:w-56"> <td className="hidden whitespace-nowrap py-4 align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:w-56">
<div className="text-sm leading-6 text-gray-900">{startTime}</div> <div className="text-sm leading-6 text-gray-900">{startTime}</div>
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500">
@ -264,9 +305,12 @@ function BookingListItem(booking: BookingItemProps) {
)} )}
{booking.attendees.length !== 0 && ( {booking.attendees.length !== 0 && (
<div className="text-sm text-gray-900 hover:text-blue-500"> <a
<a href={"mailto:" + booking.attendees[0].email}>{booking.attendees[0].email}</a> className="text-sm text-gray-900 hover:text-blue-500"
</div> href={"mailto:" + booking.attendees[0].email}
onClick={(e) => e.stopPropagation()}>
{booking.attendees[0].email}
</a>
)} )}
{isCancelled && booking.rescheduled && ( {isCancelled && booking.rescheduled && (
<div className="mt-2 inline-block text-left text-sm md:hidden"> <div className="mt-2 inline-block text-left text-sm md:hidden">

View File

@ -0,0 +1,119 @@
import { XIcon } from "@heroicons/react/solid";
import { useRouter } from "next/router";
import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "@calcom/ui/Button";
import useTheme from "@lib/hooks/useTheme";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
type Props = {
booking: {
title?: string;
uid?: string;
};
profile: {
name: string | null;
slug: string | null;
};
team?: string | null;
setIsCancellationMode: (value: boolean) => void;
theme: string | null;
};
export default function CancelBooking(props: Props) {
const [cancellationReason, setCancellationReason] = useState<string>("");
const { t } = useLocale();
const router = useRouter();
const { booking, profile, team } = props;
const [loading, setLoading] = useState(false);
const telemetry = useTelemetry();
const [error, setError] = useState<string | null>(booking ? null : t("booking_already_cancelled"));
const { isReady, Theme } = useTheme(props.theme);
if (isReady) {
return (
<>
<Theme />
{error && (
<div>
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
<XIcon className="h-6 w-6 text-red-600" />
</div>
<div className="mt-3 text-center sm:mt-5">
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
{error}
</h3>
</div>
</div>
)}
{!error && (
<div className="mt-5 sm:mt-6">
<label className="text-bookingdark font-medium dark:text-white">{t("cancellation_reason")}</label>
<textarea
placeholder={t("cancellation_reason_placeholder")}
value={cancellationReason}
onChange={(e) => setCancellationReason(e.target.value)}
className="mt-2 mb-3 w-full dark:border-gray-900 dark:bg-gray-700 dark:text-white sm:mb-3 "
rows={3}
/>
<div className="flex rtl:space-x-reverse">
<div className="w-full">
<Button color="secondary" onClick={() => router.push("/reschedule/" + booking?.uid)}>
{t("reschedule_this")}
</Button>
</div>
<div className="w-full space-x-2 text-right">
<Button color="secondary" onClick={() => props.setIsCancellationMode(false)}>
{t("nevermind")}
</Button>
<Button
data-testid="cancel"
onClick={async () => {
setLoading(true);
const payload = {
uid: booking?.uid,
reason: cancellationReason,
};
telemetry.withJitsu((jitsu) =>
jitsu.track(telemetryEventTypes.bookingCancelled, collectPageParameters())
);
const res = await fetch("/api/cancel", {
body: JSON.stringify(payload),
headers: {
"Content-Type": "application/json",
},
method: "DELETE",
});
if (res.status >= 200 && res.status < 300) {
await router.push(
`/cancel/success?name=${props.profile.name}&title=${booking?.title}&eventPage=${
profile.slug
}&team=${team ? 1 : 0}`
);
} else {
setLoading(false);
setError(
`${t("error_with_status_code_occured", { status: res.status })} ${t(
"please_try_again"
)}`
);
}
}}
loading={loading}>
{t("cancel_event")}
</Button>
</div>
</div>
</div>
)}
</>
);
}
return <></>;
}

View File

@ -1,5 +1,5 @@
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/solid"; import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/solid";
import { EventType, PeriodType } from "@prisma/client"; import { PeriodType } from "@prisma/client";
import dayjs, { Dayjs } from "dayjs"; import dayjs, { Dayjs } from "dayjs";
import dayjsBusinessTime from "dayjs-business-days2"; import dayjsBusinessTime from "dayjs-business-days2";
import timezone from "dayjs/plugin/timezone"; import timezone from "dayjs/plugin/timezone";
@ -14,6 +14,7 @@ import classNames from "@lib/classNames";
import { timeZone } from "@lib/clock"; import { timeZone } from "@lib/clock";
import { weekdayNames } from "@lib/core/i18n/weekday"; import { weekdayNames } from "@lib/core/i18n/weekday";
import { doWorkAsync } from "@lib/doWorkAsync"; import { doWorkAsync } from "@lib/doWorkAsync";
import isOutOfBounds from "@lib/isOutOfBounds";
import getSlots from "@lib/slots"; import getSlots from "@lib/slots";
import { WorkingHours } from "@lib/types/schedule"; import { WorkingHours } from "@lib/types/schedule";
@ -37,42 +38,6 @@ type DatePickerProps = {
minimumBookingNotice: number; minimumBookingNotice: number;
}; };
function isOutOfBounds(
time: dayjs.ConfigType,
{
periodType,
periodDays,
periodCountCalendarDays,
periodStartDate,
periodEndDate,
}: Pick<
EventType,
"periodType" | "periodDays" | "periodCountCalendarDays" | "periodStartDate" | "periodEndDate"
>
) {
const date = dayjs(time);
if (!periodDays) return false;
switch (periodType) {
case PeriodType.ROLLING: {
const periodRollingEndDay = periodCountCalendarDays
? dayjs().utcOffset(date.utcOffset()).add(periodDays, "days").endOf("day")
: dayjs().utcOffset(date.utcOffset()).businessDaysAdd(periodDays).endOf("day");
return date.endOf("day").isAfter(periodRollingEndDay);
}
case PeriodType.RANGE: {
const periodRangeStartDay = dayjs(periodStartDate).utcOffset(date.utcOffset()).endOf("day");
const periodRangeEndDay = dayjs(periodEndDate).utcOffset(date.utcOffset()).endOf("day");
return date.endOf("day").isBefore(periodRangeStartDay) || date.endOf("day").isAfter(periodRangeEndDay);
}
case PeriodType.UNLIMITED:
default:
return false;
}
}
function DatePicker({ function DatePicker({
weekStart, weekStart,
onDatePicked, onDatePicked,

View File

@ -77,7 +77,7 @@ type BookingFormValues = {
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?: {
[key: string]: string; [key: string]: string | boolean;
}; };
}; };
@ -216,7 +216,7 @@ const BookingPage = ({
}, [router.query.guest]); }, [router.query.guest]);
const locationInfo = (type: LocationType) => locations.find((location) => location.type === type); const locationInfo = (type: LocationType) => locations.find((location) => location.type === type);
const loggedInIsOwner = eventType?.users[0]?.name === session?.user?.name; const loggedInIsOwner = eventType?.users[0]?.id === session?.user?.id;
const guestListEmails = !isDynamicGroupBooking const guestListEmails = !isDynamicGroupBooking
? booking?.attendees.slice(1).map((attendee) => attendee.email) ? booking?.attendees.slice(1).map((attendee) => attendee.email)
: []; : [];
@ -244,11 +244,22 @@ const BookingPage = ({
if (!primaryAttendee) { if (!primaryAttendee) {
return {}; return {};
} }
const customInputType = booking.customInputs;
return { return {
name: primaryAttendee.name || "", name: primaryAttendee.name || "",
email: primaryAttendee.email || "", email: primaryAttendee.email || "",
guests: guestListEmails, guests: guestListEmails,
notes: booking.description || "", notes: booking.description || "",
customInputs: eventType.customInputs.reduce(
(customInputs, input) => ({
...customInputs,
[input.id]: booking.customInputs
? booking.customInputs[input.label as keyof typeof customInputType]
: "",
}),
{}
),
}; };
}; };
@ -400,6 +411,9 @@ const BookingPage = ({
}; };
const disableInput = !!rescheduleUid; const disableInput = !!rescheduleUid;
const disabledExceptForOwner = disableInput && !loggedInIsOwner;
const inputClassName =
"focus:border-brand block w-full rounded-sm border-gray-300 shadow-sm focus:ring-black disabled:bg-gray-200 disabled:hover:cursor-not-allowed dark:border-gray-900 dark:bg-gray-700 dark:text-white dark:selection:bg-green-500 disabled:dark:text-gray-500 sm:text-sm";
return ( return (
<div> <div>
@ -541,10 +555,7 @@ const BookingPage = ({
name="name" name="name"
id="name" id="name"
required required
className={classNames( className={inputClassName}
"focus:border-brand block w-full rounded-sm border-gray-300 shadow-sm focus:ring-black dark:border-gray-900 dark:bg-gray-700 dark:text-white dark:selection:bg-green-500 sm:text-sm",
disableInput ? "bg-gray-200 dark:text-gray-500" : ""
)}
placeholder={t("example_name")} placeholder={t("example_name")}
disabled={disableInput} disabled={disableInput}
/> />
@ -561,8 +572,7 @@ const BookingPage = ({
{...bookingForm.register("email")} {...bookingForm.register("email")}
required required
className={classNames( className={classNames(
"focus:border-brand block w-full rounded-sm shadow-sm focus:ring-black dark:bg-gray-700 dark:text-white dark:selection:bg-green-500 sm:text-sm", inputClassName,
disableInput ? "bg-gray-200 dark:text-gray-500" : "",
bookingForm.formState.errors.email bookingForm.formState.errors.email
? "border-red-700 focus:ring-red-700" ? "border-red-700 focus:ring-red-700"
: " border-gray-300 dark:border-gray-900" : " border-gray-300 dark:border-gray-900"
@ -637,12 +647,9 @@ const BookingPage = ({
})} })}
id={"custom_" + input.id} id={"custom_" + input.id}
rows={3} rows={3}
className={classNames( className={inputClassName}
"focus:border-brand block w-full rounded-sm border-gray-300 shadow-sm focus:ring-black dark:border-gray-900 dark:bg-gray-700 dark:text-white dark:selection:bg-green-500 sm:text-sm",
disableInput ? "bg-gray-200 dark:text-gray-500" : ""
)}
placeholder={input.placeholder} placeholder={input.placeholder}
disabled={disableInput} disabled={disabledExceptForOwner}
/> />
)} )}
{input.type === EventTypeCustomInputType.TEXT && ( {input.type === EventTypeCustomInputType.TEXT && (
@ -652,9 +659,9 @@ const BookingPage = ({
required: input.required, required: input.required,
})} })}
id={"custom_" + input.id} id={"custom_" + input.id}
className="focus:border-brand block w-full rounded-sm border-gray-300 shadow-sm focus:ring-black dark:border-gray-900 dark:bg-gray-700 dark:text-white dark:selection:bg-green-500 sm:text-sm" className={inputClassName}
placeholder={input.placeholder} placeholder={input.placeholder}
disabled={disableInput} disabled={disabledExceptForOwner}
/> />
)} )}
{input.type === EventTypeCustomInputType.NUMBER && ( {input.type === EventTypeCustomInputType.NUMBER && (
@ -664,8 +671,9 @@ const BookingPage = ({
required: input.required, required: input.required,
})} })}
id={"custom_" + input.id} id={"custom_" + input.id}
className="focus:border-brand block w-full rounded-sm border-gray-300 shadow-sm focus:ring-black dark:border-gray-900 dark:bg-gray-700 dark:text-white dark:selection:bg-green-500 sm:text-sm" className={inputClassName}
placeholder="" placeholder=""
disabled={disabledExceptForOwner}
/> />
)} )}
{input.type === EventTypeCustomInputType.BOOL && ( {input.type === EventTypeCustomInputType.BOOL && (
@ -676,8 +684,9 @@ const BookingPage = ({
required: input.required, required: input.required,
})} })}
id={"custom_" + input.id} id={"custom_" + input.id}
className="h-4 w-4 rounded border-gray-300 text-black focus:ring-black ltr:mr-2 rtl:ml-2" className="h-4 w-4 rounded border-gray-300 text-black focus:ring-black disabled:bg-gray-200 ltr:mr-2 rtl:ml-2 disabled:dark:text-gray-500"
placeholder="" placeholder=""
disabled={disabledExceptForOwner}
/> />
<label <label
htmlFor={"custom_" + input.id} htmlFor={"custom_" + input.id}
@ -764,12 +773,9 @@ const BookingPage = ({
id="notes" id="notes"
name="notes" name="notes"
rows={3} rows={3}
className={classNames( className={inputClassName}
"focus:border-brand block w-full rounded-sm border-gray-300 shadow-sm focus:ring-black dark:border-gray-900 dark:bg-gray-700 dark:text-white dark:selection:bg-green-500 sm:text-sm",
disableInput ? "bg-gray-200 dark:text-gray-500" : ""
)}
placeholder={t("share_additional_notes")} placeholder={t("share_additional_notes")}
disabled={disableInput} disabled={disabledExceptForOwner}
/> />
</div> </div>
<div className="flex items-start space-x-2 rtl:space-x-reverse"> <div className="flex items-start space-x-2 rtl:space-x-reverse">

View File

@ -2,7 +2,7 @@ import { ChevronDownIcon, DotsHorizontalIcon } from "@heroicons/react/solid";
import React, { FC } from "react"; import React, { FC } from "react";
import Button from "@calcom/ui/Button"; import Button from "@calcom/ui/Button";
import Dropdown, { DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from "@calcom/ui/Dropdown"; import Dropdown, { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@calcom/ui/Dropdown";
import { SVGComponent } from "@lib/types/SVGComponent"; import { SVGComponent } from "@lib/types/SVGComponent";
@ -12,15 +12,27 @@ export type ActionType = {
label: string; label: string;
disabled?: boolean; disabled?: boolean;
color?: "primary" | "secondary"; color?: "primary" | "secondary";
} & ({ href?: never; onClick: () => any } | { href?: string; onClick?: never }) & { } & (
actions?: ActionType[]; | { href: string; onClick?: never; actions?: never }
}; | { href?: never; onClick: (e: React.MouseEvent<HTMLElement, MouseEvent>) => void; actions?: never }
| { actions?: ActionType[]; href?: never; onClick?: never }
);
interface Props { interface Props {
actions: ActionType[]; actions: ActionType[];
} }
const DropdownActions = ({ actions, actionTrigger }: { actions: ActionType[]; actionTrigger?: any }) => { const defaultAction = (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
e.stopPropagation();
};
const DropdownActions = ({
actions,
actionTrigger,
}: {
actions: ActionType[];
actionTrigger?: React.ReactNode;
}) => {
return ( return (
<Dropdown> <Dropdown>
{!actionTrigger ? ( {!actionTrigger ? (
@ -40,7 +52,7 @@ const DropdownActions = ({ actions, actionTrigger }: { actions: ActionType[]; ac
className="w-full rounded-none font-normal" className="w-full rounded-none font-normal"
href={action.href} href={action.href}
StartIcon={action.icon} StartIcon={action.icon}
onClick={action.onClick} onClick={action.onClick || defaultAction}
data-testid={action.id}> data-testid={action.id}>
{action.label} {action.label}
</Button> </Button>
@ -67,7 +79,7 @@ const TableActions: FC<Props> = ({ actions }) => {
key={action.id} key={action.id}
data-testid={action.id} data-testid={action.id}
href={action.href} href={action.href}
onClick={action.onClick} onClick={action.onClick || defaultAction}
StartIcon={action.icon} StartIcon={action.icon}
{...(action?.actions ? { EndIcon: ChevronDownIcon } : null)} {...(action?.actions ? { EndIcon: ChevronDownIcon } : null)}
disabled={action.disabled} disabled={action.disabled}

View File

@ -4,8 +4,9 @@ import type { NextApiRequest, NextApiResponse } from "next";
import Stripe from "stripe"; import Stripe from "stripe";
import EventManager from "@calcom/core/EventManager"; import EventManager from "@calcom/core/EventManager";
import { isPrismaObjOrUndefined } from "@calcom/lib";
import { getErrorFromUnknown } from "@calcom/lib/errors"; import { getErrorFromUnknown } from "@calcom/lib/errors";
import prisma from "@calcom/prisma"; import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import stripe from "@calcom/stripe/server"; import stripe from "@calcom/stripe/server";
import { CalendarEvent, RecurringEvent } from "@calcom/types/Calendar"; import { CalendarEvent, RecurringEvent } from "@calcom/types/Calendar";
@ -42,16 +43,11 @@ async function handlePaymentSuccess(event: Stripe.Event) {
id: payment.bookingId, id: payment.bookingId,
}, },
select: { select: {
title: true, ...bookingMinimalSelect,
description: true,
startTime: true,
endTime: true,
confirmed: true, confirmed: true,
attendees: true,
location: true, location: true,
eventTypeId: true, eventTypeId: true,
userId: true, userId: true,
id: true,
uid: true, uid: true,
paid: true, paid: true,
destinationCalendar: true, destinationCalendar: true,
@ -113,8 +109,9 @@ async function handlePaymentSuccess(event: Stripe.Event) {
description: booking.description || undefined, description: booking.description || undefined,
startTime: booking.startTime.toISOString(), startTime: booking.startTime.toISOString(),
endTime: booking.endTime.toISOString(), endTime: booking.endTime.toISOString(),
customInputs: isPrismaObjOrUndefined(booking.customInputs),
organizer: { organizer: {
email: user.email!, email: user.email,
name: user.name!, name: user.name!,
timeZone: user.timeZone, timeZone: user.timeZone,
language: { translate: t, locale: user.locale ?? "en" }, language: { translate: t, locale: user.locale ?? "en" },

View File

@ -50,6 +50,7 @@ ${this.getWhen()}
${this.getLocation()} ${this.getLocation()}
${this.getDescription()} ${this.getDescription()}
${this.getAdditionalNotes()} ${this.getAdditionalNotes()}
${this.getCustomInputs()}
`.replace(/(<([^>]+)>)/gi, ""); `.replace(/(<([^>]+)>)/gi, "");
} }
@ -97,6 +98,7 @@ ${this.getAdditionalNotes()}
${this.getLocation()} ${this.getLocation()}
${this.getDescription()} ${this.getDescription()}
${this.getAdditionalNotes()} ${this.getAdditionalNotes()}
${this.getCustomInputs()}
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -49,6 +49,7 @@ ${this.getWhen()}
${this.getLocation()} ${this.getLocation()}
${this.getDescription()} ${this.getDescription()}
${this.getAdditionalNotes()} ${this.getAdditionalNotes()}
${this.getCustomInputs()}
${this.calEvent.cancellationReason && this.getCancellationReason()} ${this.calEvent.cancellationReason && this.getCancellationReason()}
`.replace(/(<([^>]+)>)/gi, ""); `.replace(/(<([^>]+)>)/gi, "");
} }
@ -98,6 +99,7 @@ ${this.calEvent.cancellationReason && this.getCancellationReason()}
${this.getLocation()} ${this.getLocation()}
${this.getDescription()} ${this.getDescription()}
${this.getAdditionalNotes()} ${this.getAdditionalNotes()}
${this.getCustomInputs()}
${this.calEvent.cancellationReason && this.getCancellationReason()} ${this.calEvent.cancellationReason && this.getCancellationReason()}
</div> </div>
</td> </td>

View File

@ -51,6 +51,7 @@ ${this.getWhen()}
${this.getLocation()} ${this.getLocation()}
${this.getDescription()} ${this.getDescription()}
${this.getAdditionalNotes()} ${this.getAdditionalNotes()}
${this.getCustomInputs()}
${this.getRejectionReason()} ${this.getRejectionReason()}
`.replace(/(<([^>]+)>)/gi, ""); `.replace(/(<([^>]+)>)/gi, "");
} }
@ -102,6 +103,7 @@ ${this.getRejectionReason()}
${this.getLocation()} ${this.getLocation()}
${this.getDescription()} ${this.getDescription()}
${this.getAdditionalNotes()} ${this.getAdditionalNotes()}
${this.getCustomInputs()}
${this.getRejectionReason()} ${this.getRejectionReason()}
</div> </div>
</td> </td>

View File

@ -62,6 +62,7 @@ ${this.getWhen()}
${this.getLocation()} ${this.getLocation()}
${this.getDescription()} ${this.getDescription()}
${this.getAdditionalNotes()} ${this.getAdditionalNotes()}
${this.getCustomInputs()}
`.replace(/(<([^>]+)>)/gi, ""); `.replace(/(<([^>]+)>)/gi, "");
} }
@ -119,6 +120,7 @@ ${this.getAdditionalNotes()}
${this.getLocation()} ${this.getLocation()}
${this.getDescription()} ${this.getDescription()}
${this.getAdditionalNotes()} ${this.getAdditionalNotes()}
${this.getCustomInputs()}
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -105,6 +105,7 @@ ${this.calEvent.organizer.language.translate("request_reschedule_subtitle", {
${this.getWhat()} ${this.getWhat()}
${this.getWhen()} ${this.getWhen()}
${this.getAdditionalNotes()} ${this.getAdditionalNotes()}
${this.getCustomInputs()}
${this.calEvent.organizer.language.translate("need_to_reschedule_or_cancel")} ${this.calEvent.organizer.language.translate("need_to_reschedule_or_cancel")}
${getCancelLink(this.calEvent)} ${getCancelLink(this.calEvent)}
`.replace(/(<([^>]+)>)/gi, ""); `.replace(/(<([^>]+)>)/gi, "");
@ -154,6 +155,7 @@ ${getCancelLink(this.calEvent)}
${this.getWhen()} ${this.getWhen()}
${this.getWho()} ${this.getWho()}
${this.getAdditionalNotes()} ${this.getAdditionalNotes()}
${this.getCustomInputs()}
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -57,6 +57,7 @@ export default class AttendeeRescheduledEmail extends AttendeeScheduledEmail {
${this.getLocation()} ${this.getLocation()}
${this.getDescription()} ${this.getDescription()}
${this.getAdditionalNotes()} ${this.getAdditionalNotes()}
${this.getCustomInputs()}
${this.attendee.language.translate("need_to_reschedule_or_cancel")} ${this.attendee.language.translate("need_to_reschedule_or_cancel")}
${getCancelLink(this.calEvent)} ${getCancelLink(this.calEvent)}
`.replace(/(<([^>]+)>)/gi, ""); `.replace(/(<([^>]+)>)/gi, "");
@ -69,6 +70,7 @@ ${this.getWhat()}
${this.getWhen()} ${this.getWhen()}
${this.getLocation()} ${this.getLocation()}
${this.getAdditionalNotes()} ${this.getAdditionalNotes()}
${this.getCustomInputs()}
`.replace(/(<([^>]+)>)/gi, ""); `.replace(/(<([^>]+)>)/gi, "");
} }
@ -116,6 +118,7 @@ ${this.getAdditionalNotes()}
${this.getLocation()} ${this.getLocation()}
${this.getDescription()} ${this.getDescription()}
${this.getAdditionalNotes()} ${this.getAdditionalNotes()}
${this.getCustomInputs()}
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -169,6 +169,7 @@ ${getRichDescription(this.calEvent)}
${this.getLocation()} ${this.getLocation()}
${this.getDescription()} ${this.getDescription()}
${this.getAdditionalNotes()} ${this.getAdditionalNotes()}
${this.getCustomInputs()}
</div> </div>
</td> </td>
</tr> </tr>
@ -316,6 +317,28 @@ ${getRichDescription(this.calEvent)}
`; `;
} }
protected getCustomInputs(): string {
const { customInputs } = this.calEvent;
if (!customInputs) return "";
const customInputsString = Object.keys(customInputs)
.map((key) => {
if (customInputs[key] !== "") {
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${key}</p>
<p style="color: #494949; font-weight: 400;">
${customInputs[key]}
</p>
</div>
`;
}
})
.join("");
return customInputsString;
}
protected getRejectionReason(): string { protected getRejectionReason(): string {
if (!this.calEvent.rejectionReason) return ""; if (!this.calEvent.rejectionReason) return "";
return ` return `

View File

@ -58,6 +58,7 @@ ${this.getWhen()}
${this.getLocation()} ${this.getLocation()}
${this.getDescription()} ${this.getDescription()}
${this.getAdditionalNotes()} ${this.getAdditionalNotes()}
${this.getCustomInputs()}
${this.calEvent.cancellationReason && this.getCancellationReason()} ${this.calEvent.cancellationReason && this.getCancellationReason()}
`.replace(/(<([^>]+)>)/gi, ""); `.replace(/(<([^>]+)>)/gi, "");
} }
@ -106,6 +107,7 @@ ${this.calEvent.cancellationReason && this.getCancellationReason()}
${this.getLocation()} ${this.getLocation()}
${this.getDescription()} ${this.getDescription()}
${this.getAdditionalNotes()} ${this.getAdditionalNotes()}
${this.getCustomInputs()}
${this.calEvent.cancellationReason && this.getCancellationReason()} ${this.calEvent.cancellationReason && this.getCancellationReason()}
</div> </div>
</td> </td>

View File

@ -60,6 +60,7 @@ ${this.getWhen()}
${this.getLocation()} ${this.getLocation()}
${this.getDescription()} ${this.getDescription()}
${this.getAdditionalNotes()} ${this.getAdditionalNotes()}
${this.getCustomInputs()}
`.replace(/(<([^>]+)>)/gi, ""); `.replace(/(<([^>]+)>)/gi, "");
} }
@ -139,6 +140,7 @@ ${this.getAdditionalNotes()}
${this.getLocation()} ${this.getLocation()}
${this.getDescription()} ${this.getDescription()}
${this.getAdditionalNotes()} ${this.getAdditionalNotes()}
${this.getCustomInputs()}
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -59,6 +59,7 @@ ${this.getWhen()}
${this.getLocation()} ${this.getLocation()}
${this.getDescription()} ${this.getDescription()}
${this.getAdditionalNotes()} ${this.getAdditionalNotes()}
${this.getCustomInputs()}
${this.calEvent.organizer.language.translate("confirm_or_reject_request")} ${this.calEvent.organizer.language.translate("confirm_or_reject_request")}
${process.env.NEXT_PUBLIC_WEBAPP_URL} + "/bookings/upcoming" ${process.env.NEXT_PUBLIC_WEBAPP_URL} + "/bookings/upcoming"
`.replace(/(<([^>]+)>)/gi, ""); `.replace(/(<([^>]+)>)/gi, "");
@ -110,6 +111,7 @@ ${process.env.NEXT_PUBLIC_WEBAPP_URL} + "/bookings/upcoming"
${this.getWho()} ${this.getWho()}
${this.getLocation()} ${this.getLocation()}
${this.getAdditionalNotes()} ${this.getAdditionalNotes()}
${this.getCustomInputs()}
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -59,6 +59,7 @@ ${this.getWhen()}
${this.getLocation()} ${this.getLocation()}
${this.getDescription()} ${this.getDescription()}
${this.getAdditionalNotes()} ${this.getAdditionalNotes()}
${this.getCustomInputs()}
${this.calEvent.organizer.language.translate("confirm_or_reject_request")} ${this.calEvent.organizer.language.translate("confirm_or_reject_request")}
${process.env.NEXT_PUBLIC_WEBAPP_URL} + "/bookings/upcoming" ${process.env.NEXT_PUBLIC_WEBAPP_URL} + "/bookings/upcoming"
`.replace(/(<([^>]+)>)/gi, ""); `.replace(/(<([^>]+)>)/gi, "");
@ -108,6 +109,7 @@ ${process.env.NEXT_PUBLIC_WEBAPP_URL} + "/bookings/upcoming"
${this.getLocation()} ${this.getLocation()}
${this.getDescription()} ${this.getDescription()}
${this.getAdditionalNotes()} ${this.getAdditionalNotes()}
${this.getCustomInputs()}
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -115,6 +115,7 @@ ${this.getWhat()}
${this.getWhen()} ${this.getWhen()}
${this.getLocation()} ${this.getLocation()}
${this.getAdditionalNotes()} ${this.getAdditionalNotes()}
${this.getCustomInputs()}
${this.calEvent.organizer.language.translate("need_to_reschedule_or_cancel")} ${this.calEvent.organizer.language.translate("need_to_reschedule_or_cancel")}
${getCancelLink(this.calEvent)} ${getCancelLink(this.calEvent)}
`.replace(/(<([^>]+)>)/gi, ""); `.replace(/(<([^>]+)>)/gi, "");
@ -166,6 +167,7 @@ ${getCancelLink(this.calEvent)}
${this.getWhen()} ${this.getWhen()}
${this.getWho()} ${this.getWho()}
${this.getAdditionalNotes()} ${this.getAdditionalNotes()}
${this.getCustomInputs()}
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -64,6 +64,7 @@ ${this.getWhen()}
${this.getLocation()} ${this.getLocation()}
${this.getDescription()} ${this.getDescription()}
${this.getAdditionalNotes()} ${this.getAdditionalNotes()}
${this.getCustomInputs()}
${this.calEvent.organizer.language.translate("need_to_reschedule_or_cancel")} ${this.calEvent.organizer.language.translate("need_to_reschedule_or_cancel")}
${getCancelLink(this.calEvent)} ${getCancelLink(this.calEvent)}
`.replace(/(<([^>]+)>)/gi, ""); `.replace(/(<([^>]+)>)/gi, "");
@ -113,6 +114,7 @@ ${getCancelLink(this.calEvent)}
${this.getLocation()} ${this.getLocation()}
${this.getDescription()} ${this.getDescription()}
${this.getAdditionalNotes()} ${this.getAdditionalNotes()}
${this.getCustomInputs()}
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -162,6 +162,7 @@ ${getRichDescription(this.calEvent)}
${this.getLocation()} ${this.getLocation()}
${this.getDescription()} ${this.getDescription()}
${this.getAdditionalNotes()} ${this.getAdditionalNotes()}
${this.getCustomInputs()}
</div> </div>
</td> </td>
</tr> </tr>
@ -303,6 +304,28 @@ ${getRichDescription(this.calEvent)}
`; `;
} }
protected getCustomInputs(): string {
const { customInputs } = this.calEvent;
if (!customInputs) return "";
const customInputsString = Object.keys(customInputs)
.map((key) => {
if (customInputs[key] !== "") {
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${key}</p>
<p style="color: #494949; font-weight: 400;">
${customInputs[key]}
</p>
</div>
`;
}
})
.join("");
return customInputsString;
}
protected getDescription(): string { protected getDescription(): string {
if (!this.calEvent.description) return ""; if (!this.calEvent.description) return "";
return ` return `

View File

@ -8,6 +8,7 @@ async function getBooking(prisma: PrismaClient, uid: string) {
select: { select: {
startTime: true, startTime: true,
description: true, description: true,
customInputs: true,
attendees: { attendees: {
select: { select: {
email: true, email: true,

View File

@ -0,0 +1,40 @@
import { EventType, PeriodType } from "@prisma/client";
import dayjs from "dayjs";
function isOutOfBounds(
time: dayjs.ConfigType,
{
periodType,
periodDays,
periodCountCalendarDays,
periodStartDate,
periodEndDate,
}: Pick<
EventType,
"periodType" | "periodDays" | "periodCountCalendarDays" | "periodStartDate" | "periodEndDate"
>
) {
const date = dayjs(time);
periodDays = periodDays || 0;
switch (periodType) {
case PeriodType.ROLLING: {
const periodRollingEndDay = periodCountCalendarDays
? dayjs().utcOffset(date.utcOffset()).add(periodDays, "days").endOf("day")
: dayjs().utcOffset(date.utcOffset()).businessDaysAdd(periodDays).endOf("day");
return date.endOf("day").isAfter(periodRollingEndDay);
}
case PeriodType.RANGE: {
const periodRangeStartDay = dayjs(periodStartDate).utcOffset(date.utcOffset()).endOf("day");
const periodRangeEndDay = dayjs(periodEndDate).utcOffset(date.utcOffset()).endOf("day");
return date.endOf("day").isBefore(periodRangeStartDay) || date.endOf("day").isAfter(periodRangeEndDay);
}
case PeriodType.UNLIMITED:
default:
return false;
}
}
export default isOutOfBounds;

View File

@ -24,7 +24,7 @@ export type BookingCreateBody = {
timeZone: string; timeZone: string;
user?: string | string[]; user?: string | string[];
language: string; language: string;
customInputs: { label: string; value: string }[]; customInputs: { label: string; value: string | boolean }[];
metadata: { metadata: {
[key: string]: string; [key: string]: string;
}; };

View File

@ -1,16 +1,15 @@
import { Prisma, User, Booking, SchedulingType, BookingStatus } from "@prisma/client"; import { Booking, BookingStatus, Prisma, SchedulingType, User } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import EventManager from "@calcom/core/EventManager"; import EventManager from "@calcom/core/EventManager";
import { isPrismaObjOrUndefined } from "@calcom/lib";
import logger from "@calcom/lib/logger"; import logger from "@calcom/lib/logger";
import type { AdditionInformation, RecurringEvent } from "@calcom/types/Calendar"; import type { AdditionInformation, CalendarEvent, RecurringEvent } from "@calcom/types/Calendar";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { refund } from "@ee/lib/stripe/server"; import { refund } from "@ee/lib/stripe/server";
import { asStringOrNull } from "@lib/asStringOrNull"; import { asStringOrNull } from "@lib/asStringOrNull";
import { getSession } from "@lib/auth"; import { getSession } from "@lib/auth";
import { sendDeclinedEmails } from "@lib/emails/email-manager"; import { sendDeclinedEmails, sendScheduledEmails } from "@lib/emails/email-manager";
import { sendScheduledEmails } from "@lib/emails/email-manager";
import prisma from "@lib/prisma"; import prisma from "@lib/prisma";
import { BookingConfirmBody } from "@lib/types/booking"; import { BookingConfirmBody } from "@lib/types/booking";
@ -89,6 +88,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
select: { select: {
title: true, title: true,
description: true, description: true,
customInputs: true,
startTime: true, startTime: true,
endTime: true, endTime: true,
confirmed: true, confirmed: true,
@ -156,6 +156,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
type: booking.title, type: booking.title,
title: booking.title, title: booking.title,
description: booking.description, description: booking.description,
customInputs: isPrismaObjOrUndefined(booking.customInputs),
startTime: booking.startTime.toISOString(), startTime: booking.startTime.toISOString(),
endTime: booking.endTime.toISOString(), endTime: booking.endTime.toISOString(),
organizer: { organizer: {

View File

@ -11,6 +11,7 @@ import short from "short-uuid";
import { v5 as uuidv5 } from "uuid"; import { v5 as uuidv5 } from "uuid";
import EventManager from "@calcom/core/EventManager"; import EventManager from "@calcom/core/EventManager";
import { isPrismaObjOrUndefined } from "@calcom/lib";
import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defaultEvents"; import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defaultEvents";
import { getErrorFromUnknown } from "@calcom/lib/errors"; import { getErrorFromUnknown } from "@calcom/lib/errors";
import logger from "@calcom/lib/logger"; import logger from "@calcom/lib/logger";
@ -28,6 +29,7 @@ import {
import { ensureArray } from "@lib/ensureArray"; import { ensureArray } from "@lib/ensureArray";
import { getEventName } from "@lib/event"; import { getEventName } from "@lib/event";
import getBusyTimes from "@lib/getBusyTimes"; import getBusyTimes from "@lib/getBusyTimes";
import isOutOfBounds from "@lib/isOutOfBounds";
import prisma from "@lib/prisma"; import prisma from "@lib/prisma";
import { BookingCreateBody } from "@lib/types/booking"; import { BookingCreateBody } from "@lib/types/booking";
import sendPayload from "@lib/webhooks/sendPayload"; import sendPayload from "@lib/webhooks/sendPayload";
@ -104,32 +106,6 @@ function isAvailable(busyTimes: BufferedBusyTimes, time: dayjs.ConfigType, lengt
return t; return t;
} }
function isOutOfBounds(
time: dayjs.ConfigType,
{ periodType, periodDays, periodCountCalendarDays, periodStartDate, periodEndDate, timeZone }: any // FIXME types
): boolean {
const date = dayjs(time);
switch (periodType) {
case "rolling": {
const periodRollingEndDay = periodCountCalendarDays
? dayjs().tz(timeZone).add(periodDays, "days").endOf("day")
: dayjs().tz(timeZone).businessDaysAdd(periodDays).endOf("day");
return date.endOf("day").isAfter(periodRollingEndDay);
}
case "range": {
const periodRangeStartDay = dayjs(periodStartDate).tz(timeZone).endOf("day");
const periodRangeEndDay = dayjs(periodEndDate).tz(timeZone).endOf("day");
return date.endOf("day").isBefore(periodRangeStartDay) || date.endOf("day").isAfter(periodRangeEndDay);
}
case "unlimited":
default:
return false;
}
}
const userSelect = Prisma.validator<Prisma.UserArgs>()({ const userSelect = Prisma.validator<Prisma.UserArgs>()({
select: { select: {
id: true, id: true,
@ -348,18 +324,22 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
t: tOrganizer, t: tOrganizer,
}; };
const additionalNotes = const additionalNotes = reqBody.notes;
reqBody.notes +
reqBody.customInputs.reduce( const customInputs = {} as NonNullable<CalendarEvent["customInputs"]>;
(str, input) => str + "<br /><br />" + input.label + ":<br />" + input.value,
"" if (reqBody.customInputs.length > 0) {
); reqBody.customInputs.forEach(({ label, value }) => {
customInputs[label] = value;
});
}
const evt: CalendarEvent = { const evt: CalendarEvent = {
type: eventType.title, type: eventType.title,
title: getEventName(eventNameObject), //this needs to be either forced in english, or fetched for each attendee and organizer separately title: getEventName(eventNameObject), //this needs to be either forced in english, or fetched for each attendee and organizer separately
description: eventType.description, description: eventType.description,
additionalNotes, additionalNotes,
customInputs,
startTime: reqBody.start, startTime: reqBody.start,
endTime: reqBody.end, endTime: reqBody.end,
organizer: { organizer: {
@ -456,6 +436,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
startTime: dayjs(evt.startTime).toDate(), startTime: dayjs(evt.startTime).toDate(),
endTime: dayjs(evt.endTime).toDate(), endTime: dayjs(evt.endTime).toDate(),
description: evt.additionalNotes, description: evt.additionalNotes,
customInputs: isPrismaObjOrUndefined(evt.customInputs),
confirmed: (!eventType.requiresConfirmation && !eventType.price) || !!rescheduleUid, confirmed: (!eventType.requiresConfirmation && !eventType.price) || !!rescheduleUid,
location: evt.location, location: evt.location,
eventType: eventTypeRel, eventType: eventTypeRel,
@ -613,7 +594,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
periodEndDate: eventType.periodEndDate, periodEndDate: eventType.periodEndDate,
periodStartDate: eventType.periodStartDate, periodStartDate: eventType.periodStartDate,
periodCountCalendarDays: eventType.periodCountCalendarDays, periodCountCalendarDays: eventType.periodCountCalendarDays,
timeZone: currentUser.timeZone,
}); });
} catch { } catch {
log.debug({ log.debug({
@ -731,6 +711,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
...evt, ...evt,
additionInformation: metadata, additionInformation: metadata,
additionalNotes, additionalNotes,
customInputs,
}, },
reqBody.recurringEventId ? (eventType.recurringEvent as RecurringEvent) : {} reqBody.recurringEventId ? (eventType.recurringEvent as RecurringEvent) : {}
); );

View File

@ -6,13 +6,14 @@ import { NextApiRequest, NextApiResponse } from "next";
import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter"; import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
import { getCalendar } from "@calcom/core/CalendarManager"; import { getCalendar } from "@calcom/core/CalendarManager";
import { deleteMeeting } from "@calcom/core/videoClient"; import { deleteMeeting } from "@calcom/core/videoClient";
import { isPrismaObjOrUndefined } from "@calcom/lib";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import type { CalendarEvent } from "@calcom/types/Calendar"; import type { CalendarEvent } from "@calcom/types/Calendar";
import { refund } from "@ee/lib/stripe/server"; import { refund } from "@ee/lib/stripe/server";
import { asStringOrNull } from "@lib/asStringOrNull"; import { asStringOrNull } from "@lib/asStringOrNull";
import { getSession } from "@lib/auth"; import { getSession } from "@lib/auth";
import { sendCancelledEmails } from "@lib/emails/email-manager"; import { sendCancelledEmails } from "@lib/emails/email-manager";
import prisma from "@lib/prisma";
import sendPayload from "@lib/webhooks/sendPayload"; import sendPayload from "@lib/webhooks/sendPayload";
import getWebhooks from "@lib/webhooks/subscriptions"; import getWebhooks from "@lib/webhooks/subscriptions";
@ -33,7 +34,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
uid, uid,
}, },
select: { select: {
id: true, ...bookingMinimalSelect,
userId: true, userId: true,
user: { user: {
select: { select: {
@ -45,7 +46,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
destinationCalendar: true, destinationCalendar: true,
}, },
}, },
attendees: true,
location: true, location: true,
references: { references: {
select: { select: {
@ -56,15 +56,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}, },
payment: true, payment: true,
paid: true, paid: true,
title: true,
eventType: { eventType: {
select: { select: {
title: true, title: true,
}, },
}, },
description: true,
startTime: true,
endTime: true,
uid: true, uid: true,
eventTypeId: true, eventTypeId: true,
destinationCalendar: true, destinationCalendar: true,
@ -115,6 +111,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
title: bookingToDelete?.title, title: bookingToDelete?.title,
type: (bookingToDelete?.eventType?.title as string) || bookingToDelete?.title, type: (bookingToDelete?.eventType?.title as string) || bookingToDelete?.title,
description: bookingToDelete?.description || "", description: bookingToDelete?.description || "",
customInputs: isPrismaObjOrUndefined(bookingToDelete.customInputs),
startTime: bookingToDelete?.startTime ? dayjs(bookingToDelete.startTime).format() : "", startTime: bookingToDelete?.startTime ? dayjs(bookingToDelete.startTime).format() : "",
endTime: bookingToDelete?.endTime ? dayjs(bookingToDelete.endTime).format() : "", endTime: bookingToDelete?.endTime ? dayjs(bookingToDelete.endTime).format() : "",
organizer: { organizer: {
@ -183,6 +180,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
type: bookingToDelete?.eventType?.title as string, type: bookingToDelete?.eventType?.title as string,
title: bookingToDelete.title, title: bookingToDelete.title,
description: bookingToDelete.description ?? "", description: bookingToDelete.description ?? "",
customInputs: isPrismaObjOrUndefined(bookingToDelete.customInputs),
startTime: bookingToDelete.startTime.toISOString(), startTime: bookingToDelete.startTime.toISOString(),
endTime: bookingToDelete.endTime.toISOString(), endTime: bookingToDelete.endTime.toISOString(),
organizer: { organizer: {

View File

@ -2,10 +2,11 @@ import { ReminderType } from "@prisma/client";
import dayjs from "dayjs"; import dayjs from "dayjs";
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import { isPrismaObjOrUndefined } from "@calcom/lib";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import type { CalendarEvent } from "@calcom/types/Calendar"; import type { CalendarEvent } from "@calcom/types/Calendar";
import { sendOrganizerRequestReminderEmail } from "@lib/emails/email-manager"; import { sendOrganizerRequestReminderEmail } from "@lib/emails/email-manager";
import prisma from "@lib/prisma";
import { getTranslation } from "@server/lib/i18n"; import { getTranslation } from "@server/lib/i18n";
@ -32,12 +33,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}, },
}, },
select: { select: {
title: true, ...bookingMinimalSelect,
description: true,
location: true, location: true,
startTime: true,
endTime: true,
attendees: true,
user: { user: {
select: { select: {
email: true, email: true,
@ -48,7 +45,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
destinationCalendar: true, destinationCalendar: true,
}, },
}, },
id: true,
uid: true, uid: true,
destinationCalendar: true, destinationCalendar: true,
}, },
@ -94,6 +90,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
type: booking.title, type: booking.title,
title: booking.title, title: booking.title,
description: booking.description || undefined, description: booking.description || undefined,
customInputs: isPrismaObjOrUndefined(booking.customInputs),
location: booking.location ?? "", location: booking.location ?? "",
startTime: booking.startTime.toISOString(), startTime: booking.startTime.toISOString(),
endTime: booking.endTime.toISOString(), endTime: booking.endTime.toISOString(),

View File

@ -4,13 +4,13 @@ import { GetServerSidePropsContext } from "next";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useState } from "react"; import { useState } from "react";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import { Button } from "@calcom/ui/Button"; import { Button } from "@calcom/ui/Button";
import { TextField } from "@calcom/ui/form/fields"; import { TextField } from "@calcom/ui/form/fields";
import { asStringOrUndefined } from "@lib/asStringOrNull"; import { asStringOrUndefined } from "@lib/asStringOrNull";
import { getSession } from "@lib/auth"; import { getSession } from "@lib/auth";
import { useLocale } from "@lib/hooks/useLocale"; import { useLocale } from "@lib/hooks/useLocale";
import prisma from "@lib/prisma";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import { detectBrowserTimeFormat } from "@lib/timeFormat"; import { detectBrowserTimeFormat } from "@lib/timeFormat";
import { inferSSRProps } from "@lib/types/inferSSRProps"; import { inferSSRProps } from "@lib/types/inferSSRProps";
@ -168,12 +168,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
uid: asStringOrUndefined(context.query.uid), uid: asStringOrUndefined(context.query.uid),
}, },
select: { select: {
id: true, ...bookingMinimalSelect,
title: true,
description: true,
startTime: true,
endTime: true,
attendees: true,
user: { user: {
select: { select: {
id: true, id: true,

View File

@ -1,9 +1,9 @@
import { GetServerSidePropsContext } from "next"; import { GetServerSidePropsContext } from "next";
import { getDefaultEvent } from "@calcom/lib/defaultEvents"; import { getDefaultEvent } from "@calcom/lib/defaultEvents";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import { asStringOrUndefined } from "@lib/asStringOrNull"; import { asStringOrUndefined } from "@lib/asStringOrNull";
import prisma from "@lib/prisma";
export default function Type() { export default function Type() {
// Just redirect to the schedule page to reschedule it. // Just redirect to the schedule page to reschedule it.
@ -16,7 +16,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
uid: asStringOrUndefined(context.query.uid), uid: asStringOrUndefined(context.query.uid),
}, },
select: { select: {
id: true, ...bookingMinimalSelect,
eventType: { eventType: {
select: { select: {
users: { users: {
@ -35,11 +35,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
dynamicEventSlugRef: true, dynamicEventSlugRef: true,
dynamicGroupSlugRef: true, dynamicGroupSlugRef: true,
user: true, user: true,
title: true,
description: true,
startTime: true,
endTime: true,
attendees: true,
}, },
}); });
const dynamicEventSlugRef = booking?.dynamicEventSlugRef || ""; const dynamicEventSlugRef = booking?.dynamicEventSlugRef || "";

View File

@ -40,6 +40,7 @@ import { isBrowserLocale24h } from "@lib/timeFormat";
import { inferSSRProps } from "@lib/types/inferSSRProps"; import { inferSSRProps } from "@lib/types/inferSSRProps";
import CustomBranding from "@components/CustomBranding"; import CustomBranding from "@components/CustomBranding";
import CancelBooking from "@components/booking/CancelBooking";
import { HeadSeo } from "@components/seo/head-seo"; import { HeadSeo } from "@components/seo/head-seo";
import { ssrInit } from "@server/lib/ssr"; import { ssrInit } from "@server/lib/ssr";
@ -152,13 +153,13 @@ export default function Success(props: SuccessProps) {
const { data: session } = useSession(); const { data: session } = useSession();
const [date, setDate] = useState(dayjs.utc(asStringOrThrow(router.query.date))); const [date, setDate] = useState(dayjs.utc(asStringOrThrow(router.query.date)));
const { isReady, Theme } = useTheme(props.profile.theme);
const { eventType, bookingInfo } = props; const { eventType, bookingInfo } = props;
const isBackgroundTransparent = useIsBackgroundTransparent(); const isBackgroundTransparent = useIsBackgroundTransparent();
const isEmbed = useIsEmbed(); const isEmbed = useIsEmbed();
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left"; const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed; const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
const [isCancellationMode, setIsCancellationMode] = useState(false);
const attendeeName = typeof name === "string" ? name : "Nameless"; const attendeeName = typeof name === "string" ? name : "Nameless";
@ -247,15 +248,26 @@ export default function Success(props: SuccessProps) {
return t("emailed_you_and_attendees" + titleSuffix); return t("emailed_you_and_attendees" + titleSuffix);
} }
const userIsOwner = !!(session?.user?.id && eventType.users.find((user) => (user.id = session.user.id))); const userIsOwner = !!(session?.user?.id && eventType.users.find((user) => (user.id = session.user.id)));
const { isReady, Theme } = useTheme(userIsOwner ? "light" : props.profile.theme);
const title = t( const title = t(
`booking_${needsConfirmation ? "submitted" : "confirmed"}${props.recurringBookings ? "_recurring" : ""}` `booking_${needsConfirmation ? "submitted" : "confirmed"}${props.recurringBookings ? "_recurring" : ""}`
); );
const customInputs = bookingInfo?.customInputs;
return ( return (
(isReady && ( (isReady && (
<> <>
<div <div
className={isEmbed ? "" : "h-screen bg-neutral-100 dark:bg-neutral-900"} className={isEmbed ? "" : "h-screen bg-neutral-100 dark:bg-neutral-900"}
data-testid="success-page"> data-testid="success-page">
{userIsOwner && !isEmbed && (
<div className="-mb-7 ml-9 mt-7">
<Link href="/bookings">
<a className="flex items-center text-black dark:text-white">
<ArrowLeftIcon className="mr-1 h-4 w-4" /> {t("back_to_bookings")}
</a>
</Link>
</div>
)}
<Theme /> <Theme />
<HeadSeo title={title} description={title} /> <HeadSeo title={title} description={title} />
<CustomBranding lightVal={props.profile.brandColor} darkVal={props.profile.darkBrandColor} /> <CustomBranding lightVal={props.profile.brandColor} darkVal={props.profile.darkBrandColor} />
@ -357,21 +369,66 @@ export default function Success(props: SuccessProps) {
)} )}
{bookingInfo?.description && ( {bookingInfo?.description && (
<> <>
<div className="mt-6 font-medium">{t("additional_notes")}</div> <div className="mt-9 font-medium">{t("additional_notes")}</div>
<div className="col-span-2 mt-6 mb-6"> <div className="col-span-2 mb-2 mt-9">
<p>{bookingInfo.description}</p> <p>{bookingInfo.description}</p>
</div> </div>
</> </>
)} )}
{customInputs &&
Object.keys(customInputs).map((key) => {
const customInput = customInputs[key as keyof typeof customInputs];
return (
<>
{customInput !== "" && (
<>
<div className="mt-2 pr-3 font-medium">{key}</div>
<div className="col-span-2 mt-2 mb-2">
{typeof customInput === "boolean" ? (
<p>{customInput ? "true" : "false"}</p>
) : (
<p>{customInput}</p>
)}
</div>
</>
)}
</>
);
})}
</div> </div>
</div> </div>
</div> </div>
{!needsConfirmation && ( {!needsConfirmation &&
<div className="border-bookinglightest mt-5 flex border-b pt-2 pb-4 text-center dark:border-gray-900 sm:mt-0 sm:pt-4"> (!isCancellationMode ? (
<div className="border-bookinglightest text-bookingdark mt-2 grid grid-cols-3 border-b py-4 text-left dark:border-gray-900">
<span className="flex self-center font-medium text-gray-700 ltr:mr-2 rtl:ml-2 dark:text-gray-50">
{t("need_to_make_a_change")}
</span>
<div className="ml-7 flex items-center justify-center self-center ltr:mr-2 rtl:ml-2 dark:text-gray-50">
<button className="underline" onClick={() => setIsCancellationMode(true)}>
{t("cancel")}
</button>
<div className="mx-2">{t("or_lowercase")}</div>
<div className="underline">
<Link href={"/reschedule/" + bookingInfo?.uid}>{t("Reschedule")}</Link>
</div>
</div>
</div>
) : (
<CancelBooking
booking={{ uid: bookingInfo?.uid, title: bookingInfo?.title }}
profile={{ name: props.profile.name, slug: props.profile.slug }}
team={eventType?.team?.name}
setIsCancellationMode={setIsCancellationMode}
theme={userIsOwner ? "light" : props.profile.theme}
/>
))}
{userIsOwner && !needsConfirmation && !isCancellationMode && (
<div className="border-bookinglightest mt-9 flex border-b pt-2 pb-4 text-center dark:border-gray-900 sm:mt-0 sm:pt-4">
<span className="flex self-center font-medium text-gray-700 ltr:mr-2 rtl:ml-2 dark:text-gray-50"> <span className="flex self-center font-medium text-gray-700 ltr:mr-2 rtl:ml-2 dark:text-gray-50">
{t("add_to_calendar")} {t("add_to_calendar")}
</span> </span>
<div className="flex flex-grow justify-center text-center"> <div className="-ml-16 flex flex-grow justify-center text-center">
<Link <Link
href={ href={
`https://calendar.google.com/calendar/r/eventedit?dates=${date `https://calendar.google.com/calendar/r/eventedit?dates=${date
@ -497,15 +554,6 @@ export default function Success(props: SuccessProps) {
</form> </form>
</div> </div>
)} )}
{userIsOwner && !isEmbed && (
<div className="mt-4">
<Link href="/bookings">
<a className="flex items-center text-black dark:text-white">
<ArrowLeftIcon className="mr-1 h-4 w-4" /> {t("back_to_bookings")}
</a>
</Link>
</div>
)}
</div> </div>
</div> </div>
</div> </div>
@ -618,6 +666,7 @@ const getEventTypesFromDB = async (typeId: number) => {
select: { select: {
id: true, id: true,
name: true, name: true,
username: true,
hideBranding: true, hideBranding: true,
plan: true, plan: true,
theme: true, theme: true,
@ -629,6 +678,7 @@ const getEventTypesFromDB = async (typeId: number) => {
}, },
team: { team: {
select: { select: {
slug: true,
name: true, name: true,
hideBranding: true, hideBranding: true,
}, },
@ -683,6 +733,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
select: { select: {
id: true, id: true,
name: true, name: true,
username: true,
hideBranding: true, hideBranding: true,
plan: true, plan: true,
theme: true, theme: true,
@ -714,6 +765,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
theme: (!eventType.team?.name && eventType.users[0]?.theme) || null, theme: (!eventType.team?.name && eventType.users[0]?.theme) || null,
brandColor: eventType.team ? null : eventType.users[0].brandColor || null, brandColor: eventType.team ? null : eventType.users[0].brandColor || null,
darkBrandColor: eventType.team ? null : eventType.users[0].darkBrandColor || null, darkBrandColor: eventType.team ? null : eventType.users[0].darkBrandColor || null,
slug: eventType.team?.slug || eventType.users[0]?.username || null,
}; };
const bookingInfo = await prisma.booking.findUnique({ const bookingInfo = await prisma.booking.findUnique({
@ -721,7 +773,10 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
id: bookingId, id: bookingId,
}, },
select: { select: {
title: true,
uid: true,
description: true, description: true,
customInputs: true,
user: { user: {
select: { select: {
name: true, name: true,

View File

@ -6,9 +6,9 @@ import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect } from "react"; import { useEffect } from "react";
import { useLocale } from "@lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma from "@lib/prisma"; import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import { inferSSRProps } from "@lib/types/inferSSRProps"; import { inferSSRProps } from "@calcom/types/inferSSRProps";
export type JoinCallPageProps = inferSSRProps<typeof getServerSideProps>; export type JoinCallPageProps = inferSSRProps<typeof getServerSideProps>;
@ -150,19 +150,14 @@ export async function getServerSideProps(context: NextPageContext) {
uid: context.query.uid as string, uid: context.query.uid as string,
}, },
select: { select: {
...bookingMinimalSelect,
uid: true, uid: true,
id: true,
title: true,
description: true,
startTime: true,
endTime: true,
user: { user: {
select: { select: {
id: true, id: true,
credentials: true, credentials: true,
}, },
}, },
attendees: true,
dailyRef: { dailyRef: {
select: { select: {
dailyurl: true, dailyurl: true,

View File

@ -6,9 +6,9 @@ import { getSession } from "next-auth/react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect } from "react"; import { useEffect } from "react";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import Button from "@calcom/ui/Button"; import Button from "@calcom/ui/Button";
import prisma from "@lib/prisma";
import { detectBrowserTimeFormat } from "@lib/timeFormat"; import { detectBrowserTimeFormat } from "@lib/timeFormat";
import { inferSSRProps } from "@lib/types/inferSSRProps"; import { inferSSRProps } from "@lib/types/inferSSRProps";
@ -84,18 +84,13 @@ export async function getServerSideProps(context: NextPageContext) {
uid: context.query.uid as string, uid: context.query.uid as string,
}, },
select: { select: {
...bookingMinimalSelect,
uid: true, uid: true,
id: true,
title: true,
description: true,
startTime: true,
endTime: true,
user: { user: {
select: { select: {
credentials: true, credentials: true,
}, },
}, },
attendees: true,
dailyRef: { dailyRef: {
select: { select: {
dailyurl: true, dailyurl: true,

View File

@ -6,11 +6,11 @@ import { getSession } from "next-auth/react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect } from "react"; import { useEffect } from "react";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import Button from "@calcom/ui/Button"; import Button from "@calcom/ui/Button";
import prisma from "@lib/prisma";
import { detectBrowserTimeFormat } from "@lib/timeFormat"; import { detectBrowserTimeFormat } from "@lib/timeFormat";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import { HeadSeo } from "@components/seo/head-seo"; import { HeadSeo } from "@components/seo/head-seo";
@ -90,18 +90,13 @@ export async function getServerSideProps(context: NextPageContext) {
uid: context.query.uid as string, uid: context.query.uid as string,
}, },
select: { select: {
...bookingMinimalSelect,
uid: true, uid: true,
id: true,
title: true,
description: true,
startTime: true,
endTime: true,
user: { user: {
select: { select: {
credentials: true, credentials: true,
}, },
}, },
attendees: true,
dailyRef: { dailyRef: {
select: { select: {
dailyurl: true, dailyurl: true,

View File

@ -1 +1 @@
{"triggerEvent":"BOOKING_CREATED","createdAt":"[redacted/dynamic]","payload":{"type":"30 min","title":"30 min between PRO and Test Testson","description":"","additionalNotes":"","startTime":"[redacted/dynamic]","endTime":"[redacted/dynamic]","organizer":{"name":"PRO","email":"[redacted/dynamic]","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"},"attendees":[{"email":"test@example.com","name":"Test Testson","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"}],"location":"[redacted/dynamic]","destinationCalendar":null,"hideCalendarNotes":false,"uid":"[redacted/dynamic]","metadata":{},"additionInformation":"[redacted/dynamic]"}} {"triggerEvent":"BOOKING_CREATED","createdAt":"[redacted/dynamic]","payload":{"type":"30 min","title":"30 min between PRO and Test Testson","description":"","additionalNotes":"","customInputs":{},"startTime":"[redacted/dynamic]","endTime":"[redacted/dynamic]","organizer":{"name":"PRO","email":"[redacted/dynamic]","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"},"attendees":[{"email":"test@example.com","name":"Test Testson","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"}],"location":"[redacted/dynamic]","destinationCalendar":null,"hideCalendarNotes":false,"uid":"[redacted/dynamic]","metadata":{},"additionInformation":"[redacted/dynamic]"}}

View File

@ -476,7 +476,7 @@
"back": "Back", "back": "Back",
"cancel": "Cancel", "cancel": "Cancel",
"apply": "Apply", "apply": "Apply",
"cancel_event": "Cancel this event", "cancel_event": "Cancel event",
"continue": "Continue", "continue": "Continue",
"confirm": "Confirm", "confirm": "Confirm",
"confirm_all": "Confirm all", "confirm_all": "Confirm all",
@ -823,11 +823,14 @@
"generate_api_key": "Generate Api Key", "generate_api_key": "Generate Api Key",
"your_unique_api_key": "Your unique API key", "your_unique_api_key": "Your unique API key",
"copy_safe_api_key": "Copy this API key and save it somewhere safe. If you lose this key you have to generate a new one.", "copy_safe_api_key": "Copy this API key and save it somewhere safe. If you lose this key you have to generate a new one.",
"zapier_setup_instructions": "<0>Log into your Zapier account and create a new Zap.</0><1>Select Cal.com as your Trigger app. Also choose a Trigger event.</1><2>Choose your account and then enter your Unique API Key.</2><3>Test your Trigger.</3><4>You're set!</4>",
"install_zapier_app": "Please first install the Zapier App in the app store.", "install_zapier_app": "Please first install the Zapier App in the app store.",
"go_to_app_store": "Go to App Store", "go_to_app_store": "Go to App Store",
"calendar_error": "Something went wrong, try reconnecting your calendar with all necessary permissions", "calendar_error": "Something went wrong, try reconnecting your calendar with all necessary permissions",
"calendar_no_busy_slots": "There are no busy slots", "calendar_no_busy_slots": "There are no busy slots",
"zapier_setup_instructions": "<0>Log into your Zapier account and create a new Zap.</0><1>Select Cal.com as your Trigger app. Also choose a Trigger event.</1><2>Choose your account and then enter your Unique API Key.</2><3>Test your Trigger.</3><4>You're set!</4>", "booking_details": "Booking details",
"or_lowercase": "or",
"nevermind": "Nevermind",
"go_to": "Go to: ", "go_to": "Go to: ",
"zapier_invite_link": "Zapier Invite Link" "zapier_invite_link": "Zapier Invite Link"
} }

View File

@ -6,6 +6,7 @@ import { z } from "zod";
import getApps from "@calcom/app-store/utils"; import getApps from "@calcom/app-store/utils";
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager"; import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
import { checkPremiumUsername } from "@calcom/ee/lib/core/checkPremiumUsername"; import { checkPremiumUsername } from "@calcom/ee/lib/core/checkPremiumUsername";
import { bookingMinimalSelect } from "@calcom/prisma";
import { RecurringEvent } from "@calcom/types/Calendar"; import { RecurringEvent } from "@calcom/types/Calendar";
import { checkRegularUsername } from "@lib/core/checkRegularUsername"; import { checkRegularUsername } from "@lib/core/checkRegularUsername";
@ -388,18 +389,17 @@ const loggedInViewerRouter = createProtectedRouter()
AND: passedBookingsFilter, AND: passedBookingsFilter,
}, },
select: { select: {
...bookingMinimalSelect,
uid: true, uid: true,
title: true,
description: true,
attendees: true,
confirmed: true, confirmed: true,
rejected: true, rejected: true,
id: true,
startTime: true,
recurringEventId: true, recurringEventId: true,
endTime: true, location: true,
eventType: { eventType: {
select: { select: {
slug: true,
id: true,
eventName: true,
price: true, price: true,
recurringEvent: true, recurringEvent: true,
team: { team: {

View File

@ -1,7 +1,7 @@
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import findValidApiKey from "@calcom/ee/lib/api/findValidApiKey"; import findValidApiKey from "@calcom/ee/lib/api/findValidApiKey";
import prisma from "@calcom/prisma"; import prisma, { bookingMinimalSelect } from "@calcom/prisma";
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.query.apiKey as string; const apiKey = req.query.apiKey as string;
@ -24,10 +24,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
userId: validKey.userId, userId: validKey.userId,
}, },
select: { select: {
description: true, ...bookingMinimalSelect,
startTime: true,
endTime: true,
title: true,
location: true, location: true,
attendees: { attendees: {
select: { select: {

View File

@ -4,7 +4,7 @@ import { v5 as uuidv5 } from "uuid";
import type { CalendarEvent } from "@calcom/types/Calendar"; import type { CalendarEvent } from "@calcom/types/Calendar";
import { BASE_URL } from "./constants"; import { WEBAPP_URL } from "./constants";
const translator = short(); const translator = short();
@ -55,6 +55,25 @@ ${calEvent.additionalNotes}
`; `;
}; };
export const getCustomInputs = (calEvent: CalendarEvent) => {
if (!calEvent.customInputs) {
return "";
}
const customInputsString = Object.keys(calEvent.customInputs)
.map((key) => {
if (!calEvent.customInputs) return "";
if (calEvent.customInputs[key] !== "") {
return `
${key}:
${calEvent.customInputs[key]}
`;
}
})
.join("");
return customInputsString;
};
export const getDescription = (calEvent: CalendarEvent) => { export const getDescription = (calEvent: CalendarEvent) => {
if (!calEvent.description) { if (!calEvent.description) {
return ""; return "";
@ -94,7 +113,7 @@ export const getUid = (calEvent: CalendarEvent): string => {
}; };
export const getCancelLink = (calEvent: CalendarEvent): string => { export const getCancelLink = (calEvent: CalendarEvent): string => {
return BASE_URL + "/cancel/" + getUid(calEvent); return WEBAPP_URL + "/cancel/" + getUid(calEvent);
}; };
export const getRichDescription = (calEvent: CalendarEvent, attendee?: Person) => { export const getRichDescription = (calEvent: CalendarEvent, attendee?: Person) => {
@ -110,6 +129,7 @@ ${calEvent.organizer.language.translate("where")}:
${getLocation(calEvent)} ${getLocation(calEvent)}
${getDescription(calEvent)} ${getDescription(calEvent)}
${getAdditionalNotes(calEvent)} ${getAdditionalNotes(calEvent)}
${getCustomInputs(calEvent)}
`.trim(); `.trim();
} }
@ -121,6 +141,7 @@ ${calEvent.organizer.language.translate("where")}:
${getLocation(calEvent)} ${getLocation(calEvent)}
${getDescription(calEvent)} ${getDescription(calEvent)}
${getAdditionalNotes(calEvent)} ${getAdditionalNotes(calEvent)}
${getCustomInputs(calEvent)}
${getManageLink(calEvent)} ${getManageLink(calEvent)}
`.trim(); `.trim();
}; };

1
packages/lib/index.ts Normal file
View File

@ -0,0 +1 @@
export { default as isPrismaObj, isPrismaObjOrUndefined } from "./isPrismaObj";

View File

@ -0,0 +1,11 @@
import { Prisma } from "@prisma/client";
function isPrismaObj(obj: unknown): obj is Prisma.JsonObject {
return typeof obj === "object" && !Array.isArray(obj);
}
export function isPrismaObjOrUndefined(obj: unknown) {
return isPrismaObj(obj) ? obj : undefined;
}
export default isPrismaObj;

View File

@ -3,6 +3,7 @@ import { PrismaClient } from "@prisma/client";
import { bookingReferenceMiddleware } from "./middleware"; import { bookingReferenceMiddleware } from "./middleware";
declare global { declare global {
// eslint-disable-next-line no-var
var prisma: PrismaClient | undefined; var prisma: PrismaClient | undefined;
} }
@ -19,3 +20,5 @@ if (process.env.NODE_ENV !== "production") {
bookingReferenceMiddleware(prisma); bookingReferenceMiddleware(prisma);
export default prisma; export default prisma;
export * from "./selects";

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Booking" ADD COLUMN "customInputs" JSONB;

View File

@ -263,6 +263,7 @@ model Booking {
eventTypeId Int? eventTypeId Int?
title String title String
description String? description String?
customInputs Json?
startTime DateTime startTime DateTime
endTime DateTime endTime DateTime
attendees Attendee[] attendees Attendee[]

View File

@ -0,0 +1,11 @@
import { Prisma } from "@prisma/client";
export const bookingMinimalSelect = Prisma.validator<Prisma.BookingSelect>()({
id: true,
title: true,
description: true,
customInputs: true,
startTime: true,
endTime: true,
attendees: true,
});

View File

@ -0,0 +1 @@
export * from "./booking";

View File

@ -1,4 +1,4 @@
import type { DestinationCalendar, SelectedCalendar } from "@prisma/client"; import type { Prisma, DestinationCalendar, SelectedCalendar } from "@prisma/client";
import type { Dayjs } from "dayjs"; import type { Dayjs } from "dayjs";
import type { calendar_v3 } from "googleapis"; import type { calendar_v3 } from "googleapis";
import type { Time } from "ical.js"; import type { Time } from "ical.js";
@ -97,6 +97,7 @@ export interface CalendarEvent {
organizer: Person; organizer: Person;
attendees: Person[]; attendees: Person[];
additionalNotes?: string | null; additionalNotes?: string | null;
customInputs?: Prisma.JsonObject | null;
description?: string | null; description?: string | null;
team?: { team?: {
name: string; name: string;