Merge branch 'main' into minimum-booking-notice-will-allow-hours-and-days

This commit is contained in:
Om Ray 2022-11-03 16:01:58 -04:00 committed by GitHub
commit 0384058c90
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 635 additions and 172 deletions

View File

@ -137,3 +137,7 @@ CLOSECOM_API_KEY=
# Sendgrid internal email sender
SENDGRID_API_KEY=
# Sentry
NEXT_PUBLIC_SENTRY_DSN=
SENTRY_IGNORE_API_RESOLUTION_ERROR=

4
apps/web/.gitignore vendored
View File

@ -67,4 +67,6 @@ tsconfig.tsbuildinfo
public/embed
# Copied app-store images
public/app-store
public/app-store
# Sentry
.sentryclirc

View File

@ -176,11 +176,12 @@ function BookingListItem(booking: BookingItemProps) {
setLocationMutation.mutate({ bookingId: booking.id, newLocation });
};
// Extract recurring dates is intensive to run, so use useMemo.
// Calculate the booking date(s) and setup recurring event data to show
// @FIXME: This is importing the RRULE library which is already heavy. Find out a more optimal way do this.
const [recurringStrings, recurringDates] = useMemo(() => {
if (booking.recurringBookings !== undefined && booking.eventType.recurringEvent?.freq !== undefined) {
const [, recurringDates] = useMemo(() => {
if (
booking.recurringBookings !== undefined &&
booking.eventType.recurringEvent?.freq !== undefined &&
booking.recurringEventId
) {
return extractRecurringDates(booking, user?.timeZone, i18n);
}
return [[], []];
@ -275,13 +276,11 @@ function BookingListItem(booking: BookingItemProps) {
attendees={booking.attendees}
/>
</div>
{isPending && (
<Badge className="ltr:mr-2 rtl:ml-2" variant="orange">
{t("unconfirmed")}
</Badge>
)}
{booking.eventType?.team && (
<Badge className="ltr:mr-2 rtl:ml-2" variant="gray">
{booking.eventType.team.name}
@ -292,7 +291,6 @@ function BookingListItem(booking: BookingItemProps) {
{t("pending_payment")}
</Badge>
)}
<div className="mt-2 text-sm text-gray-400">
<RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} />
</div>

View File

@ -7,8 +7,9 @@ import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { weekdayNames } from "@calcom/lib/weekday";
import { trpc } from "@calcom/trpc/react";
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
import { Icon } from "@calcom/ui";
import { Badge } from "@calcom/ui/v2";
import { Badge, SettingsToggle } from "@calcom/ui/v2";
import Button from "@calcom/ui/v2/core/Button";
import Select from "@calcom/ui/v2/core/form/select";
import { SkeletonText } from "@calcom/ui/v2/core/skeleton";
@ -91,93 +92,119 @@ const AvailabilitySelect = ({
);
};
const format = (date: Date) =>
Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "numeric" }).format(
const format = (date: Date, hour12: boolean) =>
Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "numeric", hour12 }).format(
new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss"))
);
export const AvailabilityTab = () => {
export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
const { t, i18n } = useLocale();
const { watch } = useFormContext<FormValues>();
const EventTypeSchedule = () => {
const me = useMeQuery();
const timeFormat = me?.data?.timeFormat;
return (
<div className="space-y-4">
<div>
<div className="min-w-4 mb-2">
<label htmlFor="availability" className="mt-0 flex text-sm font-medium text-neutral-700">
{t("availability")}
</label>
</div>
<Controller
name="schedule"
render={({ field }) => (
<AvailabilitySelect
value={field.value}
onBlur={field.onBlur}
name={field.name}
onChange={(selected) => {
field.onChange(selected?.value || null);
}}
/>
)}
/>
</div>
<div className="space-y-4 rounded border p-4 py-6 pt-2 md:p-8">
<ol className="table border-collapse text-sm">
{weekdayNames(i18n.language, 1, "long").map((day, index) => {
const isAvailable = !!filterDays(index).length;
return (
<li key={day} className="my-6 flex border-transparent last:mb-2">
<span
className={classNames(
"w-20 font-medium sm:w-32",
!isAvailable && "text-gray-500 opacity-50"
)}>
{day}
</span>
{isLoading ? (
<SkeletonText className="block h-5 w-60" />
) : isAvailable ? (
<div className="space-y-3 text-right">
{filterDays(index).map((dayRange, i) => (
<div key={i} className="flex items-center leading-4">
<span className="w-16 sm:w-28 sm:text-left">
{format(dayRange.startTime, timeFormat === 12)}
</span>
<span className="ml-4">-</span>
<div className="ml-6">{format(dayRange.endTime, timeFormat === 12)}</div>
</div>
))}
</div>
) : (
<span className="ml-6 text-gray-500 opacity-50 sm:ml-0">{t("unavailable")}</span>
)}
</li>
);
})}
</ol>
<hr />
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
<span className="flex items-center justify-center text-sm text-gray-600 sm:justify-start">
<Icon.FiGlobe className="mr-2" />
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
</span>
<Button
href={`/availability/${schedule?.schedule.id}`}
color="minimal"
EndIcon={Icon.FiExternalLink}
target="_blank"
className="justify-center border sm:border-0"
rel="noopener noreferrer">
{t("edit_availability")}
</Button>
</div>
</div>
</div>
);
};
const scheduleId = watch("schedule");
const { isLoading, data: schedule } = trpc.useQuery(["viewer.availability.schedule", { scheduleId }]);
const filterDays = (dayNum: number) =>
schedule?.schedule.availability.filter((item) => item.days.includes((dayNum + 1) % 7)) || [];
return (
<>
<div>
<div className="min-w-4 mb-2">
<label htmlFor="availability" className="mt-0 flex text-sm font-medium text-neutral-700">
{t("availability")}
</label>
</div>
<Controller
name="schedule"
render={({ field }) => (
<AvailabilitySelect
value={field.value}
onBlur={field.onBlur}
name={field.name}
onChange={(selected) => {
field.onChange(selected?.value || null);
}}
/>
)}
/>
</div>
if (!isTeamEvent) {
return <EventTypeSchedule />;
}
<div className="space-y-4 rounded border p-4 py-6 pt-2 md:p-8">
<ol className="table border-collapse text-sm">
{weekdayNames(i18n.language, 1, "long").map((day, index) => {
const isAvailable = !!filterDays(index).length;
return (
<li key={day} className="my-6 flex border-transparent last:mb-2">
<span
className={classNames(
"w-20 font-medium sm:w-32",
!isAvailable && "text-gray-500 opacity-50"
)}>
{day}
</span>
{isLoading ? (
<SkeletonText className="block h-5 w-60" />
) : isAvailable ? (
<div className="space-y-3 text-right">
{filterDays(index).map((dayRange, i) => (
<div key={i} className="flex items-center leading-4">
<span className="w-16 sm:w-28 sm:text-left">{format(dayRange.startTime)}</span>
<span className="ml-4">-</span>
<div className="ml-6">{format(dayRange.endTime)}</div>
</div>
))}
</div>
) : (
<span className="ml-6 text-gray-500 opacity-50 sm:ml-0">{t("unavailable")}</span>
)}
</li>
);
})}
</ol>
<hr />
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
<span className="flex items-center justify-center text-sm text-gray-600 sm:justify-start">
<Icon.FiGlobe className="mr-2" />
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
</span>
<Button
href={`/availability/${schedule?.schedule.id}`}
color="minimal"
EndIcon={Icon.FiExternalLink}
target="_blank"
className="justify-center border sm:border-0"
rel="noopener noreferrer">
{t("edit_availability")}
</Button>
</div>
</div>
</>
return (
<Controller
name="metadata.config.useHostSchedulesForTeamEvent"
render={({ field: { value, onChange } }) => (
<SettingsToggle
checked={!value}
onCheckedChange={(checked) => {
onChange(!checked);
}}
title={t("choose_common_schedule_team_event")}
description={t("choose_common_schedule_team_event_description")}>
<EventTypeSchedule />
</SettingsToggle>
)}
/>
);
};

View File

@ -75,11 +75,16 @@ export const extractRecurringDates = (
const recurringInfo = booking.recurringBookings.find(
(val) => val.recurringEventId === booking.recurringEventId
);
if (!recurringInfo) {
// something went wrong, fail here before RRule.
return [[], []];
}
const allDates = new RRule({
...rest,
count: recurringInfo?._count.recurringEventId,
dtstart: recurringInfo?._min.startTime,
}).all();
const utcOffset = dayjs(recurringInfo?._min.startTime).tz(timeZone).utcOffset();
const dateStrings = allDates.map((r) => {
return processDate(dayjs.utc(r).utcOffset(utcOffset), i18n);

View File

@ -39,6 +39,10 @@ const middleware: NextMiddleware = async (req) => {
return NextResponse.next();
};
export const config = {
matcher: ["/api/collect-events/:path*", "/api/auth/:path*", "/apps/routing_forms/:path*", "/:path*/embed"],
};
export default collectEvents({
middleware,
...nextCollectBasicSettings,

View File

@ -1,5 +1,6 @@
require("dotenv").config({ path: "../../.env" });
const CopyWebpackPlugin = require("copy-webpack-plugin");
const { withSentryConfig } = require("@sentry/nextjs");
const withTM = require("next-transpile-modules")([
"@calcom/app-store",
@ -228,6 +229,18 @@ const nextConfig = {
return redirects;
},
sentry: {
hideSourceMaps: true,
},
};
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
const sentryWebpackPluginOptions = {
silent: true, // Suppresses all logs
};
const moduleExports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
// Sentry should be the last thing to export to catch everything right
module.exports = process.env.NEXT_PUBLIC_SENTRY_DSN
? withSentryConfig(moduleExports, sentryWebpackPluginOptions)
: moduleExports;

View File

@ -56,6 +56,7 @@
"@radix-ui/react-switch": "^1.0.0",
"@radix-ui/react-toggle-group": "^1.0.0",
"@radix-ui/react-tooltip": "^1.0.0",
"@sentry/nextjs": "^7.17.3",
"@stripe/react-stripe-js": "^1.10.0",
"@stripe/stripe-js": "^1.35.0",
"@tanstack/react-query": "^4.3.9",

View File

@ -2,6 +2,7 @@
* Typescript class based component for custom-error
* @link https://nextjs.org/docs/advanced-features/custom-error-page
*/
import * as Sentry from "@sentry/nextjs";
import { NextPage, NextPageContext } from "next";
import NextError, { ErrorProps } from "next/error";
import React from "react";
@ -46,7 +47,9 @@ const CustomError: NextPage<CustomErrorProps> = (props) => {
* Partially adapted from the example in
* https://github.com/vercel/next.js/tree/canary/examples/with-sentry
*/
CustomError.getInitialProps = async ({ res, err, asPath }: AugmentedNextPageContext) => {
CustomError.getInitialProps = async (ctx: AugmentedNextPageContext) => {
const { res, err, asPath } = ctx;
await Sentry.captureUnderscoreErrorException(ctx);
const errorInitialProps = (await NextError.getInitialProps({
res,
err,

View File

@ -9,6 +9,8 @@ import { AppGetServerSideProps } from "@calcom/types/AppGetServerSideProps";
import { AppProps } from "@lib/app-providers";
import { getSession } from "@lib/auth";
import { ssrInit } from "@server/lib/ssr";
type AppPageType = {
getServerSideProps: AppGetServerSideProps;
// A component than can accept any properties
@ -128,7 +130,8 @@ export async function getServerSideProps(
appPages: string[];
}>,
prisma,
user
user,
ssrInit
);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore

View File

@ -201,7 +201,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
teamMembers={teamMembers}
/>
),
availability: <AvailabilityTab />,
availability: <AvailabilityTab isTeamEvent={!!team} />,
team: (
<EventTeamTab
eventType={eventType}

View File

@ -1225,6 +1225,8 @@
"exchange_authentication_ntlm": "NTLM authentication",
"exchange_compression": "GZip compression",
"routing_forms_description": "You can see all forms and routes you have created here.",
"routing_forms_send_email_owner": "Send Email to Owner",
"routing_forms_send_email_owner_description": "Sends an email to the owner when the form is submitted",
"add_new_form": "Add new form",
"form_description": "Create your form to route a booker",
"copy_link_to_form": "Copy link to form",
@ -1339,5 +1341,7 @@
"number_sms_notifications": "Phone number (SMS\u00a0notifications)",
"attendee_email_workflow": "Attendee email",
"attendee_email_info": "The person booking's email",
"invalid_credential": "Oh no! Looks like permission expired or was revoked. Please reinstall again."
"invalid_credential": "Oh no! Looks like permission expired or was revoked. Please reinstall again.",
"choose_common_schedule_team_event": "Choose a common schedule",
"choose_common_schedule_team_event_description": "Enable this if you want to use a common schedule between hosts. When disabled, each host will be booked based on their default schedule."
}

View File

@ -0,0 +1,16 @@
// This file configures the initialization of Sentry on the browser.
// The config you add here will be used whenever a page is visited.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN;
Sentry.init({
dsn: SENTRY_DSN,
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1.0,
// ...
// Note: if you want to override the automatic release value, do not set a
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
// that it will also get attached to your source maps
});

View File

@ -0,0 +1,3 @@
defaults.url=https://sentry.io/
defaults.org=calcom
defaults.project=cal

View File

@ -0,0 +1,16 @@
// This file configures the initialization of Sentry on the server.
// The config you add here will be used whenever the server handles a request.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN;
Sentry.init({
dsn: SENTRY_DSN,
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1.0,
// ...
// Note: if you want to override the automatic release value, do not set a
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
// that it will also get attached to your source maps
});

View File

@ -1,6 +1,6 @@
import { App_RoutingForms_Form } from "@prisma/client";
import { useEffect } from "react";
import { useForm, UseFormReturn } from "react-hook-form";
import { useForm, UseFormReturn, Controller } from "react-hook-form";
import useApp from "@calcom/lib/hooks/useApp";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@ -10,6 +10,7 @@ import { Form } from "@calcom/ui/form/fields";
import { showToast, DropdownMenuSeparator } from "@calcom/ui/v2";
import { ButtonGroup, TextAreaField, TextField, Tooltip, Button, VerticalDivider } from "@calcom/ui/v2";
import Meta from "@calcom/ui/v2/core/Meta";
import SettingsToggle from "@calcom/ui/v2/core/SettingsToggle";
import { ShellMain } from "@calcom/ui/v2/core/Shell";
import Banner from "@calcom/ui/v2/core/banner";
@ -187,6 +188,7 @@ type SingleFormComponentProps = {
function SingleForm({ form, appUrl, Page }: SingleFormComponentProps) {
const utils = trpc.useContext();
const { t } = useLocale();
const hookForm = useForm({
defaultValues: form,
@ -241,6 +243,23 @@ function SingleForm({ form, appUrl, Page }: SingleFormComponentProps) {
{...hookForm.register("description")}
defaultValue={form.description || ""}
/>
<div className="mt-6">
<Controller
name="settings.emailOwnerOnSubmission"
control={hookForm.control}
render={({ field: { value, onChange } }) => {
return (
<SettingsToggle
title={t("routing_forms_send_email_owner")}
description={t("routing_forms_send_email_owner_description")}
checked={value}
onCheckedChange={(val) => onChange(val)}
/>
);
}}
/>
</div>
{!form._count?.responses && (
<Banner
className="mt-6"

View File

@ -49,7 +49,7 @@ const TextWidget = (props: TextWidgetProps & { type?: string }) => {
const textValue = value || "";
return (
<TextField
containerClassName="w-full mt-2"
containerClassName="w-full"
type={type}
className="dark:border-darkgray-300 flex flex-grow border-gray-300 text-sm dark:bg-transparent dark:text-white dark:selection:bg-green-500 disabled:dark:text-gray-500"
value={textValue}
@ -66,6 +66,7 @@ function NumberWidget({ value, setValue, ...remainingProps }: NumberWidgetProps)
return (
<TextField
type="number"
containerClassName="w-full"
className="dark:border-darkgray-300 mt-0 border-gray-300 text-sm dark:bg-transparent dark:text-white dark:selection:bg-green-500 disabled:dark:text-gray-500"
value={value}
onChange={(e) => {
@ -101,7 +102,7 @@ const MultiSelectWidget = ({
return (
<Select
className="dark:border-darkgray-300 block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 dark:bg-transparent dark:text-white dark:selection:bg-green-500 disabled:dark:text-gray-500 sm:text-sm"
className="dark:border-darkgray-300 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 dark:bg-transparent dark:text-white dark:selection:bg-green-500 disabled:dark:text-gray-500 sm:text-sm"
menuPosition="fixed"
onChange={(items) => {
setValue(items?.map((item) => item.value));
@ -135,7 +136,7 @@ function SelectWidget({
return (
<Select
className="data-testid-select dark:border-darkgray-300 block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 dark:bg-transparent dark:text-white dark:selection:bg-green-500 disabled:dark:text-gray-500 sm:text-sm"
className="data-testid-select dark:border-darkgray-300 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 dark:bg-transparent dark:text-white dark:selection:bg-green-500 disabled:dark:text-gray-500 sm:text-sm"
menuPosition="fixed"
onChange={(item) => {
if (!item) {

View File

@ -0,0 +1,52 @@
import { BaseEmailHtml, Info } from "@calcom/emails/src/components";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { Response } from "../../types/types";
import { App_RoutingForms_Form } from ".prisma/client";
export const ResponseEmail = ({
form,
response,
...props
}: {
form: Pick<App_RoutingForms_Form, "id" | "name">;
response: Response;
subject: string;
} & Partial<React.ComponentProps<typeof BaseEmailHtml>>) => {
return (
<BaseEmailHtml
callToAction={
<div
style={{
fontFamily: "Roboto, Helvetica, sans-serif",
fontSize: "16px",
fontWeight: 500,
lineHeight: "0px",
textAlign: "left",
color: "#3e3e3e",
}}>
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
<a href={`${WEBAPP_URL}/apps/routing-forms/form-edit/${form.id}`} style={{ color: "#3e3e3e" }}>
<>Manage this form</>
</a>
</p>
</div>
}
title={form.name}
subtitle="New Response Received"
{...props}>
{Object.entries(response).map(([fieldId, fieldResponse]) => {
return (
<Info
withSpacer
key={fieldId}
label={fieldResponse.label}
description={
fieldResponse.value instanceof Array ? fieldResponse.value.join(",") : fieldResponse.value
}
/>
);
})}
</BaseEmailHtml>
);
};

View File

@ -0,0 +1 @@
export { ResponseEmail } from "./ResponseEmail";

View File

@ -0,0 +1,33 @@
import { renderEmail } from "@calcom/emails";
import BaseEmail from "@calcom/emails/templates/_base-email";
import { Response } from "../../types/types";
import { App_RoutingForms_Form } from ".prisma/client";
type Form = Pick<App_RoutingForms_Form, "id" | "name">;
export default class ResponseEmail extends BaseEmail {
response: Response;
toAddresses: string[];
form: Form;
constructor({ toAddresses, response, form }: { form: Form; toAddresses: string[]; response: Response }) {
super();
this.form = form;
this.response = response;
this.toAddresses = toAddresses;
}
protected getNodeMailerPayload(): Record<string, unknown> {
const toAddresses = this.toAddresses;
const subject = `${this.form.name} has a new response`;
return {
from: `Cal.com <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
subject,
html: renderEmail("ResponseEmail", {
form: this.form,
response: this.response,
subject,
}),
};
}
}

View File

@ -1,5 +1,7 @@
import { App_RoutingForms_Form } from "@prisma/client";
import { RoutingFormSettings } from "@calcom/prisma/zod-utils";
import { SerializableForm } from "../types/types";
import { zodFields, zodRoutes } from "../zod";
@ -13,10 +15,17 @@ export function getSerializableForm<TForm extends App_RoutingForms_Form>(form: T
if (!fieldsParsed.success) {
throw new Error("Error parsing fields");
}
const settings = RoutingFormSettings.parse(
form.settings || {
// Would have really loved to do it using zod. But adding .default(true) throws type error in prisma/zod/app_routingforms_form.ts
emailOwnerOnSubmission: true,
}
);
// Ideally we shouldb't have needed to explicitly type it but due to some reason it's not working reliably with VSCode TypeCheck
const serializableForm: SerializableForm<TForm> = {
...form,
settings: settings,
fields: fieldsParsed.data,
routes: routesParsed.data,
createdAt: form.createdAt.toString(),

View File

@ -6,7 +6,12 @@ import { UseFormReturn } from "react-hook-form";
import { v4 as uuidv4 } from "uuid";
import classNames from "@calcom/lib/classNames";
import { AppGetServerSidePropsContext, AppPrisma, AppUser } from "@calcom/types/AppGetServerSideProps";
import {
AppGetServerSidePropsContext,
AppPrisma,
AppUser,
AppSsrInit,
} from "@calcom/types/AppGetServerSideProps";
import { Icon } from "@calcom/ui";
import { Button, EmptyScreen, SelectField, TextAreaField, TextField, Shell } from "@calcom/ui/v2";
import { BooleanToggleGroupField } from "@calcom/ui/v2/core/form/BooleanToggleGroup";
@ -300,8 +305,10 @@ FormEditPage.getLayout = (page: React.ReactElement) => {
export const getServerSideProps = async function getServerSideProps(
context: AppGetServerSidePropsContext,
prisma: AppPrisma,
user: AppUser
user: AppUser,
ssrInit: AppSsrInit
) {
const ssr = await ssrInit(context);
if (!user) {
return {
redirect: {
@ -349,6 +356,8 @@ export const getServerSideProps = async function getServerSideProps(
}
return {
props: {
trpcState: ssr.dehydrate(),
form: getSerializableForm(form),
},
};

View File

@ -6,7 +6,12 @@ import { Query, Config, Builder, Utils as QbUtils } from "react-awesome-query-bu
import { JsonTree, ImmutableTree, BuilderProps } from "react-awesome-query-builder";
import { trpc } from "@calcom/trpc/react";
import { AppGetServerSidePropsContext, AppPrisma, AppUser } from "@calcom/types/AppGetServerSideProps";
import {
AppGetServerSidePropsContext,
AppPrisma,
AppUser,
AppSsrInit,
} from "@calcom/types/AppGetServerSideProps";
import { inferSSRProps } from "@calcom/types/inferSSRProps";
import { Icon } from "@calcom/ui";
import { Button, TextField, SelectWithValidation as Select, TextArea, Shell } from "@calcom/ui/v2";
@ -463,8 +468,11 @@ RouteBuilder.getLayout = (page: React.ReactElement) => {
export const getServerSideProps = async function getServerSideProps(
context: AppGetServerSidePropsContext,
prisma: AppPrisma,
user: AppUser
user: AppUser,
ssrInit: AppSsrInit
) {
const ssr = await ssrInit(context);
if (!user) {
return {
redirect: {
@ -513,6 +521,7 @@ export const getServerSideProps = async function getServerSideProps(
return {
props: {
trpcState: ssr.dehydrate(),
form: getSerializableForm(form),
},
};

View File

@ -1,16 +1,77 @@
import { Prisma, WebhookTriggerEvents } from "@prisma/client";
import { App_RoutingForms_Form, Prisma, User, WebhookTriggerEvents } from "@prisma/client";
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import { sendGenericWebhookPayload } from "@calcom/features/webhooks/lib/sendPayload";
import logger from "@calcom/lib/logger";
import { RoutingFormSettings } from "@calcom/prisma/zod-utils";
import { TRPCError } from "@calcom/trpc/server";
import { createProtectedRouter, createRouter } from "@calcom/trpc/server/createRouter";
import { Ensure } from "@calcom/types/utils";
import ResponseEmail from "./emails/templates/response-email";
import { getSerializableForm } from "./lib/getSerializableForm";
import { isAllowed } from "./lib/isAllowed";
import { Response, SerializableForm } from "./types/types";
import { zodFields, zodRoutes } from "./zod";
async function onFormSubmission(
form: Ensure<SerializableForm<App_RoutingForms_Form> & { user: User }, "fields">,
response: Response
) {
const fieldResponsesByName: Record<string, typeof response[keyof typeof response]["value"]> = {};
for (const [fieldId, fieldResponse] of Object.entries(response)) {
// Use the label lowercased as the key to identify a field.
const key =
form.fields.find((f) => f.id === fieldId)?.identifier ||
(fieldResponse.label as keyof typeof fieldResponsesByName);
fieldResponsesByName[key] = fieldResponse.value;
}
const subscriberOptions = {
userId: form.user.id,
// It isn't an eventType webhook
eventTypeId: -1,
triggerEvent: WebhookTriggerEvents.FORM_SUBMITTED,
};
const webhooks = await getWebhooks(subscriberOptions);
const promises = webhooks.map((webhook) => {
sendGenericWebhookPayload(
webhook.secret,
"FORM_SUBMITTED",
new Date().toISOString(),
webhook,
fieldResponsesByName
).catch((e) => {
console.error(`Error executing routing form webhook`, webhook, e);
});
});
await Promise.all(promises);
if (form.settings?.emailOwnerOnSubmission) {
logger.debug(
`Preparing to send Form Response email for Form:${form.id} to form owner: ${form.user.email}`
);
await sendResponseEmail(form, response, form.user.email);
}
}
const sendResponseEmail = async (
form: Pick<App_RoutingForms_Form, "id" | "name">,
response: Response,
ownerEmail: string
) => {
try {
const email = new ResponseEmail({ form: form, toAddresses: [ownerEmail], response: response });
await email.sendEmail();
} catch (e) {
logger.error("Error sending response email", e);
}
};
const app_RoutingForms = createRouter()
.merge(
"public.",
@ -41,24 +102,21 @@ const app_RoutingForms = createRouter()
code: "NOT_FOUND",
});
}
const fieldsParsed = zodFields.safeParse(form.fields);
if (!fieldsParsed.success) {
// This should not be possible normally as before saving the form it is verified by zod
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
});
}
const fields = fieldsParsed.data;
if (!fields) {
const serializableForm = getSerializableForm(form);
if (!serializableForm.fields) {
// There is no point in submitting a form that doesn't have fields defined
throw new TRPCError({
code: "BAD_REQUEST",
});
}
const missingFields = fields
const serializableFormWithFields = {
...serializableForm,
fields: serializableForm.fields,
};
const missingFields = serializableFormWithFields.fields
.filter((field) => !(field.required ? response[field.id]?.value : true))
.map((f) => f.label);
@ -68,7 +126,7 @@ const app_RoutingForms = createRouter()
message: `Missing required fields ${missingFields.join(", ")}`,
});
}
const invalidFields = fields
const invalidFields = serializableFormWithFields.fields
.filter((field) => {
const fieldValue = response[field.id]?.value;
// The field isn't required at this point. Validate only if it's set
@ -94,39 +152,12 @@ const app_RoutingForms = createRouter()
});
}
const fieldResponsesByName: Record<string, typeof response[keyof typeof response]["value"]> = {};
for (const [fieldId, fieldResponse] of Object.entries(response)) {
// Use the label lowercased as the key to identify a field.
const key =
fields.find((f) => f.id === fieldId)?.identifier ||
(fieldResponse.label as keyof typeof fieldResponsesByName);
fieldResponsesByName[key] = fieldResponse.value;
}
const subscriberOptions = {
userId: form.user.id,
// It isn't an eventType webhook
eventTypeId: -1,
triggerEvent: WebhookTriggerEvents.FORM_SUBMITTED,
};
const webhooks = await getWebhooks(subscriberOptions);
const promises = webhooks.map((webhook) => {
sendGenericWebhookPayload(
webhook.secret,
"FORM_SUBMITTED",
new Date().toISOString(),
webhook,
fieldResponsesByName
).catch((e) => {
console.error(`Error executing routing form webhook`, webhook, e);
});
});
await Promise.all(promises);
return await prisma.app_RoutingForms_FormResponse.create({
const dbFormResponse = await prisma.app_RoutingForms_FormResponse.create({
data: input,
});
await onFormSubmission(serializableFormWithFields, dbFormResponse.response as Response);
return dbFormResponse;
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
if (e.code === "P2002") {
@ -201,9 +232,10 @@ const app_RoutingForms = createRouter()
routes: zodRoutes,
addFallback: z.boolean().optional(),
duplicateFrom: z.string().nullable().optional(),
settings: RoutingFormSettings.optional(),
}),
async resolve({ ctx: { user, prisma }, input }) {
const { name, id, description, disabled, addFallback, duplicateFrom } = input;
const { name, id, description, settings, disabled, addFallback, duplicateFrom } = input;
if (!(await isAllowed({ userId: user.id, formId: id }))) {
throw new TRPCError({
code: "FORBIDDEN",
@ -284,6 +316,7 @@ const app_RoutingForms = createRouter()
fields: fields,
name: name,
description,
settings: settings === null ? Prisma.JsonNull : settings,
routes: routes === null ? Prisma.JsonNull : routes,
},
});

View File

@ -1,6 +1,8 @@
import { App_RoutingForms_Form } from "@prisma/client";
import z from "zod";
import { RoutingFormSettings } from "@calcom/prisma/zod-utils";
import { zodFields, zodRoutes } from "../zod";
export type Response = Record<
@ -17,10 +19,11 @@ export type Routes = z.infer<typeof zodRoutes>;
export type Route = Routes[0];
export type SerializableForm<T extends App_RoutingForms_Form> = Omit<
T,
"fields" | "routes" | "createdAt" | "updatedAt"
"fields" | "routes" | "createdAt" | "updatedAt" | "settings"
> & {
routes: Routes;
fields: Fields;
settings: z.infer<typeof RoutingFormSettings>;
createdAt: string;
updatedAt: string;
};

View File

@ -9,7 +9,7 @@ import logger from "@calcom/lib/logger";
import { checkLimit } from "@calcom/lib/server";
import { performance } from "@calcom/lib/server/perfObserver";
import prisma, { availabilityUserSelect } from "@calcom/prisma";
import { stringToDayjs } from "@calcom/prisma/zod-utils";
import { EventTypeMetaDataSchema, stringToDayjs } from "@calcom/prisma/zod-utils";
import { BookingLimit, EventBusyDetails } from "@calcom/types/Calendar";
import { getBusyTimes } from "./getBusyTimes";
@ -27,14 +27,15 @@ const availabilitySchema = z
})
.refine((data) => !!data.username || !!data.userId, "Either username or userId should be filled in.");
const getEventType = (id: number) =>
prisma.eventType.findUnique({
const getEventType = async (id: number) => {
const eventType = await prisma.eventType.findUnique({
where: { id },
select: {
id: true,
seatsPerTimeSlot: true,
bookingLimits: true,
timeZone: true,
metadata: true,
schedule: {
select: {
availability: true,
@ -50,6 +51,14 @@ const getEventType = (id: number) =>
},
},
});
if (!eventType) {
return eventType;
}
return {
...eventType,
metadata: EventTypeMetaDataSchema.parse(eventType.metadata),
};
};
type EventType = Awaited<ReturnType<typeof getEventType>>;
@ -200,13 +209,15 @@ export async function getUserAvailability(
});
}
}
const schedule = eventType?.schedule
? { ...eventType?.schedule }
: {
...currentUser.schedules.filter(
(schedule) => !currentUser.defaultScheduleId || schedule.id === currentUser.defaultScheduleId
)[0],
};
const schedule =
!eventType?.metadata?.config?.useHostSchedulesForTeamEvent && eventType?.schedule
? { ...eventType?.schedule }
: {
...currentUser.schedules.filter(
(schedule) => !currentUser.defaultScheduleId || schedule.id === currentUser.defaultScheduleId
)[0],
};
const startGetWorkingHours = performance.now();

View File

@ -18,3 +18,4 @@ export { OrganizerRescheduledEmail } from "./OrganizerRescheduledEmail";
export { OrganizerScheduledEmail } from "./OrganizerScheduledEmail";
export { TeamInviteEmail } from "./TeamInviteEmail";
export { BrokenIntegrationEmail } from "./BrokenIntegrationEmail";
export * from "@calcom/app-store/ee/routing-forms/emails/components";

View File

@ -20,7 +20,7 @@ const customTemplate = async (text: string, variables: VariablesType, locale: st
let locationString = variables.location || "";
if (text.includes("{LOCATION}")) {
locationString = guessEventLocationType(locationString)?.label || "";
locationString = guessEventLocationType(locationString)?.label || locationString;
}
let dynamicText = text

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "App_RoutingForms_Form" ADD COLUMN "settings" JSONB;

View File

@ -518,6 +518,8 @@ model App_RoutingForms_Form {
userId Int
responses App_RoutingForms_FormResponse[]
disabled Boolean @default(false)
/// @zod.custom(imports.RoutingFormSettings)
settings Json?
}
model App_RoutingForms_FormResponse {

View File

@ -32,6 +32,11 @@ export const EventTypeMetaDataSchema = z
giphyThankYouPage: z.string().optional(),
apps: z.object(appDataSchemas).partial().optional(),
additionalNotesRequired: z.boolean().optional(),
config: z
.object({
useHostSchedulesForTeamEvent: z.boolean().optional(),
})
.optional(),
})
.nullable();
@ -208,6 +213,12 @@ export const successRedirectUrl = z
])
.optional();
export const RoutingFormSettings = z
.object({
emailOwnerOnSubmission: z.boolean(),
})
.nullable();
export type ZodDenullish<T extends ZodTypeAny> = T extends ZodNullable<infer U> | ZodOptional<infer U>
? ZodDenullish<U>
: T;

View File

@ -11,6 +11,7 @@ import logger from "@calcom/lib/logger";
import { performance } from "@calcom/lib/server/perfObserver";
import getTimeSlots from "@calcom/lib/slots";
import prisma, { availabilityUserSelect } from "@calcom/prisma";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { EventBusyDate } from "@calcom/types/Calendar";
import { TimeRange } from "@calcom/types/schedule";
@ -104,7 +105,7 @@ export const slotsRouter = createRouter().query("getSchedule", {
});
async function getEventType(ctx: { prisma: typeof prisma }, input: z.infer<typeof getScheduleSchema>) {
return ctx.prisma.eventType.findUnique({
const eventType = await ctx.prisma.eventType.findUnique({
where: {
id: input.eventTypeId,
},
@ -124,6 +125,7 @@ async function getEventType(ctx: { prisma: typeof prisma }, input: z.infer<typeo
periodEndDate: true,
periodCountCalendarDays: true,
periodDays: true,
metadata: true,
schedule: {
select: {
availability: true,
@ -144,6 +146,14 @@ async function getEventType(ctx: { prisma: typeof prisma }, input: z.infer<typeo
},
},
});
if (!eventType) {
return eventType;
}
return {
...eventType,
metadata: EventTypeMetaDataSchema.parse(eventType.metadata),
};
}
async function getDynamicEventType(ctx: { prisma: typeof prisma }, input: z.infer<typeof getScheduleSchema>) {

View File

@ -3,14 +3,17 @@ import { CalendsoSessionUser } from "next-auth";
import prisma from "@calcom/prisma";
import type { ssrInit } from "@server/lib/ssr";
export type AppUser = CalendsoSessionUser | undefined;
export type AppPrisma = typeof prisma;
export type AppGetServerSidePropsContext = GetServerSidePropsContext<{
appPages: string[];
}>;
export type AppSsrInit = ssrInit;
export type AppGetServerSideProps = (
context: AppGetServerSidePropsContext,
prisma: AppPrisma,
user: AppUser
user: AppUser,
ssrInit: AppSsrInit
) => GetServerSidePropsResult;

View File

@ -44,8 +44,8 @@ function SettingsToggle({
</div>
</div>
{children && (
<div className="mt-4 lg:ml-14" ref={animateRef}>
{checked && children}
<div className="lg:ml-14" ref={animateRef}>
{checked && <div className="mt-4">{children}</div>}
</div>
)}
</fieldset>

View File

@ -228,6 +228,8 @@
"$SAML_ADMINS",
"$SAML_DATABASE_URL",
"$SEND_FEEDBACK_EMAIL",
"$SENTRY_DSN",
"$NEXT_PUBLIC_SENTRY_DSN",
"$SLACK_CLIENT_ID",
"$SLACK_CLIENT_SECRET",
"$SLACK_SIGNING_SECRET",

177
yarn.lock
View File

@ -4806,7 +4806,20 @@
resolved "https://registry.yarnpkg.com/@resvg/resvg-wasm/-/resvg-wasm-2.0.0-alpha.4.tgz#fc2f86186a9641df030d8f9f3f9d995899cd1ecb"
integrity sha512-pWIG9a/x1ky8gXKRhPH1OPKpHFoMN1ISLbJ+O+gPXQHIAKhNd5I28RlWf7q576hAOQA9JZTlo3p/M2uyLzJmmw==
"@rollup/pluginutils@^4.2.1":
"@rollup/plugin-sucrase@4.0.4":
version "4.0.4"
resolved "https://registry.yarnpkg.com/@rollup/plugin-sucrase/-/plugin-sucrase-4.0.4.tgz#0a3b3d97cdc239ec3399f5a10711f751e9f95d98"
integrity sha512-YH4J8yoJb5EVnLhAwWxYAQNh2SJOR+SdZ6XdgoKEv6Kxm33riYkM8MlMaggN87UoISP52qAFyZ5ey56wu6umGg==
dependencies:
"@rollup/pluginutils" "^4.1.1"
sucrase "^3.20.0"
"@rollup/plugin-virtual@3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@rollup/plugin-virtual/-/plugin-virtual-3.0.0.tgz#8c3f54b4ab4b267d9cd3dcbaedc58d4fd1deddca"
integrity sha512-K9KORe1myM62o0lKkNR4MmCxjwuAXsZEtIHpaILfv4kILXTOrXt/R2ha7PzMcCHPYdnkWPiBZK8ed4Zr3Ll5lQ==
"@rollup/pluginutils@^4.1.1", "@rollup/pluginutils@^4.2.1":
version "4.2.1"
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d"
integrity sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==
@ -4852,6 +4865,16 @@
"@sentry/utils" "6.19.7"
tslib "^1.9.3"
"@sentry/browser@7.17.4":
version "7.17.4"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.17.4.tgz#2e4ad09180905929b823d01e2a029f10933c0a03"
integrity sha512-cNLQ/6ea0KOZyLKXGLTdgfqWYdg43+T/uF9D/hmc6kp/5qXm2dR3FcFRZX6OicaENM3dXvSoBIF6bSWmcszRtQ==
dependencies:
"@sentry/core" "7.17.4"
"@sentry/types" "7.17.4"
"@sentry/utils" "7.17.4"
tslib "^1.9.3"
"@sentry/cli@^1.73.0":
version "1.74.5"
resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-1.74.5.tgz#4a5c622913087c9ab6f82994da9a7526423779b8"
@ -4865,6 +4888,19 @@
proxy-from-env "^1.1.0"
which "^2.0.2"
"@sentry/cli@^1.74.6":
version "1.74.6"
resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-1.74.6.tgz#c4f276e52c6f5e8c8d692845a965988068ebc6f5"
integrity sha512-pJ7JJgozyjKZSTjOGi86chIngZMLUlYt2HOog+OJn+WGvqEkVymu8m462j1DiXAnex9NspB4zLLNuZ/R6rTQHg==
dependencies:
https-proxy-agent "^5.0.0"
mkdirp "^0.5.5"
node-fetch "^2.6.7"
npmlog "^4.1.2"
progress "^2.0.3"
proxy-from-env "^1.1.0"
which "^2.0.2"
"@sentry/core@6.19.7":
version "6.19.7"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.19.7.tgz#156aaa56dd7fad8c89c145be6ad7a4f7209f9785"
@ -4876,6 +4912,15 @@
"@sentry/utils" "6.19.7"
tslib "^1.9.3"
"@sentry/core@7.17.4":
version "7.17.4"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.17.4.tgz#62686e1b2baf4e7ba3d9ca57d91d2dfc643b8250"
integrity sha512-U3ABSJBKGK8dJ01nEG2+qNOb6Wv7U3VqoajiZxfV4lpPWNFGCoEhiTytxBlFTOCmdUH8209zSZiWJZaDLy+TSA==
dependencies:
"@sentry/types" "7.17.4"
"@sentry/utils" "7.17.4"
tslib "^1.9.3"
"@sentry/hub@6.19.7":
version "6.19.7"
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.19.7.tgz#58ad7776bbd31e9596a8ec46365b45cd8b9cfd11"
@ -4895,6 +4940,16 @@
localforage "^1.8.1"
tslib "^1.9.3"
"@sentry/integrations@7.17.4":
version "7.17.4"
resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.17.4.tgz#facdc4abb1d4714175c7d66bab634a898cc7eead"
integrity sha512-9vL0RRLuMnj0mj61THgRhDUCgTGYbRoCT6HmLGrErHjyTG3d7lUgUwTBQPgw9qYLss1uLKoEtRvcxgtW7ef0BA==
dependencies:
"@sentry/types" "7.17.4"
"@sentry/utils" "7.17.4"
localforage "^1.8.1"
tslib "^1.9.3"
"@sentry/minimal@6.19.7":
version "6.19.7"
resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.19.7.tgz#b3ee46d6abef9ef3dd4837ebcb6bdfd01b9aa7b4"
@ -4919,6 +4974,25 @@
"@sentry/webpack-plugin" "1.18.8"
tslib "^1.9.3"
"@sentry/nextjs@^7.17.3":
version "7.17.4"
resolved "https://registry.yarnpkg.com/@sentry/nextjs/-/nextjs-7.17.4.tgz#9c6d71d17e37da3fcbe753a374e93307a56dbf02"
integrity sha512-l4aHhkYjiC49yvvu/iG0CZGpAfmWdVQ4PlcnzqO9XY40So1LixY7idJEDefqx6LSZ1+VadpZT0o6iRwHfuOllQ==
dependencies:
"@rollup/plugin-sucrase" "4.0.4"
"@rollup/plugin-virtual" "3.0.0"
"@sentry/core" "7.17.4"
"@sentry/integrations" "7.17.4"
"@sentry/node" "7.17.4"
"@sentry/react" "7.17.4"
"@sentry/tracing" "7.17.4"
"@sentry/types" "7.17.4"
"@sentry/utils" "7.17.4"
"@sentry/webpack-plugin" "1.20.0"
chalk "3.0.0"
rollup "2.78.0"
tslib "^1.9.3"
"@sentry/node@6.19.7":
version "6.19.7"
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-6.19.7.tgz#32963b36b48daebbd559e6f13b1deb2415448592"
@ -4933,6 +5007,19 @@
lru_map "^0.3.3"
tslib "^1.9.3"
"@sentry/node@7.17.4":
version "7.17.4"
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.17.4.tgz#647ebcbd7228e5fa326dc3f95aceab34771205d3"
integrity sha512-cR+Gsir9c/tzFWxvk4zXkMQy6tNRHEYixHrb88XIjZVYDqDS9l2/bKs5nJusdmaUeLtmPp5Et2o7RJyS7gvKTQ==
dependencies:
"@sentry/core" "7.17.4"
"@sentry/types" "7.17.4"
"@sentry/utils" "7.17.4"
cookie "^0.4.1"
https-proxy-agent "^5.0.0"
lru_map "^0.3.3"
tslib "^1.9.3"
"@sentry/react@6.19.7":
version "6.19.7"
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-6.19.7.tgz#58cc2d6da20f7d3b0df40638dfbbbc86c9c85caf"
@ -4945,6 +5032,17 @@
hoist-non-react-statics "^3.3.2"
tslib "^1.9.3"
"@sentry/react@7.17.4":
version "7.17.4"
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.17.4.tgz#1d2c752841ace0561043be6a863de9fc7437f64c"
integrity sha512-Hw8lgeCgUthgVQ5OG24/iZWGNXnxodVfCmfngeIfqUWeFgQUae1V833GNkYZCiE5j2yjNVh3LL2bXA8PnvMCEg==
dependencies:
"@sentry/browser" "7.17.4"
"@sentry/types" "7.17.4"
"@sentry/utils" "7.17.4"
hoist-non-react-statics "^3.3.2"
tslib "^1.9.3"
"@sentry/tracing@6.19.7":
version "6.19.7"
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-6.19.7.tgz#54bb99ed5705931cd33caf71da347af769f02a4c"
@ -4956,11 +5054,26 @@
"@sentry/utils" "6.19.7"
tslib "^1.9.3"
"@sentry/tracing@7.17.4":
version "7.17.4"
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.17.4.tgz#1254f4eb7397e1134248de1dade33d2fec864005"
integrity sha512-9Fz6DI16ddnd970mlB5MiCNRSmSXp4SVZ1Yv3L22oS3kQeNxjBTE+htYNwJzSPrQp9aL/LqTYwlnrCy24u9XQA==
dependencies:
"@sentry/core" "7.17.4"
"@sentry/types" "7.17.4"
"@sentry/utils" "7.17.4"
tslib "^1.9.3"
"@sentry/types@6.19.7":
version "6.19.7"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.19.7.tgz#c6b337912e588083fc2896eb012526cf7cfec7c7"
integrity sha512-jH84pDYE+hHIbVnab3Hr+ZXr1v8QABfhx39KknxqKWr2l0oEItzepV0URvbEhB446lk/S/59230dlUUIBGsXbg==
"@sentry/types@7.17.4":
version "7.17.4"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.17.4.tgz#476522bc988989101e7aee9eee3c3f8f16fa59ea"
integrity sha512-QJj8vO4AtxuzQfJIzDnECSmoxwnS+WJsm1Ta2Cwdy+TUCBJyWpW7aIJJGta76zb9gNPGb3UcAbeEjhMJBJeRMQ==
"@sentry/utils@6.19.7":
version "6.19.7"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.19.7.tgz#6edd739f8185fd71afe49cbe351c1bbf5e7b7c79"
@ -4969,6 +5082,14 @@
"@sentry/types" "6.19.7"
tslib "^1.9.3"
"@sentry/utils@7.17.4":
version "7.17.4"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.17.4.tgz#b8f4fb6a329765b97668b34f6d397e8de2169fef"
integrity sha512-ioG0ANy8uiWzig82/e7cc+6C9UOxkyBzJDi1luoQVDH6P0/PvM8GzVU+1iUVUipf8+OL1Jh09GrWnd5wLm3XNQ==
dependencies:
"@sentry/types" "7.17.4"
tslib "^1.9.3"
"@sentry/webpack-plugin@1.18.8":
version "1.18.8"
resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-1.18.8.tgz#247a73a0aa9e28099a736bbe89ca0d35cbac7636"
@ -4976,6 +5097,14 @@
dependencies:
"@sentry/cli" "^1.73.0"
"@sentry/webpack-plugin@1.20.0":
version "1.20.0"
resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-1.20.0.tgz#e7add76122708fb6b4ee7951294b521019720e58"
integrity sha512-Ssj1mJVFsfU6vMCOM2d+h+KQR7QHSfeIP16t4l20Uq/neqWXZUQ2yvQfe4S3BjdbJXz/X4Rw8Hfy1Sd0ocunYw==
dependencies:
"@sentry/cli" "^1.74.6"
webpack-sources "^2.0.0 || ^3.0.0"
"@shuding/opentype.js@1.4.0-beta.0":
version "1.4.0-beta.0"
resolved "https://registry.yarnpkg.com/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz#5d1e7e9e056f546aad41df1c5043f8f85d39e24b"
@ -9665,6 +9794,14 @@ chalk@2.3.0:
escape-string-regexp "^1.0.5"
supports-color "^4.0.0"
chalk@3.0.0, chalk@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4"
integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chalk@4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad"
@ -9701,14 +9838,6 @@ chalk@^2.0.0, chalk@^2.4.1:
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
chalk@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4"
integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
change-case@^2.3.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/change-case/-/change-case-2.3.1.tgz#2c4fde3f063bb41d00cd68e0d5a09db61cbe894f"
@ -18289,7 +18418,7 @@ mysql2@2.3.3:
seq-queue "^0.0.5"
sqlstring "^2.3.2"
mz@^2.4.0:
mz@^2.4.0, mz@^2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32"
integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==
@ -21711,6 +21840,13 @@ rlp@^2.0.0, rlp@^2.2.3, rlp@^2.2.4:
dependencies:
bn.js "^5.2.0"
rollup@2.78.0:
version "2.78.0"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.78.0.tgz#00995deae70c0f712ea79ad904d5f6b033209d9e"
integrity sha512-4+YfbQC9QEVvKTanHhIAFVUFSRsezvQF8vFOJwtGfb9Bb+r014S+qryr9PSmw8x6sMnPkmFBGAvIFVQxvJxjtg==
optionalDependencies:
fsevents "~2.3.2"
"rollup@>=2.59.0 <2.78.0":
version "2.77.3"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.77.3.tgz#8f00418d3a2740036e15deb653bed1a90ee0cc12"
@ -23021,6 +23157,18 @@ stylis@4.0.13:
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.13.tgz#f5db332e376d13cc84ecfe5dace9a2a51d954c91"
integrity sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag==
sucrase@^3.20.0:
version "3.28.0"
resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.28.0.tgz#7fd8b3118d2155fcdf291088ab77fa6eefd63c4c"
integrity sha512-TK9600YInjuiIhVM3729rH4ZKPOsGeyXUwY+Ugu9eilNbdTFyHr6XcAGYbRVZPDgWj6tgI7bx95aaJjHnbffag==
dependencies:
commander "^4.0.0"
glob "7.1.6"
lines-and-columns "^1.1.6"
mz "^2.7.0"
pirates "^4.0.1"
ts-interface-checker "^0.1.9"
superagent@^5.1.1:
version "5.3.1"
resolved "https://registry.yarnpkg.com/superagent/-/superagent-5.3.1.tgz#d62f3234d76b8138c1320e90fa83dc1850ccabf1"
@ -23723,6 +23871,11 @@ ts-essentials@^7.0.3:
resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-7.0.3.tgz#686fd155a02133eedcc5362dc8b5056cde3e5a38"
integrity sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ==
ts-interface-checker@^0.1.9:
version "0.1.13"
resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699"
integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==
ts-jest@^28.0.8:
version "28.0.8"
resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-28.0.8.tgz#cd204b8e7a2f78da32cf6c95c9a6165c5b99cc73"
@ -25223,7 +25376,7 @@ webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3:
source-list-map "^2.0.0"
source-map "~0.6.1"
webpack-sources@^3.2.3:
"webpack-sources@^2.0.0 || ^3.0.0", webpack-sources@^3.2.3:
version "3.2.3"
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
@ -25845,7 +25998,7 @@ zod-prisma@^0.5.4:
parenthesis "^3.1.8"
ts-morph "^13.0.2"
zod@^3.17.3, zod@^3.19.1:
zod@^3.17.3, zod@^3.18.0, zod@^3.19.1:
version "3.19.1"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.19.1.tgz#112f074a97b50bfc4772d4ad1576814bd8ac4473"
integrity sha512-LYjZsEDhCdYET9ikFu6dVPGp2YH9DegXjdJToSzD9rO6fy4qiRYFoyEYwps88OseJlPyl2NOe2iJuhEhL7IpEA==