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 GitHub
parent 463aff0702
commit 4b6c701c48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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 { PaperAirplaneIcon } from "@heroicons/react/outline";
import {
BanIcon,
CheckIcon,
ClockIcon,
PaperAirplaneIcon,
PencilAltIcon,
XIcon,
} from "@heroicons/react/outline";
import { RefreshIcon } from "@heroicons/react/solid";
import { BookingStatus } from "@prisma/client";
import dayjs from "dayjs";
import { useRouter } from "next/router";
import { useState } from "react";
import { useMutation } from "react-query";
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 useMeQuery from "@lib/hooks/useMeQuery";
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 TableActions, { ActionType } from "@components/ui/TableActions";
@ -37,6 +44,7 @@ function BookingListItem(booking: BookingItemProps) {
const user = query.data;
const { t, i18n } = useLocale();
const utils = trpc.useContext();
const router = useRouter();
const [rejectionReason, setRejectionReason] = useState<string>("");
const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false);
const mutation = useMutation(
@ -81,7 +89,10 @@ function BookingListItem(booking: BookingItemProps) {
booking.listingStatus === "upcoming" && booking.recurringEventId !== null
? t("reject_all")
: t("reject"),
onClick: () => setRejectionDialogIsOpen(true),
onClick: (e) => {
e.stopPropagation();
setRejectionDialogIsOpen(true);
},
icon: BanIcon,
disabled: mutation.isLoading,
},
@ -91,7 +102,10 @@ function BookingListItem(booking: BookingItemProps) {
booking.listingStatus === "upcoming" && booking.recurringEventId !== null
? t("confirm_all")
: t("confirm"),
onClick: () => mutation.mutate(true),
onClick: (e) => {
e.stopPropagation();
mutation.mutate(true);
},
icon: CheckIcon,
disabled: mutation.isLoading,
color: "primary",
@ -120,7 +134,10 @@ function BookingListItem(booking: BookingItemProps) {
id: "reschedule_request",
icon: ClockIcon,
label: t("send_reschedule_request"),
onClick: () => setIsOpenRescheduleDialog(true),
onClick: (e) => {
e.stopPropagation();
setIsOpenRescheduleDialog(true);
},
},
],
},
@ -150,6 +167,7 @@ function BookingListItem(booking: BookingItemProps) {
i18n
);
}
return (
<>
<RescheduleDialog
@ -191,7 +209,30 @@ function BookingListItem(booking: BookingItemProps) {
</DialogContent>
</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">
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
<div className="text-sm text-gray-500">
@ -264,9 +305,12 @@ function BookingListItem(booking: BookingItemProps) {
)}
{booking.attendees.length !== 0 && (
<div className="text-sm text-gray-900 hover:text-blue-500">
<a href={"mailto:" + booking.attendees[0].email}>{booking.attendees[0].email}</a>
</div>
<a
className="text-sm text-gray-900 hover:text-blue-500"
href={"mailto:" + booking.attendees[0].email}
onClick={(e) => e.stopPropagation()}>
{booking.attendees[0].email}
</a>
)}
{isCancelled && booking.rescheduled && (
<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 { EventType, PeriodType } from "@prisma/client";
import { PeriodType } from "@prisma/client";
import dayjs, { Dayjs } from "dayjs";
import dayjsBusinessTime from "dayjs-business-days2";
import timezone from "dayjs/plugin/timezone";
@ -14,6 +14,7 @@ import classNames from "@lib/classNames";
import { timeZone } from "@lib/clock";
import { weekdayNames } from "@lib/core/i18n/weekday";
import { doWorkAsync } from "@lib/doWorkAsync";
import isOutOfBounds from "@lib/isOutOfBounds";
import getSlots from "@lib/slots";
import { WorkingHours } from "@lib/types/schedule";
@ -37,42 +38,6 @@ type DatePickerProps = {
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({
weekStart,
onDatePicked,

View File

@ -77,7 +77,7 @@ type BookingFormValues = {
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;
[key: string]: string | boolean;
};
};
@ -216,7 +216,7 @@ const BookingPage = ({
}, [router.query.guest]);
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
? booking?.attendees.slice(1).map((attendee) => attendee.email)
: [];
@ -244,11 +244,22 @@ const BookingPage = ({
if (!primaryAttendee) {
return {};
}
const customInputType = booking.customInputs;
return {
name: primaryAttendee.name || "",
email: primaryAttendee.email || "",
guests: guestListEmails,
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 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 (
<div>
@ -541,10 +555,7 @@ const BookingPage = ({
name="name"
id="name"
required
className={classNames(
"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" : ""
)}
className={inputClassName}
placeholder={t("example_name")}
disabled={disableInput}
/>
@ -561,8 +572,7 @@ const BookingPage = ({
{...bookingForm.register("email")}
required
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",
disableInput ? "bg-gray-200 dark:text-gray-500" : "",
inputClassName,
bookingForm.formState.errors.email
? "border-red-700 focus:ring-red-700"
: " border-gray-300 dark:border-gray-900"
@ -637,12 +647,9 @@ const BookingPage = ({
})}
id={"custom_" + input.id}
rows={3}
className={classNames(
"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" : ""
)}
className={inputClassName}
placeholder={input.placeholder}
disabled={disableInput}
disabled={disabledExceptForOwner}
/>
)}
{input.type === EventTypeCustomInputType.TEXT && (
@ -652,9 +659,9 @@ const BookingPage = ({
required: input.required,
})}
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}
disabled={disableInput}
disabled={disabledExceptForOwner}
/>
)}
{input.type === EventTypeCustomInputType.NUMBER && (
@ -664,8 +671,9 @@ const BookingPage = ({
required: input.required,
})}
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=""
disabled={disabledExceptForOwner}
/>
)}
{input.type === EventTypeCustomInputType.BOOL && (
@ -676,8 +684,9 @@ const BookingPage = ({
required: input.required,
})}
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=""
disabled={disabledExceptForOwner}
/>
<label
htmlFor={"custom_" + input.id}
@ -764,12 +773,9 @@ const BookingPage = ({
id="notes"
name="notes"
rows={3}
className={classNames(
"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" : ""
)}
className={inputClassName}
placeholder={t("share_additional_notes")}
disabled={disableInput}
disabled={disabledExceptForOwner}
/>
</div>
<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 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";
@ -12,15 +12,27 @@ export type ActionType = {
label: string;
disabled?: boolean;
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 {
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 (
<Dropdown>
{!actionTrigger ? (
@ -40,7 +52,7 @@ const DropdownActions = ({ actions, actionTrigger }: { actions: ActionType[]; ac
className="w-full rounded-none font-normal"
href={action.href}
StartIcon={action.icon}
onClick={action.onClick}
onClick={action.onClick || defaultAction}
data-testid={action.id}>
{action.label}
</Button>
@ -67,7 +79,7 @@ const TableActions: FC<Props> = ({ actions }) => {
key={action.id}
data-testid={action.id}
href={action.href}
onClick={action.onClick}
onClick={action.onClick || defaultAction}
StartIcon={action.icon}
{...(action?.actions ? { EndIcon: ChevronDownIcon } : null)}
disabled={action.disabled}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -169,6 +169,7 @@ ${getRichDescription(this.calEvent)}
${this.getLocation()}
${this.getDescription()}
${this.getAdditionalNotes()}
${this.getCustomInputs()}
</div>
</td>
</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 {
if (!this.calEvent.rejectionReason) return "";
return `

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -162,6 +162,7 @@ ${getRichDescription(this.calEvent)}
${this.getLocation()}
${this.getDescription()}
${this.getAdditionalNotes()}
${this.getCustomInputs()}
</div>
</td>
</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 {
if (!this.calEvent.description) return "";
return `

View File

@ -8,6 +8,7 @@ async function getBooking(prisma: PrismaClient, uid: string) {
select: {
startTime: true,
description: true,
customInputs: true,
attendees: {
select: {
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;
user?: string | string[];
language: string;
customInputs: { label: string; value: string }[];
customInputs: { label: string; value: string | boolean }[];
metadata: {
[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 EventManager from "@calcom/core/EventManager";
import { isPrismaObjOrUndefined } from "@calcom/lib";
import logger from "@calcom/lib/logger";
import type { AdditionInformation, RecurringEvent } from "@calcom/types/Calendar";
import type { CalendarEvent } from "@calcom/types/Calendar";
import type { AdditionInformation, CalendarEvent, RecurringEvent } from "@calcom/types/Calendar";
import { refund } from "@ee/lib/stripe/server";
import { asStringOrNull } from "@lib/asStringOrNull";
import { getSession } from "@lib/auth";
import { sendDeclinedEmails } from "@lib/emails/email-manager";
import { sendScheduledEmails } from "@lib/emails/email-manager";
import { sendDeclinedEmails, sendScheduledEmails } from "@lib/emails/email-manager";
import prisma from "@lib/prisma";
import { BookingConfirmBody } from "@lib/types/booking";
@ -89,6 +88,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
select: {
title: true,
description: true,
customInputs: true,
startTime: true,
endTime: true,
confirmed: true,
@ -156,6 +156,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
type: booking.title,
title: booking.title,
description: booking.description,
customInputs: isPrismaObjOrUndefined(booking.customInputs),
startTime: booking.startTime.toISOString(),
endTime: booking.endTime.toISOString(),
organizer: {

View File

@ -11,6 +11,7 @@ import short from "short-uuid";
import { v5 as uuidv5 } from "uuid";
import EventManager from "@calcom/core/EventManager";
import { isPrismaObjOrUndefined } from "@calcom/lib";
import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defaultEvents";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import logger from "@calcom/lib/logger";
@ -28,6 +29,7 @@ import {
import { ensureArray } from "@lib/ensureArray";
import { getEventName } from "@lib/event";
import getBusyTimes from "@lib/getBusyTimes";
import isOutOfBounds from "@lib/isOutOfBounds";
import prisma from "@lib/prisma";
import { BookingCreateBody } from "@lib/types/booking";
import sendPayload from "@lib/webhooks/sendPayload";
@ -104,32 +106,6 @@ function isAvailable(busyTimes: BufferedBusyTimes, time: dayjs.ConfigType, lengt
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>()({
select: {
id: true,
@ -348,18 +324,22 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
t: tOrganizer,
};
const additionalNotes =
reqBody.notes +
reqBody.customInputs.reduce(
(str, input) => str + "<br /><br />" + input.label + ":<br />" + input.value,
""
);
const additionalNotes = reqBody.notes;
const customInputs = {} as NonNullable<CalendarEvent["customInputs"]>;
if (reqBody.customInputs.length > 0) {
reqBody.customInputs.forEach(({ label, value }) => {
customInputs[label] = value;
});
}
const evt: CalendarEvent = {
type: eventType.title,
title: getEventName(eventNameObject), //this needs to be either forced in english, or fetched for each attendee and organizer separately
description: eventType.description,
additionalNotes,
customInputs,
startTime: reqBody.start,
endTime: reqBody.end,
organizer: {
@ -456,6 +436,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
startTime: dayjs(evt.startTime).toDate(),
endTime: dayjs(evt.endTime).toDate(),
description: evt.additionalNotes,
customInputs: isPrismaObjOrUndefined(evt.customInputs),
confirmed: (!eventType.requiresConfirmation && !eventType.price) || !!rescheduleUid,
location: evt.location,
eventType: eventTypeRel,
@ -613,7 +594,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
periodEndDate: eventType.periodEndDate,
periodStartDate: eventType.periodStartDate,
periodCountCalendarDays: eventType.periodCountCalendarDays,
timeZone: currentUser.timeZone,
});
} catch {
log.debug({
@ -731,6 +711,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
...evt,
additionInformation: metadata,
additionalNotes,
customInputs,
},
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 { getCalendar } from "@calcom/core/CalendarManager";
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 { refund } from "@ee/lib/stripe/server";
import { asStringOrNull } from "@lib/asStringOrNull";
import { getSession } from "@lib/auth";
import { sendCancelledEmails } from "@lib/emails/email-manager";
import prisma from "@lib/prisma";
import sendPayload from "@lib/webhooks/sendPayload";
import getWebhooks from "@lib/webhooks/subscriptions";
@ -33,7 +34,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
uid,
},
select: {
id: true,
...bookingMinimalSelect,
userId: true,
user: {
select: {
@ -45,7 +46,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
destinationCalendar: true,
},
},
attendees: true,
location: true,
references: {
select: {
@ -56,15 +56,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
payment: true,
paid: true,
title: true,
eventType: {
select: {
title: true,
},
},
description: true,
startTime: true,
endTime: true,
uid: true,
eventTypeId: true,
destinationCalendar: true,
@ -115,6 +111,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
title: bookingToDelete?.title,
type: (bookingToDelete?.eventType?.title as string) || bookingToDelete?.title,
description: bookingToDelete?.description || "",
customInputs: isPrismaObjOrUndefined(bookingToDelete.customInputs),
startTime: bookingToDelete?.startTime ? dayjs(bookingToDelete.startTime).format() : "",
endTime: bookingToDelete?.endTime ? dayjs(bookingToDelete.endTime).format() : "",
organizer: {
@ -183,6 +180,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
type: bookingToDelete?.eventType?.title as string,
title: bookingToDelete.title,
description: bookingToDelete.description ?? "",
customInputs: isPrismaObjOrUndefined(bookingToDelete.customInputs),
startTime: bookingToDelete.startTime.toISOString(),
endTime: bookingToDelete.endTime.toISOString(),
organizer: {

View File

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

View File

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

View File

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

View File

@ -40,6 +40,7 @@ import { isBrowserLocale24h } from "@lib/timeFormat";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import CustomBranding from "@components/CustomBranding";
import CancelBooking from "@components/booking/CancelBooking";
import { HeadSeo } from "@components/seo/head-seo";
import { ssrInit } from "@server/lib/ssr";
@ -152,13 +153,13 @@ export default function Success(props: SuccessProps) {
const { data: session } = useSession();
const [date, setDate] = useState(dayjs.utc(asStringOrThrow(router.query.date)));
const { isReady, Theme } = useTheme(props.profile.theme);
const { eventType, bookingInfo } = props;
const isBackgroundTransparent = useIsBackgroundTransparent();
const isEmbed = useIsEmbed();
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
const [isCancellationMode, setIsCancellationMode] = useState(false);
const attendeeName = typeof name === "string" ? name : "Nameless";
@ -247,15 +248,26 @@ export default function Success(props: SuccessProps) {
return t("emailed_you_and_attendees" + titleSuffix);
}
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(
`booking_${needsConfirmation ? "submitted" : "confirmed"}${props.recurringBookings ? "_recurring" : ""}`
);
const customInputs = bookingInfo?.customInputs;
return (
(isReady && (
<>
<div
className={isEmbed ? "" : "h-screen bg-neutral-100 dark:bg-neutral-900"}
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 />
<HeadSeo title={title} description={title} />
<CustomBranding lightVal={props.profile.brandColor} darkVal={props.profile.darkBrandColor} />
@ -357,21 +369,66 @@ export default function Success(props: SuccessProps) {
)}
{bookingInfo?.description && (
<>
<div className="mt-6 font-medium">{t("additional_notes")}</div>
<div className="col-span-2 mt-6 mb-6">
<div className="mt-9 font-medium">{t("additional_notes")}</div>
<div className="col-span-2 mb-2 mt-9">
<p>{bookingInfo.description}</p>
</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>
{!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">
{!needsConfirmation &&
(!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">
{t("add_to_calendar")}
</span>
<div className="flex flex-grow justify-center text-center">
<div className="-ml-16 flex flex-grow justify-center text-center">
<Link
href={
`https://calendar.google.com/calendar/r/eventedit?dates=${date
@ -497,15 +554,6 @@ export default function Success(props: SuccessProps) {
</form>
</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>
@ -618,6 +666,7 @@ const getEventTypesFromDB = async (typeId: number) => {
select: {
id: true,
name: true,
username: true,
hideBranding: true,
plan: true,
theme: true,
@ -629,6 +678,7 @@ const getEventTypesFromDB = async (typeId: number) => {
},
team: {
select: {
slug: true,
name: true,
hideBranding: true,
},
@ -683,6 +733,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
select: {
id: true,
name: true,
username: true,
hideBranding: true,
plan: true,
theme: true,
@ -714,6 +765,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
theme: (!eventType.team?.name && eventType.users[0]?.theme) || null,
brandColor: eventType.team ? null : eventType.users[0].brandColor || 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({
@ -721,7 +773,10 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
id: bookingId,
},
select: {
title: true,
uid: true,
description: true,
customInputs: true,
user: {
select: {
name: true,

View File

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

View File

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

View File

@ -6,11 +6,11 @@ import { getSession } from "next-auth/react";
import { useRouter } from "next/router";
import { useEffect } from "react";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import Button from "@calcom/ui/Button";
import prisma from "@lib/prisma";
import { detectBrowserTimeFormat } from "@lib/timeFormat";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import { HeadSeo } from "@components/seo/head-seo";
@ -90,18 +90,13 @@ export async function getServerSideProps(context: NextPageContext) {
uid: context.query.uid as string,
},
select: {
...bookingMinimalSelect,
uid: true,
id: true,
title: true,
description: true,
startTime: true,
endTime: true,
user: {
select: {
credentials: true,
},
},
attendees: true,
dailyRef: {
select: {
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",
"cancel": "Cancel",
"apply": "Apply",
"cancel_event": "Cancel this event",
"cancel_event": "Cancel event",
"continue": "Continue",
"confirm": "Confirm",
"confirm_all": "Confirm all",
@ -823,11 +823,14 @@
"generate_api_key": "Generate 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.",
"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.",
"go_to_app_store": "Go to App Store",
"calendar_error": "Something went wrong, try reconnecting your calendar with all necessary permissions",
"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: ",
"zapier_invite_link": "Zapier Invite Link"
}

View File

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

View File

@ -1,7 +1,7 @@
import type { NextApiRequest, NextApiResponse } from "next";
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) {
const apiKey = req.query.apiKey as string;
@ -24,10 +24,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
userId: validKey.userId,
},
select: {
description: true,
startTime: true,
endTime: true,
title: true,
...bookingMinimalSelect,
location: true,
attendees: {
select: {

View File

@ -4,7 +4,7 @@ import { v5 as uuidv5 } from "uuid";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { BASE_URL } from "./constants";
import { WEBAPP_URL } from "./constants";
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) => {
if (!calEvent.description) {
return "";
@ -94,7 +113,7 @@ export const getUid = (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) => {
@ -110,6 +129,7 @@ ${calEvent.organizer.language.translate("where")}:
${getLocation(calEvent)}
${getDescription(calEvent)}
${getAdditionalNotes(calEvent)}
${getCustomInputs(calEvent)}
`.trim();
}
@ -121,6 +141,7 @@ ${calEvent.organizer.language.translate("where")}:
${getLocation(calEvent)}
${getDescription(calEvent)}
${getAdditionalNotes(calEvent)}
${getCustomInputs(calEvent)}
${getManageLink(calEvent)}
`.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";
declare global {
// eslint-disable-next-line no-var
var prisma: PrismaClient | undefined;
}
@ -19,3 +20,5 @@ if (process.env.NODE_ENV !== "production") {
bookingReferenceMiddleware(prisma);
export default prisma;
export * from "./selects";

View File

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

View File

@ -262,6 +262,7 @@ model Booking {
eventTypeId Int?
title String
description String?
customInputs Json?
startTime DateTime
endTime DateTime
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 { calendar_v3 } from "googleapis";
import type { Time } from "ical.js";
@ -97,6 +97,7 @@ export interface CalendarEvent {
organizer: Person;
attendees: Person[];
additionalNotes?: string | null;
customInputs?: Prisma.JsonObject | null;
description?: string | null;
team?: {
name: string;