Allow choosing destination calendar per event type (#1514)
parent
7737164bbf
commit
8f6f34931b
|
@ -1 +1,2 @@
|
|||
node_modules
|
||||
prisma/zod
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import Select from "react-select";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import { trpc } from "@lib/trpc";
|
||||
|
||||
import Button from "@components/ui/Button";
|
||||
|
||||
interface Props {
|
||||
onChange: (value: { externalId: string; integration: string }) => void;
|
||||
isLoading?: boolean;
|
||||
hidePlaceholder?: boolean;
|
||||
/** The external Id of the connected calendar */
|
||||
value: string | undefined;
|
||||
}
|
||||
|
||||
const DestinationCalendarSelector = ({
|
||||
onChange,
|
||||
isLoading,
|
||||
value,
|
||||
hidePlaceholder,
|
||||
}: Props): JSX.Element | null => {
|
||||
const { t } = useLocale();
|
||||
const query = trpc.useQuery(["viewer.connectedCalendars"]);
|
||||
const [selectedOption, setSelectedOption] = useState<{ value: string; label: string } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedOption) {
|
||||
const selected = query.data?.connectedCalendars
|
||||
.map((connected) => connected.calendars ?? [])
|
||||
.flat()
|
||||
.find((cal) => cal.externalId === value);
|
||||
|
||||
if (selected) {
|
||||
setSelectedOption({
|
||||
value: `${selected.integration}:${selected.externalId}`,
|
||||
label: selected.name || "",
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [query.data?.connectedCalendars, selectedOption, value]);
|
||||
|
||||
if (!query.data?.connectedCalendars.length) {
|
||||
return null;
|
||||
}
|
||||
const options =
|
||||
query.data.connectedCalendars.map((selectedCalendar) => ({
|
||||
key: selectedCalendar.credentialId,
|
||||
label: `${selectedCalendar.integration.title} (${selectedCalendar.primary?.name})`,
|
||||
options: (selectedCalendar.calendars ?? []).map((cal) => ({
|
||||
label: cal.name || "",
|
||||
value: `${cal.integration}:${cal.externalId}`,
|
||||
})),
|
||||
})) ?? [];
|
||||
return (
|
||||
<div className="relative">
|
||||
{/* There's no easy way to customize the displayed value for a Select, so we fake it. */}
|
||||
{!hidePlaceholder && (
|
||||
<div className="absolute z-10 pointer-events-none">
|
||||
<Button size="sm" color="secondary" className="border-transparent m-[1px] rounded-sm">
|
||||
{t("select_destination_calendar")}: {selectedOption?.label || ""}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<Select
|
||||
name={"primarySelectedCalendar"}
|
||||
placeholder={!hidePlaceholder ? `${t("select_destination_calendar")}:` : undefined}
|
||||
options={options}
|
||||
isSearchable={false}
|
||||
className="flex-1 block w-full min-w-0 mt-1 mb-2 border-gray-300 rounded-none focus:ring-primary-500 focus:border-primary-500 rounded-r-md sm:text-sm"
|
||||
onChange={(option) => {
|
||||
setSelectedOption(option);
|
||||
if (!option) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Split only the first `:`, since Apple uses the full URL as externalId */
|
||||
const [integration, externalId] = option.value.split(/:(.+)/);
|
||||
|
||||
onChange({
|
||||
integration,
|
||||
externalId,
|
||||
});
|
||||
}}
|
||||
isLoading={isLoading}
|
||||
value={selectedOption}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DestinationCalendarSelector;
|
|
@ -1,19 +1,20 @@
|
|||
import { ChevronDownIcon, PlusIcon } from "@heroicons/react/solid";
|
||||
import { zodResolver } from "@hookform/resolvers/zod/dist/zod";
|
||||
import { SchedulingType } from "@prisma/client";
|
||||
import { useRouter } from "next/router";
|
||||
import { createEventTypeInput } from "prisma/zod/eventtypeCustom";
|
||||
import React, { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useMutation } from "react-query";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { HttpError } from "@lib/core/http/error";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import { useToggleQuery } from "@lib/hooks/useToggleQuery";
|
||||
import createEventType from "@lib/mutations/event-types/create-event-type";
|
||||
import showToast from "@lib/notification";
|
||||
import { CreateEventType } from "@lib/types/event-type";
|
||||
import { trpc } from "@lib/trpc";
|
||||
|
||||
import { Dialog, DialogClose, DialogContent } from "@components/Dialog";
|
||||
import { TextField, InputLeading, TextAreaField, Form } from "@components/form/fields";
|
||||
import { Form, InputLeading, TextAreaField, TextField } from "@components/form/fields";
|
||||
import Avatar from "@components/ui/Avatar";
|
||||
import { Button } from "@components/ui/Button";
|
||||
import Dropdown, {
|
||||
|
@ -47,8 +48,14 @@ export default function CreateEventTypeButton(props: Props) {
|
|||
const router = useRouter();
|
||||
const modalOpen = useToggleQuery("new");
|
||||
|
||||
const form = useForm<CreateEventType>({
|
||||
defaultValues: { length: 15 },
|
||||
// URL encoded params
|
||||
const teamId: number | undefined = Number(router.query.teamId) || undefined;
|
||||
const pageSlug = router.query.eventPage || props.options[0].slug;
|
||||
const hasTeams = !!props.options.find((option) => option.teamId);
|
||||
|
||||
const form = useForm<z.infer<typeof createEventTypeInput>>({
|
||||
resolver: zodResolver(createEventTypeInput),
|
||||
defaultValues: { length: 15, teamId },
|
||||
});
|
||||
const { setValue, watch, register } = form;
|
||||
|
||||
|
@ -62,20 +69,16 @@ export default function CreateEventTypeButton(props: Props) {
|
|||
return () => subscription.unsubscribe();
|
||||
}, [watch, setValue]);
|
||||
|
||||
// URL encoded params
|
||||
const teamId: number | null = Number(router.query.teamId) || null;
|
||||
const pageSlug = router.query.eventPage || props.options[0].slug;
|
||||
|
||||
const hasTeams = !!props.options.find((option) => option.teamId);
|
||||
|
||||
const createMutation = useMutation(createEventType, {
|
||||
const createMutation = trpc.useMutation("viewer.eventTypes.create", {
|
||||
onSuccess: async ({ eventType }) => {
|
||||
await router.push("/event-types/" + eventType.id);
|
||||
showToast(t("event_type_created_successfully", { eventTypeTitle: eventType.title }), "success");
|
||||
},
|
||||
onError: (err: HttpError) => {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
onError: (err) => {
|
||||
if (err instanceof HttpError) {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -83,19 +86,19 @@ export default function CreateEventTypeButton(props: Props) {
|
|||
const openModal = (option: EventTypeParent) => {
|
||||
// setTimeout fixes a bug where the url query params are removed immediately after opening the modal
|
||||
setTimeout(() => {
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: {
|
||||
...router.query,
|
||||
new: "1",
|
||||
eventPage: option.slug,
|
||||
...(option.teamId
|
||||
? {
|
||||
teamId: option.teamId,
|
||||
}
|
||||
: {}),
|
||||
router.push(
|
||||
{
|
||||
pathname: router.pathname,
|
||||
query: {
|
||||
...router.query,
|
||||
new: "1",
|
||||
eventPage: option.slug,
|
||||
teamId: option.teamId || undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
undefined,
|
||||
{ shallow: true }
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -103,7 +106,7 @@ export default function CreateEventTypeButton(props: Props) {
|
|||
const closeModal = () => {
|
||||
router.replace({
|
||||
pathname: router.pathname,
|
||||
query: { id: router.query.id },
|
||||
query: { id: router.query.id || undefined },
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -160,20 +163,10 @@ export default function CreateEventTypeButton(props: Props) {
|
|||
<Form
|
||||
form={form}
|
||||
handleSubmit={(values) => {
|
||||
const payload: CreateEventType = {
|
||||
title: values.title,
|
||||
slug: values.slug,
|
||||
description: values.description,
|
||||
length: values.length,
|
||||
};
|
||||
if (router.query.teamId) {
|
||||
payload.teamId = parseInt(`${router.query.teamId}`, 10);
|
||||
payload.schedulingType = values.schedulingType as SchedulingType;
|
||||
}
|
||||
|
||||
createMutation.mutate(payload);
|
||||
createMutation.mutate(values);
|
||||
}}>
|
||||
<div className="mt-3 space-y-4">
|
||||
{teamId && <input type="hidden" {...register("teamId", { valueAsNumber: true })} />}
|
||||
<TextField label={t("title")} placeholder={t("quick_chat")} {...register("title")} />
|
||||
|
||||
<TextField
|
||||
|
@ -201,7 +194,7 @@ export default function CreateEventTypeButton(props: Props) {
|
|||
defaultValue={15}
|
||||
label={t("length")}
|
||||
className="pr-20"
|
||||
{...register("length")}
|
||||
{...register("length", { valueAsNumber: true })}
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center pt-4 mt-1.5 pr-3 text-sm text-gray-400">
|
||||
{t("minutes")}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import React, { Fragment, useState } from "react";
|
||||
import React, { Fragment } from "react";
|
||||
import { useMutation } from "react-query";
|
||||
import Select from "react-select";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import showToast from "@lib/notification";
|
||||
import { trpc } from "@lib/trpc";
|
||||
|
||||
import DestinationCalendarSelector from "@components/DestinationCalendarSelector";
|
||||
import { List } from "@components/List";
|
||||
import { ShellSubHeading } from "@components/Shell";
|
||||
import { Alert } from "@components/ui/Alert";
|
||||
|
@ -161,76 +161,6 @@ function ConnectedCalendarsList(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
function PrimaryCalendarSelector() {
|
||||
const { t } = useLocale();
|
||||
const query = trpc.useQuery(["viewer.connectedCalendars"], {
|
||||
suspense: true,
|
||||
});
|
||||
const [selectedOption, setSelectedOption] = useState(() => {
|
||||
const selected = query.data?.connectedCalendars
|
||||
.map((connected) => connected.calendars ?? [])
|
||||
.flat()
|
||||
.find((cal) => cal.externalId === query.data.destinationCalendar?.externalId);
|
||||
|
||||
if (!selected) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
value: `${selected.integration}:${selected.externalId}`,
|
||||
label: selected.name,
|
||||
};
|
||||
});
|
||||
|
||||
const mutation = trpc.useMutation("viewer.setUserDestinationCalendar");
|
||||
|
||||
if (!query.data?.connectedCalendars.length) {
|
||||
return null;
|
||||
}
|
||||
const options =
|
||||
query.data.connectedCalendars.map((selectedCalendar) => ({
|
||||
key: selectedCalendar.credentialId,
|
||||
label: `${selectedCalendar.integration.title} (${selectedCalendar.primary?.name})`,
|
||||
options: (selectedCalendar.calendars ?? []).map((cal) => ({
|
||||
label: cal.name || "",
|
||||
value: `${cal.integration}:${cal.externalId}`,
|
||||
})),
|
||||
})) ?? [];
|
||||
return (
|
||||
<div className="relative">
|
||||
{/* There's no easy way to customize the displayed value for a Select, so we fake it. */}
|
||||
<div className="absolute z-10 pointer-events-none">
|
||||
<Button size="sm" color="secondary" className="border-transparent m-[1px] rounded-sm">
|
||||
{t("select_destination_calendar")}: {selectedOption?.label || ""}
|
||||
</Button>
|
||||
</div>
|
||||
<Select
|
||||
name={"primarySelectedCalendar"}
|
||||
placeholder={`${t("select_destination_calendar")}:`}
|
||||
options={options}
|
||||
isSearchable={false}
|
||||
className="flex-1 block w-full min-w-0 mt-1 mb-2 border-gray-300 rounded-none focus:ring-primary-500 focus:border-primary-500 rounded-r-md sm:text-sm"
|
||||
onChange={(option) => {
|
||||
setSelectedOption(option);
|
||||
if (!option) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Split only the first `:`, since Apple uses the full URL as externalId */
|
||||
const [integration, externalId] = option.value.split(/:(.+)/);
|
||||
|
||||
mutation.mutate({
|
||||
integration,
|
||||
externalId,
|
||||
});
|
||||
}}
|
||||
isLoading={mutation.isLoading}
|
||||
value={selectedOption}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CalendarList(props: Props) {
|
||||
const { t } = useLocale();
|
||||
const query = trpc.useQuery(["viewer.integrations"]);
|
||||
|
@ -272,6 +202,8 @@ export function CalendarListContainer(props: { heading?: false }) {
|
|||
utils.invalidateQueries(["viewer.connectedCalendars"]),
|
||||
]);
|
||||
const query = trpc.useQuery(["viewer.connectedCalendars"]);
|
||||
const mutation = trpc.useMutation("viewer.setDestinationCalendar");
|
||||
|
||||
return (
|
||||
<>
|
||||
{heading && (
|
||||
|
@ -286,7 +218,11 @@ export function CalendarListContainer(props: { heading?: false }) {
|
|||
subtitle={t("configure_how_your_event_types_interact")}
|
||||
actions={
|
||||
<div className="block max-w-full sm:min-w-80">
|
||||
<PrimaryCalendarSelector />
|
||||
<DestinationCalendarSelector
|
||||
onChange={mutation.mutate}
|
||||
isLoading={mutation.isLoading}
|
||||
value={query.data?.destinationCalendar?.externalId}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -7,7 +7,6 @@ import React, { useEffect, useState } from "react";
|
|||
import TimezoneSelect, { ITimezoneOption } from "react-timezone-select";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import { WorkingHours } from "@lib/types/schedule";
|
||||
|
||||
import Button from "@components/ui/Button";
|
||||
|
||||
|
@ -17,11 +16,16 @@ import SetTimesModal from "./modal/SetTimesModal";
|
|||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
type AvailabilityInput = Pick<Availability, "days" | "startTime" | "endTime">;
|
||||
|
||||
type Props = {
|
||||
timeZone: string;
|
||||
availability: Availability[];
|
||||
setTimeZone: (timeZone: string) => void;
|
||||
setAvailability: (schedule: { openingHours: WorkingHours[]; dateOverrides: WorkingHours[] }) => void;
|
||||
setAvailability: (schedule: {
|
||||
openingHours: AvailabilityInput[];
|
||||
dateOverrides: AvailabilityInput[];
|
||||
}) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -6,7 +6,6 @@ import React, { SyntheticEvent, useEffect, useState } from "react";
|
|||
|
||||
import { PaymentData } from "@ee/lib/stripe/server";
|
||||
|
||||
import useDarkMode from "@lib/core/browser/useDarkMode";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
import Button from "@components/ui/Button";
|
||||
|
@ -18,8 +17,8 @@ const CARD_OPTIONS: stripejs.StripeCardElementOptions = {
|
|||
},
|
||||
style: {
|
||||
base: {
|
||||
color: "#000",
|
||||
iconColor: "#000",
|
||||
color: "#666",
|
||||
iconColor: "#666",
|
||||
fontFamily: "ui-sans-serif, system-ui",
|
||||
fontSmoothing: "antialiased",
|
||||
fontSize: "16px",
|
||||
|
@ -53,18 +52,10 @@ export default function PaymentComponent(props: Props) {
|
|||
const stripe = useStripe();
|
||||
const elements = useElements();
|
||||
|
||||
const { isDarkMode } = useDarkMode();
|
||||
|
||||
useEffect(() => {
|
||||
elements?.update({ locale: i18n.language as StripeElementLocale });
|
||||
}, [elements, i18n.language]);
|
||||
|
||||
if (isDarkMode) {
|
||||
CARD_OPTIONS.style!.base!.color = "#fff";
|
||||
CARD_OPTIONS.style!.base!.iconColor = "#fff";
|
||||
CARD_OPTIONS.style!.base!["::placeholder"]!.color = "#fff";
|
||||
}
|
||||
|
||||
const handleChange = async (event: StripeCardElementChangeEvent) => {
|
||||
// Listen for changes in the CardElement
|
||||
// and display any errors as the customer types their card details
|
||||
|
|
|
@ -50,6 +50,7 @@ async function handlePaymentSuccess(event: Stripe.Event) {
|
|||
id: true,
|
||||
uid: true,
|
||||
paid: true,
|
||||
destinationCalendar: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
|
@ -87,6 +88,7 @@ async function handlePaymentSuccess(event: Stripe.Event) {
|
|||
organizer: { email: user.email!, name: user.name!, timeZone: user.timeZone },
|
||||
attendees: booking.attendees,
|
||||
uid: booking.uid,
|
||||
destinationCalendar: booking.destinationCalendar || user.destinationCalendar,
|
||||
language: t,
|
||||
};
|
||||
|
||||
|
|
|
@ -257,6 +257,10 @@ export default class EventManager {
|
|||
return Promise.all(destinationCalendarCredentials.map(async (c) => await createEvent(c, event)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Not ideal but, if we don't find a destination calendar,
|
||||
* fallback to the first connected calendar
|
||||
*/
|
||||
const [credential] = this.calendarCredentials;
|
||||
if (!credential) {
|
||||
return [];
|
||||
|
|
|
@ -26,6 +26,7 @@ export type NewCalendarEventType = {
|
|||
export type CalendarEventType = {
|
||||
uid: string;
|
||||
etag: string;
|
||||
/** This is the actual caldav event url, not the location url. */
|
||||
url: string;
|
||||
summary: string;
|
||||
description: string;
|
||||
|
|
|
@ -64,7 +64,7 @@ type EventBusyDate = Record<"start" | "end", Date | string>;
|
|||
export interface Calendar {
|
||||
createEvent(event: CalendarEvent): Promise<NewCalendarEventType>;
|
||||
|
||||
updateEvent(uid: string, event: CalendarEvent): Promise<any>;
|
||||
updateEvent(uid: string, event: CalendarEvent): Promise<unknown>;
|
||||
|
||||
deleteEvent(uid: string, event: CalendarEvent): Promise<unknown>;
|
||||
|
||||
|
|
|
@ -116,10 +116,11 @@ export default abstract class BaseCalendarService implements Calendar {
|
|||
}
|
||||
}
|
||||
|
||||
async updateEvent(uid: string, event: CalendarEvent): Promise<any> {
|
||||
async updateEvent(uid: string, event: CalendarEvent): Promise<unknown> {
|
||||
try {
|
||||
const events = await this.getEventsByUID(uid);
|
||||
|
||||
/** We generate the ICS files */
|
||||
const { error, value: iCalString } = createEvent({
|
||||
uid,
|
||||
startInputType: "utc",
|
||||
|
@ -138,15 +139,15 @@ export default abstract class BaseCalendarService implements Calendar {
|
|||
return {};
|
||||
}
|
||||
|
||||
const eventsToUpdate = events.filter((event) => event.uid === uid);
|
||||
const eventsToUpdate = events.filter((e) => e.uid === uid);
|
||||
|
||||
return Promise.all(
|
||||
eventsToUpdate.map((event) => {
|
||||
eventsToUpdate.map((e) => {
|
||||
return updateCalendarObject({
|
||||
calendarObject: {
|
||||
url: event.url,
|
||||
url: e.url,
|
||||
data: iCalString,
|
||||
etag: event?.etag,
|
||||
etag: e?.etag,
|
||||
},
|
||||
headers: this.headers,
|
||||
});
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import * as fetch from "@lib/core/http/fetch-wrapper";
|
||||
import { CreateEventType, CreateEventTypeResponse } from "@lib/types/event-type";
|
||||
|
||||
/**
|
||||
* @deprecated Use `trpc.useMutation("viewer.eventTypes.create")` instead.
|
||||
*/
|
||||
const createEventType = async (data: CreateEventType) => {
|
||||
const response = await fetch.post<CreateEventType, CreateEventTypeResponse>(
|
||||
"/api/availability/eventtype",
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import * as fetch from "@lib/core/http/fetch-wrapper";
|
||||
|
||||
/**
|
||||
* @deprecated Use `trpc.useMutation("viewer.eventTypes.delete")` instead.
|
||||
*/
|
||||
const deleteEventType = async (data: { id: number }) => {
|
||||
const response = await fetch.remove<{ id: number }, Record<string, never>>(
|
||||
"/api/availability/eventtype",
|
||||
|
|
|
@ -7,6 +7,9 @@ type EventTypeResponse = {
|
|||
eventType: EventType;
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated Use `trpc.useMutation("viewer.eventTypes.update")` instead.
|
||||
*/
|
||||
const updateEventType = async (data: EventTypeInput) => {
|
||||
const response = await fetch.patch<EventTypeInput, EventTypeResponse>("/api/availability/eventtype", data);
|
||||
return response;
|
||||
|
|
|
@ -20,6 +20,12 @@ export type AdvancedOptions = {
|
|||
availability?: { openingHours: WorkingHours[]; dateOverrides: WorkingHours[] };
|
||||
customInputs?: EventTypeCustomInput[];
|
||||
timeZone?: string;
|
||||
destinationCalendar?: {
|
||||
userId?: number;
|
||||
eventTypeId?: number;
|
||||
integration: string;
|
||||
externalId: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type EventTypeCustomInput = {
|
||||
|
@ -49,6 +55,7 @@ export type EventTypeInput = AdvancedOptions & {
|
|||
slug: string;
|
||||
description: string;
|
||||
length: number;
|
||||
teamId?: number;
|
||||
hidden: boolean;
|
||||
locations: unknown;
|
||||
availability?: { openingHours: WorkingHours[]; dateOverrides: WorkingHours[] };
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
"analyze:browser": "BUNDLE_ANALYZE=browser next build",
|
||||
"dev": "next dev",
|
||||
"db-up": "docker-compose up -d",
|
||||
"db-migrate": "yarn prisma migrate dev",
|
||||
"db-migrate": "yarn prisma migrate dev && yarn format-schemas",
|
||||
"db-deploy": "yarn prisma migrate deploy",
|
||||
"db-seed": "yarn prisma db seed",
|
||||
"db-nuke": "docker-compose down --volumes --remove-orphans",
|
||||
|
@ -22,7 +22,9 @@
|
|||
"type-check": "tsc --pretty --noEmit",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"postinstall": "prisma generate",
|
||||
"format-schemas": "prettier --write ./prisma",
|
||||
"generate-schemas": "prisma generate && yarn format-schemas",
|
||||
"postinstall": "yarn generate-schemas",
|
||||
"pre-commit": "lint-staged",
|
||||
"lint": "eslint . --ext .ts,.js,.tsx,.jsx",
|
||||
"prepare": "husky install",
|
||||
|
@ -145,7 +147,8 @@
|
|||
"tailwindcss": "^3.0.0",
|
||||
"ts-jest": "^26.0.0",
|
||||
"ts-node": "^10.2.1",
|
||||
"typescript": "^4.5.2"
|
||||
"typescript": "^4.5.2",
|
||||
"zod-prisma": "^0.5.2"
|
||||
},
|
||||
"lint-staged": {
|
||||
"./{*,{ee,pages,components,lib}/**/*}.{js,ts,jsx,tsx}": [
|
||||
|
|
|
@ -1,227 +1,32 @@
|
|||
import { EventTypeCustomInput, MembershipRole, Prisma, PeriodType } from "@prisma/client";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
import prisma from "@lib/prisma";
|
||||
import { WorkingHours } from "@lib/types/schedule";
|
||||
|
||||
function isPeriodType(keyInput: string): keyInput is PeriodType {
|
||||
return Object.keys(PeriodType).includes(keyInput);
|
||||
}
|
||||
|
||||
function handlePeriodType(periodType: string): PeriodType | undefined {
|
||||
if (typeof periodType !== "string") return undefined;
|
||||
const passedPeriodType = periodType.toUpperCase();
|
||||
if (!isPeriodType(passedPeriodType)) return undefined;
|
||||
return PeriodType[passedPeriodType];
|
||||
}
|
||||
|
||||
function handleCustomInputs(customInputs: EventTypeCustomInput[], eventTypeId: number) {
|
||||
if (!customInputs || !customInputs?.length) return undefined;
|
||||
const cInputsIdsToDelete = customInputs.filter((input) => input.id > 0).map((e) => e.id);
|
||||
const cInputsToCreate = customInputs
|
||||
.filter((input) => input.id < 0)
|
||||
.map((input) => ({
|
||||
type: input.type,
|
||||
label: input.label,
|
||||
required: input.required,
|
||||
placeholder: input.placeholder,
|
||||
}));
|
||||
const cInputsToUpdate = customInputs
|
||||
.filter((input) => input.id > 0)
|
||||
.map((input) => ({
|
||||
data: {
|
||||
type: input.type,
|
||||
label: input.label,
|
||||
required: input.required,
|
||||
placeholder: input.placeholder,
|
||||
},
|
||||
where: {
|
||||
id: input.id,
|
||||
},
|
||||
}));
|
||||
|
||||
return {
|
||||
deleteMany: {
|
||||
eventTypeId,
|
||||
NOT: {
|
||||
id: { in: cInputsIdsToDelete },
|
||||
},
|
||||
},
|
||||
createMany: {
|
||||
data: cInputsToCreate,
|
||||
},
|
||||
update: cInputsToUpdate,
|
||||
};
|
||||
}
|
||||
import { createContext } from "@server/createContext";
|
||||
import { viewerRouter } from "@server/routers/viewer";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = await getSession({ req });
|
||||
/** So we can reuse tRCP queries */
|
||||
const trpcCtx = await createContext({ req, res });
|
||||
|
||||
if (!session) {
|
||||
if (!session?.user?.id) {
|
||||
res.status(401).json({ message: "Not authenticated" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!session.user?.id) {
|
||||
console.error("Session is missing a user id");
|
||||
return res.status(500).json({ message: "Something went wrong" });
|
||||
if (req.method === "POST") {
|
||||
const eventType = await viewerRouter.createCaller(trpcCtx).mutation("eventTypes.create", req.body);
|
||||
res.status(201).json({ eventType });
|
||||
}
|
||||
|
||||
if (req.method !== "POST") {
|
||||
const event = await prisma.eventType.findUnique({
|
||||
where: { id: req.body.id },
|
||||
include: {
|
||||
users: true,
|
||||
team: {
|
||||
select: {
|
||||
members: {
|
||||
select: {
|
||||
userId: true,
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!event) {
|
||||
return res.status(404).json({ message: "No event exists matching that id." });
|
||||
}
|
||||
|
||||
const isAuthorized = (function () {
|
||||
if (event.team) {
|
||||
return event.team.members
|
||||
.filter((member) => member.role === MembershipRole.OWNER || member.role === MembershipRole.ADMIN)
|
||||
.map((member) => member.userId)
|
||||
.includes(session.user.id);
|
||||
}
|
||||
return (
|
||||
event.userId === session.user.id ||
|
||||
event.users.find((user) => {
|
||||
return user.id === session.user?.id;
|
||||
})
|
||||
);
|
||||
})();
|
||||
|
||||
if (!isAuthorized) {
|
||||
console.warn(`User ${session.user.id} attempted to an access an event ${event.id} they do not own.`);
|
||||
return res.status(403).json({ message: "No event exists matching that id." });
|
||||
}
|
||||
if (req.method === "PATCH") {
|
||||
const eventType = await viewerRouter.createCaller(trpcCtx).mutation("eventTypes.update", req.body);
|
||||
res.status(201).json({ eventType });
|
||||
}
|
||||
|
||||
if (req.method === "PATCH" || req.method === "POST") {
|
||||
const data: Prisma.EventTypeCreateInput | Prisma.EventTypeUpdateInput = {
|
||||
title: req.body.title,
|
||||
slug: req.body.slug.trim(),
|
||||
description: req.body.description,
|
||||
length: parseInt(req.body.length),
|
||||
hidden: req.body.hidden,
|
||||
requiresConfirmation: req.body.requiresConfirmation,
|
||||
disableGuests: req.body.disableGuests,
|
||||
locations: req.body.locations,
|
||||
eventName: req.body.eventName,
|
||||
customInputs: handleCustomInputs(req.body.customInputs as EventTypeCustomInput[], req.body.id),
|
||||
periodType: handlePeriodType(req.body.periodType),
|
||||
periodDays: req.body.periodDays,
|
||||
periodStartDate: req.body.periodStartDate,
|
||||
periodEndDate: req.body.periodEndDate,
|
||||
periodCountCalendarDays: req.body.periodCountCalendarDays,
|
||||
minimumBookingNotice:
|
||||
req.body.minimumBookingNotice || req.body.minimumBookingNotice === 0
|
||||
? parseInt(req.body.minimumBookingNotice, 10)
|
||||
: undefined,
|
||||
slotInterval: req.body.slotInterval,
|
||||
price: req.body.price,
|
||||
currency: req.body.currency,
|
||||
};
|
||||
|
||||
if (req.body.schedulingType) {
|
||||
data.schedulingType = req.body.schedulingType;
|
||||
}
|
||||
|
||||
if (req.method == "POST") {
|
||||
if (req.body.teamId) {
|
||||
data.team = {
|
||||
connect: {
|
||||
id: req.body.teamId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const eventType = await prisma.eventType.create({
|
||||
data: {
|
||||
...(data as Prisma.EventTypeCreateInput),
|
||||
users: {
|
||||
connect: {
|
||||
id: session?.user?.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
res.status(201).json({ eventType });
|
||||
} else if (req.method == "PATCH") {
|
||||
if (req.body.users) {
|
||||
data.users = {
|
||||
set: [],
|
||||
connect: req.body.users.map((id: string) => ({ id: parseInt(id) })),
|
||||
};
|
||||
}
|
||||
|
||||
if (req.body.timeZone) {
|
||||
data.timeZone = req.body.timeZone;
|
||||
}
|
||||
|
||||
if (req.body.availability) {
|
||||
const openingHours: WorkingHours[] = req.body.availability.openingHours || [];
|
||||
// const overrides = req.body.availability.dateOverrides || [];
|
||||
|
||||
const eventTypeId = +req.body.id;
|
||||
if (eventTypeId) {
|
||||
await prisma.availability.deleteMany({
|
||||
where: {
|
||||
eventTypeId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const availabilityToCreate = openingHours.map((openingHour) => ({
|
||||
startTime: new Date(openingHour.startTime),
|
||||
endTime: new Date(openingHour.endTime),
|
||||
days: openingHour.days,
|
||||
}));
|
||||
|
||||
data.availability = {
|
||||
createMany: {
|
||||
data: availabilityToCreate,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const eventType = await prisma.eventType.update({
|
||||
where: {
|
||||
id: req.body.id,
|
||||
},
|
||||
data,
|
||||
});
|
||||
res.status(200).json({ eventType });
|
||||
}
|
||||
}
|
||||
|
||||
if (req.method == "DELETE") {
|
||||
await prisma.eventTypeCustomInput.deleteMany({
|
||||
where: {
|
||||
eventTypeId: req.body.id,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.eventType.delete({
|
||||
where: {
|
||||
id: req.body.id,
|
||||
},
|
||||
});
|
||||
|
||||
res.status(200).json({});
|
||||
if (req.method === "DELETE") {
|
||||
await viewerRouter.createCaller(trpcCtx).mutation("eventTypes.delete", { id: req.body.id });
|
||||
res.status(200).json({ id: req.body.id, message: "Event Type deleted" });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -96,6 +96,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
id: true,
|
||||
uid: true,
|
||||
payment: true,
|
||||
destinationCalendar: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -126,6 +127,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
location: booking.location ?? "",
|
||||
uid: booking.uid,
|
||||
language: t,
|
||||
destinationCalendar: booking?.destinationCalendar || currentUser.destinationCalendar,
|
||||
};
|
||||
|
||||
if (reqBody.confirmed) {
|
||||
|
|
|
@ -224,6 +224,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
userId: true,
|
||||
price: true,
|
||||
currency: true,
|
||||
destinationCalendar: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -306,7 +307,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
location: reqBody.location, // Will be processed by the EventManager later.
|
||||
language: t,
|
||||
/** For team events, we will need to handle each member destinationCalendar eventually */
|
||||
destinationCalendar: users[0].destinationCalendar,
|
||||
destinationCalendar: eventType.destinationCalendar || users[0].destinationCalendar,
|
||||
};
|
||||
|
||||
if (eventType.schedulingType === SchedulingType.COLLECTIVE) {
|
||||
|
@ -350,6 +351,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
id: users[0].id,
|
||||
},
|
||||
},
|
||||
destinationCalendar: evt.destinationCalendar
|
||||
? {
|
||||
connect: { id: evt.destinationCalendar.id },
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -64,6 +64,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
endTime: true,
|
||||
uid: true,
|
||||
eventTypeId: true,
|
||||
destinationCalendar: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -111,7 +112,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
uid: bookingToDelete?.uid,
|
||||
location: bookingToDelete?.location,
|
||||
language: t,
|
||||
destinationCalendar: bookingToDelete?.user.destinationCalendar,
|
||||
destinationCalendar: bookingToDelete?.destinationCalendar || bookingToDelete?.user.destinationCalendar,
|
||||
};
|
||||
|
||||
// Hook up the webhook logic here
|
||||
|
@ -171,6 +172,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
location: bookingToDelete.location ?? "",
|
||||
uid: bookingToDelete.uid ?? "",
|
||||
language: t,
|
||||
destinationCalendar: bookingToDelete?.destinationCalendar || bookingToDelete?.user.destinationCalendar,
|
||||
};
|
||||
await refund(bookingToDelete, evt);
|
||||
await prisma.booking.update({
|
||||
|
|
|
@ -44,10 +44,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
username: true,
|
||||
locale: true,
|
||||
timeZone: true,
|
||||
destinationCalendar: true,
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
uid: true,
|
||||
destinationCalendar: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -88,6 +90,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
attendees: booking.attendees,
|
||||
uid: booking.uid,
|
||||
language: t,
|
||||
destinationCalendar: booking.destinationCalendar || user.destinationCalendar,
|
||||
};
|
||||
|
||||
await sendOrganizerRequestReminderEmail(evt);
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { PhoneIcon, XIcon } from "@heroicons/react/outline";
|
||||
import {
|
||||
ChevronRightIcon,
|
||||
ClockIcon,
|
||||
DocumentIcon,
|
||||
ExternalLinkIcon,
|
||||
ClockIcon,
|
||||
LinkIcon,
|
||||
LocationMarkerIcon,
|
||||
PencilIcon,
|
||||
|
@ -12,7 +12,7 @@ import {
|
|||
UserAddIcon,
|
||||
UsersIcon,
|
||||
} from "@heroicons/react/solid";
|
||||
import { EventTypeCustomInput, Prisma, SchedulingType } from "@prisma/client";
|
||||
import { Availability, EventTypeCustomInput, PeriodType, Prisma, SchedulingType } from "@prisma/client";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
|
||||
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||
import dayjs from "dayjs";
|
||||
|
@ -21,28 +21,25 @@ import utc from "dayjs/plugin/utc";
|
|||
import { GetServerSidePropsContext } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||
import { useMutation } from "react-query";
|
||||
import Select from "react-select";
|
||||
|
||||
import { StripeData } from "@ee/lib/stripe/server";
|
||||
|
||||
import { asNumberOrUndefined, asStringOrThrow, asStringOrUndefined } from "@lib/asStringOrNull";
|
||||
import { asStringOrThrow, asStringOrUndefined } from "@lib/asStringOrNull";
|
||||
import { getSession } from "@lib/auth";
|
||||
import { HttpError } from "@lib/core/http/error";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import getIntegrations, { hasIntegration } from "@lib/integrations/getIntegrations";
|
||||
import { LocationType } from "@lib/location";
|
||||
import deleteEventType from "@lib/mutations/event-types/delete-event-type";
|
||||
import updateEventType from "@lib/mutations/event-types/update-event-type";
|
||||
import showToast from "@lib/notification";
|
||||
import prisma from "@lib/prisma";
|
||||
import { defaultAvatarSrc } from "@lib/profile";
|
||||
import { AdvancedOptions, EventTypeInput } from "@lib/types/event-type";
|
||||
import { trpc } from "@lib/trpc";
|
||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
import { WorkingHours } from "@lib/types/schedule";
|
||||
|
||||
import DestinationCalendarSelector from "@components/DestinationCalendarSelector";
|
||||
import { Dialog, DialogContent, DialogTrigger } from "@components/Dialog";
|
||||
import Shell from "@components/Shell";
|
||||
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
|
||||
|
@ -60,6 +57,8 @@ import * as RadioArea from "@components/ui/form/radio-area";
|
|||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
type AvailabilityInput = Pick<Availability, "days" | "startTime" | "endTime">;
|
||||
|
||||
type OptionTypeBase = {
|
||||
label: string;
|
||||
value: LocationType;
|
||||
|
@ -109,27 +108,32 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
|
||||
const router = useRouter();
|
||||
|
||||
const updateMutation = useMutation(updateEventType, {
|
||||
const updateMutation = trpc.useMutation("viewer.eventTypes.update", {
|
||||
onSuccess: async ({ eventType }) => {
|
||||
await router.push("/event-types");
|
||||
showToast(t("event_type_updated_successfully", { eventTypeTitle: eventType.title }), "success");
|
||||
},
|
||||
onError: (err: HttpError) => {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
onError: (err) => {
|
||||
if (err instanceof HttpError) {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation(deleteEventType, {
|
||||
const deleteMutation = trpc.useMutation("viewer.eventTypes.delete", {
|
||||
onSuccess: async () => {
|
||||
await router.push("/event-types");
|
||||
showToast(t("event_type_deleted_successfully"), "success");
|
||||
},
|
||||
onError: (err: HttpError) => {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
onError: (err) => {
|
||||
if (err instanceof HttpError) {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
const connectedCalendarsQuery = trpc.useQuery(["viewer.connectedCalendars"]);
|
||||
|
||||
const [editIcon, setEditIcon] = useState(true);
|
||||
const [showLocationModal, setShowLocationModal] = useState(false);
|
||||
|
@ -207,8 +211,9 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
return <p className="text-sm">{t("cal_provide_zoom_meeting_url")}</p>;
|
||||
case LocationType.Daily:
|
||||
return <p className="text-sm">{t("cal_provide_video_meeting_url")}</p>;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const removeCustom = (index: number) => {
|
||||
|
@ -255,7 +260,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
|
||||
const formMethods = useForm<{
|
||||
title: string;
|
||||
eventTitle: string;
|
||||
eventName: string;
|
||||
slug: string;
|
||||
length: number;
|
||||
description: string;
|
||||
|
@ -263,20 +268,22 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
requiresConfirmation: boolean;
|
||||
schedulingType: SchedulingType | null;
|
||||
price: number;
|
||||
isHidden: boolean;
|
||||
hidden: boolean;
|
||||
locations: { type: LocationType; address?: string }[];
|
||||
customInputs: EventTypeCustomInput[];
|
||||
users: string[];
|
||||
scheduler: {
|
||||
enteredAvailability: { openingHours: WorkingHours[]; dateOverrides: WorkingHours[] };
|
||||
selectedTimezone: string;
|
||||
};
|
||||
periodType: string | number;
|
||||
availability: { openingHours: AvailabilityInput[]; dateOverrides: AvailabilityInput[] };
|
||||
timeZone: string;
|
||||
periodType: PeriodType;
|
||||
periodDays: number;
|
||||
periodDaysType: string;
|
||||
periodCountCalendarDays: "1" | "0";
|
||||
periodDates: { startDate: Date; endDate: Date };
|
||||
minimumBookingNotice: number;
|
||||
slotInterval: number | null;
|
||||
destinationCalendar: {
|
||||
integration: string;
|
||||
externalId: string;
|
||||
};
|
||||
}>({
|
||||
defaultValues: {
|
||||
locations: eventType.locations || [],
|
||||
|
@ -504,46 +511,14 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
<Form
|
||||
form={formMethods}
|
||||
handleSubmit={async (values) => {
|
||||
const enteredTitle: string = values.title;
|
||||
|
||||
const advancedPayload: AdvancedOptions = {};
|
||||
if (advancedSettingsVisible) {
|
||||
advancedPayload.eventName = values.eventTitle;
|
||||
advancedPayload.periodType = asStringOrUndefined(values.periodType);
|
||||
advancedPayload.periodDays = asNumberOrUndefined(values.periodDays);
|
||||
advancedPayload.periodCountCalendarDays = Boolean(parseInt(values.periodDaysType));
|
||||
advancedPayload.periodStartDate = values.periodDates.startDate || undefined;
|
||||
advancedPayload.periodEndDate = values.periodDates.endDate || undefined;
|
||||
advancedPayload.minimumBookingNotice = values.minimumBookingNotice;
|
||||
advancedPayload.slotInterval = values.slotInterval;
|
||||
advancedPayload.price = requirePayment
|
||||
? Math.round(parseFloat(asStringOrThrow(values.price)) * 100)
|
||||
: 0;
|
||||
advancedPayload.currency = currency;
|
||||
advancedPayload.availability = values.scheduler.enteredAvailability || undefined;
|
||||
advancedPayload.customInputs = values.customInputs;
|
||||
advancedPayload.timeZone = values.scheduler.selectedTimezone;
|
||||
advancedPayload.disableGuests = values.disableGuests;
|
||||
advancedPayload.requiresConfirmation = values.requiresConfirmation;
|
||||
}
|
||||
|
||||
const payload: EventTypeInput = {
|
||||
const { periodDates, periodCountCalendarDays, ...input } = values;
|
||||
updateMutation.mutate({
|
||||
...input,
|
||||
periodStartDate: periodDates.startDate,
|
||||
periodEndDate: periodDates.endDate,
|
||||
periodCountCalendarDays: periodCountCalendarDays === "1",
|
||||
id: eventType.id,
|
||||
title: enteredTitle,
|
||||
slug: asStringOrThrow(values.slug),
|
||||
description: asStringOrThrow(values.description),
|
||||
length: values.length,
|
||||
hidden: values.isHidden,
|
||||
locations: values.locations,
|
||||
...advancedPayload,
|
||||
...(team
|
||||
? {
|
||||
schedulingType: values.schedulingType as SchedulingType,
|
||||
users: values.users,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
updateMutation.mutate(payload);
|
||||
});
|
||||
}}
|
||||
className="space-y-6">
|
||||
<div className="space-y-3">
|
||||
|
@ -704,6 +679,36 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
</span>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="space-y-6">
|
||||
{/**
|
||||
* Only display calendar selector if user has connected calendars AND if it's not
|
||||
* a team event. Since we don't have logic to handle each attende calendar (for now).
|
||||
* This will fallback to each user selected destination calendar.
|
||||
*/}
|
||||
{!!connectedCalendarsQuery.data?.connectedCalendars.length && !team && (
|
||||
<div className="items-center block sm:flex">
|
||||
<div className="mb-4 min-w-48 sm:mb-0">
|
||||
<label htmlFor="eventName" className="flex text-sm font-medium text-neutral-700">
|
||||
Create events on:
|
||||
</label>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<div className="relative mt-1 rounded-sm shadow-sm">
|
||||
<Controller
|
||||
control={formMethods.control}
|
||||
name="destinationCalendar"
|
||||
defaultValue={eventType.destinationCalendar || undefined}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<DestinationCalendarSelector
|
||||
value={value ? value.externalId : undefined}
|
||||
onChange={onChange}
|
||||
hidePlaceholder
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="items-center block sm:flex">
|
||||
<div className="mb-4 min-w-48 sm:mb-0">
|
||||
<label htmlFor="eventName" className="flex text-sm font-medium text-neutral-700">
|
||||
|
@ -717,7 +722,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
className="block w-full border-gray-300 rounded-sm shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
|
||||
placeholder={t("meeting_with_user")}
|
||||
defaultValue={eventType.eventName || ""}
|
||||
{...formMethods.register("eventTitle")}
|
||||
{...formMethods.register("eventName")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -914,7 +919,9 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
render={() => (
|
||||
<RadioGroup.Root
|
||||
defaultValue={periodType?.type}
|
||||
onValueChange={(val) => formMethods.setValue("periodType", val)}>
|
||||
onValueChange={(val) =>
|
||||
formMethods.setValue("periodType", val as PeriodType)
|
||||
}>
|
||||
{PERIOD_TYPES.map((period) => (
|
||||
<div className="flex items-center mb-2 text-sm" key={period.type}>
|
||||
<RadioGroup.Item
|
||||
|
@ -927,19 +934,16 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
{period.type === "ROLLING" && (
|
||||
<div className="inline-flex">
|
||||
<input
|
||||
type="text"
|
||||
className="block w-12 mr-2 border-gray-300 rounded-sm shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
|
||||
type="number"
|
||||
className="block w-12 mr-2 border-gray-300 rounded-sm shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm [appearance:textfield]"
|
||||
placeholder="30"
|
||||
{...formMethods.register("periodDays")}
|
||||
{...formMethods.register("periodDays", { valueAsNumber: true })}
|
||||
defaultValue={eventType.periodDays || 30}
|
||||
onChange={(e) => {
|
||||
formMethods.setValue("periodDays", Number(e.target.value));
|
||||
}}
|
||||
/>
|
||||
<select
|
||||
id=""
|
||||
className="block w-full py-2 pl-3 pr-10 text-base border-gray-300 rounded-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
|
||||
{...formMethods.register("periodDaysType")}
|
||||
{...formMethods.register("periodCountCalendarDays")}
|
||||
defaultValue={eventType.periodCountCalendarDays ? "1" : "0"}>
|
||||
<option value="1">{t("calendar_days")}</option>
|
||||
<option value="0">{t("business_days")}</option>
|
||||
|
@ -985,21 +989,18 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
</div>
|
||||
<div className="w-full">
|
||||
<Controller
|
||||
name="scheduler"
|
||||
name="availability"
|
||||
control={formMethods.control}
|
||||
render={() => (
|
||||
<Scheduler
|
||||
setAvailability={(val: {
|
||||
openingHours: WorkingHours[];
|
||||
dateOverrides: WorkingHours[];
|
||||
}) => {
|
||||
formMethods.setValue("scheduler.enteredAvailability", {
|
||||
setAvailability={(val) => {
|
||||
formMethods.setValue("availability", {
|
||||
openingHours: val.openingHours,
|
||||
dateOverrides: val.dateOverrides,
|
||||
});
|
||||
}}
|
||||
setTimeZone={(timeZone) => {
|
||||
formMethods.setValue("scheduler.selectedTimezone", timeZone);
|
||||
formMethods.setValue("timeZone", timeZone);
|
||||
setSelectedTimeZone(timeZone);
|
||||
}}
|
||||
timeZone={selectedTimeZone}
|
||||
|
@ -1033,7 +1034,12 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
<div className="relative flex items-start">
|
||||
<div className="flex items-center h-5">
|
||||
<input
|
||||
onChange={(event) => setRequirePayment(event.target.checked)}
|
||||
onChange={(event) => {
|
||||
setRequirePayment(event.target.checked);
|
||||
if (!event.target.checked) {
|
||||
formMethods.setValue("price", 0);
|
||||
}
|
||||
}}
|
||||
id="requirePayment"
|
||||
name="requirePayment"
|
||||
type="checkbox"
|
||||
|
@ -1063,16 +1069,25 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
<div className="items-center block sm:flex">
|
||||
<div className="w-full">
|
||||
<div className="relative mt-1 rounded-sm shadow-sm">
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
required
|
||||
className="block w-full pl-2 pr-12 border-gray-300 rounded-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
|
||||
placeholder="Price"
|
||||
defaultValue={
|
||||
eventType.price > 0 ? eventType.price / 100.0 : undefined
|
||||
}
|
||||
{...formMethods.register("price")}
|
||||
<Controller
|
||||
defaultValue={eventType.price}
|
||||
control={formMethods.control}
|
||||
name="price"
|
||||
render={({ field }) => (
|
||||
<input
|
||||
{...field}
|
||||
step="0.01"
|
||||
min="0.5"
|
||||
type="number"
|
||||
required
|
||||
className="block w-full pl-2 pr-12 border-gray-300 rounded-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
|
||||
placeholder="Price"
|
||||
onChange={(e) => {
|
||||
field.onChange(e.target.valueAsNumber * 100);
|
||||
}}
|
||||
value={field.value > 0 ? field.value / 100 : 0}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
|
||||
<span className="text-gray-500 sm:text-sm" id="duration">
|
||||
|
@ -1103,7 +1118,9 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
<Button href="/event-types" color="secondary" tabIndex={-1}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button type="submit">{t("update")}</Button>
|
||||
<Button type="submit" disabled={updateMutation.isLoading}>
|
||||
{t("update")}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
|
@ -1111,14 +1128,14 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
<div className="w-full px-2 mt-8 ml-2 sm:w-3/12 sm:mt-0 min-w-[177px] ">
|
||||
<div className="px-2">
|
||||
<Controller
|
||||
name="isHidden"
|
||||
name="hidden"
|
||||
control={formMethods.control}
|
||||
defaultValue={eventType.hidden}
|
||||
render={({ field }) => (
|
||||
<Switch
|
||||
defaultChecked={field.value}
|
||||
onCheckedChange={(isChecked) => {
|
||||
formMethods.setValue("isHidden", isChecked);
|
||||
formMethods.setValue("hidden", isChecked);
|
||||
}}
|
||||
label={t("hide_event_type")}
|
||||
/>
|
||||
|
@ -1397,6 +1414,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
userId: true,
|
||||
price: true,
|
||||
currency: true,
|
||||
destinationCalendar: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -10,6 +10,13 @@ generator client {
|
|||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
generator zod {
|
||||
provider = "zod-prisma"
|
||||
output = "./zod"
|
||||
imports = "./zod-utils"
|
||||
relationModel = "default"
|
||||
}
|
||||
|
||||
enum SchedulingType {
|
||||
ROUND_ROBIN @map("roundRobin")
|
||||
COLLECTIVE @map("collective")
|
||||
|
@ -23,10 +30,13 @@ enum PeriodType {
|
|||
|
||||
model EventType {
|
||||
id Int @id @default(autoincrement())
|
||||
/// @zod.nonempty()
|
||||
title String
|
||||
/// @zod.custom(imports.eventTypeSlug)
|
||||
slug String
|
||||
description String?
|
||||
position Int @default(0)
|
||||
/// @zod.custom(imports.eventTypeLocations)
|
||||
locations Json?
|
||||
length Int
|
||||
hidden Boolean @default(false)
|
||||
|
@ -36,7 +46,7 @@ model EventType {
|
|||
teamId Int?
|
||||
bookings Booking[]
|
||||
availability Availability[]
|
||||
destinationCalendar DestinationCalendar[]
|
||||
destinationCalendar DestinationCalendar?
|
||||
eventName String?
|
||||
customInputs EventTypeCustomInput[]
|
||||
timeZone String?
|
||||
|
@ -93,6 +103,7 @@ model User {
|
|||
id Int @id @default(autoincrement())
|
||||
username String? @unique
|
||||
name String?
|
||||
/// @zod.email()
|
||||
email String @unique
|
||||
emailVerified DateTime?
|
||||
password String?
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { LocationType } from "@lib/location";
|
||||
|
||||
export const eventTypeLocations = z.array(
|
||||
z.object({ type: z.nativeEnum(LocationType), address: z.string().optional() })
|
||||
);
|
||||
|
||||
export const eventTypeSlug = z.string().transform((val) => val.trim());
|
||||
export const stringToDate = z.string().transform((a) => new Date(a));
|
||||
export const stringOrNumber = z.union([z.string().transform((v) => parseInt(v, 10)), z.number().int()]);
|
|
@ -0,0 +1,27 @@
|
|||
import * as z from "zod";
|
||||
|
||||
import * as imports from "../zod-utils";
|
||||
import { CompleteBooking, BookingModel } from "./index";
|
||||
|
||||
export const _AttendeeModel = z.object({
|
||||
id: z.number().int(),
|
||||
email: z.string(),
|
||||
name: z.string(),
|
||||
timeZone: z.string(),
|
||||
bookingId: z.number().int().nullish(),
|
||||
});
|
||||
|
||||
export interface CompleteAttendee extends z.infer<typeof _AttendeeModel> {
|
||||
booking?: CompleteBooking | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* AttendeeModel contains all relations on your model in addition to the scalars
|
||||
*
|
||||
* NOTE: Lazy required in case of potential circular dependencies within schema
|
||||
*/
|
||||
export const AttendeeModel: z.ZodSchema<CompleteAttendee> = z.lazy(() =>
|
||||
_AttendeeModel.extend({
|
||||
booking: BookingModel.nullish(),
|
||||
})
|
||||
);
|
|
@ -0,0 +1,32 @@
|
|||
import * as z from "zod";
|
||||
|
||||
import * as imports from "../zod-utils";
|
||||
import { CompleteUser, UserModel, CompleteEventType, EventTypeModel } from "./index";
|
||||
|
||||
export const _AvailabilityModel = z.object({
|
||||
id: z.number().int(),
|
||||
label: z.string().nullish(),
|
||||
userId: z.number().int().nullish(),
|
||||
eventTypeId: z.number().int().nullish(),
|
||||
days: z.number().int().array(),
|
||||
startTime: z.date(),
|
||||
endTime: z.date(),
|
||||
date: z.date().nullish(),
|
||||
});
|
||||
|
||||
export interface CompleteAvailability extends z.infer<typeof _AvailabilityModel> {
|
||||
user?: CompleteUser | null;
|
||||
eventType?: CompleteEventType | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* AvailabilityModel contains all relations on your model in addition to the scalars
|
||||
*
|
||||
* NOTE: Lazy required in case of potential circular dependencies within schema
|
||||
*/
|
||||
export const AvailabilityModel: z.ZodSchema<CompleteAvailability> = z.lazy(() =>
|
||||
_AvailabilityModel.extend({
|
||||
user: UserModel.nullish(),
|
||||
eventType: EventTypeModel.nullish(),
|
||||
})
|
||||
);
|
|
@ -0,0 +1,65 @@
|
|||
import * as z from "zod";
|
||||
|
||||
import { BookingStatus } from "../../node_modules/@prisma/client";
|
||||
import * as imports from "../zod-utils";
|
||||
import {
|
||||
CompleteUser,
|
||||
UserModel,
|
||||
CompleteBookingReference,
|
||||
BookingReferenceModel,
|
||||
CompleteEventType,
|
||||
EventTypeModel,
|
||||
CompleteAttendee,
|
||||
AttendeeModel,
|
||||
CompleteDailyEventReference,
|
||||
DailyEventReferenceModel,
|
||||
CompletePayment,
|
||||
PaymentModel,
|
||||
CompleteDestinationCalendar,
|
||||
DestinationCalendarModel,
|
||||
} from "./index";
|
||||
|
||||
export const _BookingModel = z.object({
|
||||
id: z.number().int(),
|
||||
uid: z.string(),
|
||||
userId: z.number().int().nullish(),
|
||||
eventTypeId: z.number().int().nullish(),
|
||||
title: z.string(),
|
||||
description: z.string().nullish(),
|
||||
startTime: z.date(),
|
||||
endTime: z.date(),
|
||||
location: z.string().nullish(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date().nullish(),
|
||||
confirmed: z.boolean(),
|
||||
rejected: z.boolean(),
|
||||
status: z.nativeEnum(BookingStatus),
|
||||
paid: z.boolean(),
|
||||
});
|
||||
|
||||
export interface CompleteBooking extends z.infer<typeof _BookingModel> {
|
||||
user?: CompleteUser | null;
|
||||
references: CompleteBookingReference[];
|
||||
eventType?: CompleteEventType | null;
|
||||
attendees: CompleteAttendee[];
|
||||
dailyRef?: CompleteDailyEventReference | null;
|
||||
payment: CompletePayment[];
|
||||
destinationCalendar?: CompleteDestinationCalendar | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* BookingModel contains all relations on your model in addition to the scalars
|
||||
*
|
||||
* NOTE: Lazy required in case of potential circular dependencies within schema
|
||||
*/
|
||||
export const BookingModel: z.ZodSchema<CompleteBooking> = z.lazy(() =>
|
||||
_BookingModel.extend({
|
||||
user: UserModel.nullish(),
|
||||
references: BookingReferenceModel.array(),
|
||||
eventType: EventTypeModel.nullish(),
|
||||
attendees: AttendeeModel.array(),
|
||||
dailyRef: DailyEventReferenceModel.nullish(),
|
||||
payment: PaymentModel.array(),
|
||||
destinationCalendar: DestinationCalendarModel.nullish(),
|
||||
})
|
||||
);
|
|
@ -0,0 +1,29 @@
|
|||
import * as z from "zod";
|
||||
|
||||
import * as imports from "../zod-utils";
|
||||
import { CompleteBooking, BookingModel } from "./index";
|
||||
|
||||
export const _BookingReferenceModel = z.object({
|
||||
id: z.number().int(),
|
||||
type: z.string(),
|
||||
uid: z.string(),
|
||||
meetingId: z.string().nullish(),
|
||||
meetingPassword: z.string().nullish(),
|
||||
meetingUrl: z.string().nullish(),
|
||||
bookingId: z.number().int().nullish(),
|
||||
});
|
||||
|
||||
export interface CompleteBookingReference extends z.infer<typeof _BookingReferenceModel> {
|
||||
booking?: CompleteBooking | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* BookingReferenceModel contains all relations on your model in addition to the scalars
|
||||
*
|
||||
* NOTE: Lazy required in case of potential circular dependencies within schema
|
||||
*/
|
||||
export const BookingReferenceModel: z.ZodSchema<CompleteBookingReference> = z.lazy(() =>
|
||||
_BookingReferenceModel.extend({
|
||||
booking: BookingModel.nullish(),
|
||||
})
|
||||
);
|
|
@ -0,0 +1,34 @@
|
|||
import * as z from "zod";
|
||||
|
||||
import * as imports from "../zod-utils";
|
||||
import { CompleteUser, UserModel } from "./index";
|
||||
|
||||
// Helper schema for JSON fields
|
||||
type Literal = boolean | number | string;
|
||||
type Json = Literal | { [key: string]: Json } | Json[];
|
||||
const literalSchema = z.union([z.string(), z.number(), z.boolean()]);
|
||||
const jsonSchema: z.ZodSchema<Json> = z.lazy(() =>
|
||||
z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)])
|
||||
);
|
||||
|
||||
export const _CredentialModel = z.object({
|
||||
id: z.number().int(),
|
||||
type: z.string(),
|
||||
key: jsonSchema,
|
||||
userId: z.number().int().nullish(),
|
||||
});
|
||||
|
||||
export interface CompleteCredential extends z.infer<typeof _CredentialModel> {
|
||||
user?: CompleteUser | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* CredentialModel contains all relations on your model in addition to the scalars
|
||||
*
|
||||
* NOTE: Lazy required in case of potential circular dependencies within schema
|
||||
*/
|
||||
export const CredentialModel: z.ZodSchema<CompleteCredential> = z.lazy(() =>
|
||||
_CredentialModel.extend({
|
||||
user: UserModel.nullish(),
|
||||
})
|
||||
);
|
|
@ -0,0 +1,26 @@
|
|||
import * as z from "zod";
|
||||
|
||||
import * as imports from "../zod-utils";
|
||||
import { CompleteBooking, BookingModel } from "./index";
|
||||
|
||||
export const _DailyEventReferenceModel = z.object({
|
||||
id: z.number().int(),
|
||||
dailyurl: z.string(),
|
||||
dailytoken: z.string(),
|
||||
bookingId: z.number().int().nullish(),
|
||||
});
|
||||
|
||||
export interface CompleteDailyEventReference extends z.infer<typeof _DailyEventReferenceModel> {
|
||||
booking?: CompleteBooking | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* DailyEventReferenceModel contains all relations on your model in addition to the scalars
|
||||
*
|
||||
* NOTE: Lazy required in case of potential circular dependencies within schema
|
||||
*/
|
||||
export const DailyEventReferenceModel: z.ZodSchema<CompleteDailyEventReference> = z.lazy(() =>
|
||||
_DailyEventReferenceModel.extend({
|
||||
booking: BookingModel.nullish(),
|
||||
})
|
||||
);
|
|
@ -0,0 +1,39 @@
|
|||
import * as z from "zod";
|
||||
|
||||
import * as imports from "../zod-utils";
|
||||
import {
|
||||
CompleteUser,
|
||||
UserModel,
|
||||
CompleteBooking,
|
||||
BookingModel,
|
||||
CompleteEventType,
|
||||
EventTypeModel,
|
||||
} from "./index";
|
||||
|
||||
export const _DestinationCalendarModel = z.object({
|
||||
id: z.number().int(),
|
||||
integration: z.string(),
|
||||
externalId: z.string(),
|
||||
userId: z.number().int().nullish(),
|
||||
bookingId: z.number().int().nullish(),
|
||||
eventTypeId: z.number().int().nullish(),
|
||||
});
|
||||
|
||||
export interface CompleteDestinationCalendar extends z.infer<typeof _DestinationCalendarModel> {
|
||||
user?: CompleteUser | null;
|
||||
booking?: CompleteBooking | null;
|
||||
eventType?: CompleteEventType | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* DestinationCalendarModel contains all relations on your model in addition to the scalars
|
||||
*
|
||||
* NOTE: Lazy required in case of potential circular dependencies within schema
|
||||
*/
|
||||
export const DestinationCalendarModel: z.ZodSchema<CompleteDestinationCalendar> = z.lazy(() =>
|
||||
_DestinationCalendarModel.extend({
|
||||
user: UserModel.nullish(),
|
||||
booking: BookingModel.nullish(),
|
||||
eventType: EventTypeModel.nullish(),
|
||||
})
|
||||
);
|
|
@ -0,0 +1,82 @@
|
|||
import * as z from "zod";
|
||||
|
||||
import { PeriodType, SchedulingType } from "../../node_modules/@prisma/client";
|
||||
import * as imports from "../zod-utils";
|
||||
import {
|
||||
CompleteUser,
|
||||
UserModel,
|
||||
CompleteTeam,
|
||||
TeamModel,
|
||||
CompleteBooking,
|
||||
BookingModel,
|
||||
CompleteAvailability,
|
||||
AvailabilityModel,
|
||||
CompleteDestinationCalendar,
|
||||
DestinationCalendarModel,
|
||||
CompleteEventTypeCustomInput,
|
||||
EventTypeCustomInputModel,
|
||||
CompleteSchedule,
|
||||
ScheduleModel,
|
||||
} from "./index";
|
||||
|
||||
// Helper schema for JSON fields
|
||||
type Literal = boolean | number | string;
|
||||
type Json = Literal | { [key: string]: Json } | Json[];
|
||||
const literalSchema = z.union([z.string(), z.number(), z.boolean()]);
|
||||
const jsonSchema: z.ZodSchema<Json> = z.lazy(() =>
|
||||
z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)])
|
||||
);
|
||||
|
||||
export const _EventTypeModel = z.object({
|
||||
id: z.number().int(),
|
||||
title: z.string().nonempty(),
|
||||
slug: imports.eventTypeSlug,
|
||||
description: z.string().nullish(),
|
||||
position: z.number().int(),
|
||||
locations: imports.eventTypeLocations,
|
||||
length: z.number().int(),
|
||||
hidden: z.boolean(),
|
||||
userId: z.number().int().nullish(),
|
||||
teamId: z.number().int().nullish(),
|
||||
eventName: z.string().nullish(),
|
||||
timeZone: z.string().nullish(),
|
||||
periodType: z.nativeEnum(PeriodType),
|
||||
periodStartDate: z.date().nullish(),
|
||||
periodEndDate: z.date().nullish(),
|
||||
periodDays: z.number().int().nullish(),
|
||||
periodCountCalendarDays: z.boolean().nullish(),
|
||||
requiresConfirmation: z.boolean(),
|
||||
disableGuests: z.boolean(),
|
||||
minimumBookingNotice: z.number().int(),
|
||||
schedulingType: z.nativeEnum(SchedulingType).nullish(),
|
||||
price: z.number().int(),
|
||||
currency: z.string(),
|
||||
slotInterval: z.number().int().nullish(),
|
||||
});
|
||||
|
||||
export interface CompleteEventType extends z.infer<typeof _EventTypeModel> {
|
||||
users: CompleteUser[];
|
||||
team?: CompleteTeam | null;
|
||||
bookings: CompleteBooking[];
|
||||
availability: CompleteAvailability[];
|
||||
destinationCalendar?: CompleteDestinationCalendar | null;
|
||||
customInputs: CompleteEventTypeCustomInput[];
|
||||
Schedule: CompleteSchedule[];
|
||||
}
|
||||
|
||||
/**
|
||||
* EventTypeModel contains all relations on your model in addition to the scalars
|
||||
*
|
||||
* NOTE: Lazy required in case of potential circular dependencies within schema
|
||||
*/
|
||||
export const EventTypeModel: z.ZodSchema<CompleteEventType> = z.lazy(() =>
|
||||
_EventTypeModel.extend({
|
||||
users: UserModel.array(),
|
||||
team: TeamModel.nullish(),
|
||||
bookings: BookingModel.array(),
|
||||
availability: AvailabilityModel.array(),
|
||||
destinationCalendar: DestinationCalendarModel.nullish(),
|
||||
customInputs: EventTypeCustomInputModel.array(),
|
||||
Schedule: ScheduleModel.array(),
|
||||
})
|
||||
);
|
|
@ -0,0 +1,17 @@
|
|||
import { _EventTypeModel } from "prisma/zod";
|
||||
|
||||
const createEventTypeBaseInput = _EventTypeModel
|
||||
.pick({
|
||||
title: true,
|
||||
slug: true,
|
||||
description: true,
|
||||
length: true,
|
||||
teamId: true,
|
||||
schedulingType: true,
|
||||
})
|
||||
.refine((data) => (data.teamId ? data.teamId && data.schedulingType : true), {
|
||||
path: ["schedulingType"],
|
||||
message: "You must select a scheduling type for team events",
|
||||
});
|
||||
|
||||
export const createEventTypeInput = createEventTypeBaseInput;
|
|
@ -0,0 +1,29 @@
|
|||
import * as z from "zod";
|
||||
|
||||
import { EventTypeCustomInputType } from "../../node_modules/@prisma/client";
|
||||
import * as imports from "../zod-utils";
|
||||
import { CompleteEventType, EventTypeModel } from "./index";
|
||||
|
||||
export const _EventTypeCustomInputModel = z.object({
|
||||
id: z.number().int(),
|
||||
eventTypeId: z.number().int(),
|
||||
label: z.string(),
|
||||
type: z.nativeEnum(EventTypeCustomInputType),
|
||||
required: z.boolean(),
|
||||
placeholder: z.string(),
|
||||
});
|
||||
|
||||
export interface CompleteEventTypeCustomInput extends z.infer<typeof _EventTypeCustomInputModel> {
|
||||
eventType: CompleteEventType;
|
||||
}
|
||||
|
||||
/**
|
||||
* EventTypeCustomInputModel contains all relations on your model in addition to the scalars
|
||||
*
|
||||
* NOTE: Lazy required in case of potential circular dependencies within schema
|
||||
*/
|
||||
export const EventTypeCustomInputModel: z.ZodSchema<CompleteEventTypeCustomInput> = z.lazy(() =>
|
||||
_EventTypeCustomInputModel.extend({
|
||||
eventType: EventTypeModel,
|
||||
})
|
||||
);
|
|
@ -0,0 +1,19 @@
|
|||
export * from "./eventtype";
|
||||
export * from "./credential";
|
||||
export * from "./destinationcalendar";
|
||||
export * from "./user";
|
||||
export * from "./team";
|
||||
export * from "./membership";
|
||||
export * from "./verificationrequest";
|
||||
export * from "./bookingreference";
|
||||
export * from "./attendee";
|
||||
export * from "./dailyeventreference";
|
||||
export * from "./booking";
|
||||
export * from "./schedule";
|
||||
export * from "./availability";
|
||||
export * from "./selectedcalendar";
|
||||
export * from "./eventtypecustominput";
|
||||
export * from "./resetpasswordrequest";
|
||||
export * from "./remindermail";
|
||||
export * from "./payment";
|
||||
export * from "./webhook";
|
|
@ -0,0 +1,29 @@
|
|||
import * as z from "zod";
|
||||
|
||||
import { MembershipRole } from "../../node_modules/@prisma/client";
|
||||
import * as imports from "../zod-utils";
|
||||
import { CompleteTeam, TeamModel, CompleteUser, UserModel } from "./index";
|
||||
|
||||
export const _MembershipModel = z.object({
|
||||
teamId: z.number().int(),
|
||||
userId: z.number().int(),
|
||||
accepted: z.boolean(),
|
||||
role: z.nativeEnum(MembershipRole),
|
||||
});
|
||||
|
||||
export interface CompleteMembership extends z.infer<typeof _MembershipModel> {
|
||||
team: CompleteTeam;
|
||||
user: CompleteUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* MembershipModel contains all relations on your model in addition to the scalars
|
||||
*
|
||||
* NOTE: Lazy required in case of potential circular dependencies within schema
|
||||
*/
|
||||
export const MembershipModel: z.ZodSchema<CompleteMembership> = z.lazy(() =>
|
||||
_MembershipModel.extend({
|
||||
team: TeamModel,
|
||||
user: UserModel,
|
||||
})
|
||||
);
|
|
@ -0,0 +1,42 @@
|
|||
import * as z from "zod";
|
||||
|
||||
import { PaymentType } from "../../node_modules/@prisma/client";
|
||||
import * as imports from "../zod-utils";
|
||||
import { CompleteBooking, BookingModel } from "./index";
|
||||
|
||||
// Helper schema for JSON fields
|
||||
type Literal = boolean | number | string;
|
||||
type Json = Literal | { [key: string]: Json } | Json[];
|
||||
const literalSchema = z.union([z.string(), z.number(), z.boolean()]);
|
||||
const jsonSchema: z.ZodSchema<Json> = z.lazy(() =>
|
||||
z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)])
|
||||
);
|
||||
|
||||
export const _PaymentModel = z.object({
|
||||
id: z.number().int(),
|
||||
uid: z.string(),
|
||||
type: z.nativeEnum(PaymentType),
|
||||
bookingId: z.number().int(),
|
||||
amount: z.number().int(),
|
||||
fee: z.number().int(),
|
||||
currency: z.string(),
|
||||
success: z.boolean(),
|
||||
refunded: z.boolean(),
|
||||
data: jsonSchema,
|
||||
externalId: z.string(),
|
||||
});
|
||||
|
||||
export interface CompletePayment extends z.infer<typeof _PaymentModel> {
|
||||
booking?: CompleteBooking | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* PaymentModel contains all relations on your model in addition to the scalars
|
||||
*
|
||||
* NOTE: Lazy required in case of potential circular dependencies within schema
|
||||
*/
|
||||
export const PaymentModel: z.ZodSchema<CompletePayment> = z.lazy(() =>
|
||||
_PaymentModel.extend({
|
||||
booking: BookingModel.nullish(),
|
||||
})
|
||||
);
|
|
@ -0,0 +1,12 @@
|
|||
import * as z from "zod";
|
||||
|
||||
import { ReminderType } from "../../node_modules/@prisma/client";
|
||||
import * as imports from "../zod-utils";
|
||||
|
||||
export const _ReminderMailModel = z.object({
|
||||
id: z.number().int(),
|
||||
referenceId: z.number().int(),
|
||||
reminderType: z.nativeEnum(ReminderType),
|
||||
elapsedMinutes: z.number().int(),
|
||||
createdAt: z.date(),
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
import * as z from "zod";
|
||||
|
||||
import * as imports from "../zod-utils";
|
||||
|
||||
export const _ResetPasswordRequestModel = z.object({
|
||||
id: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
email: z.string(),
|
||||
expires: z.date(),
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
import * as z from "zod";
|
||||
|
||||
import * as imports from "../zod-utils";
|
||||
import { CompleteUser, UserModel, CompleteEventType, EventTypeModel } from "./index";
|
||||
|
||||
// Helper schema for JSON fields
|
||||
type Literal = boolean | number | string;
|
||||
type Json = Literal | { [key: string]: Json } | Json[];
|
||||
const literalSchema = z.union([z.string(), z.number(), z.boolean()]);
|
||||
const jsonSchema: z.ZodSchema<Json> = z.lazy(() =>
|
||||
z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)])
|
||||
);
|
||||
|
||||
export const _ScheduleModel = z.object({
|
||||
id: z.number().int(),
|
||||
userId: z.number().int().nullish(),
|
||||
eventTypeId: z.number().int().nullish(),
|
||||
title: z.string().nullish(),
|
||||
freeBusyTimes: jsonSchema,
|
||||
});
|
||||
|
||||
export interface CompleteSchedule extends z.infer<typeof _ScheduleModel> {
|
||||
user?: CompleteUser | null;
|
||||
eventType?: CompleteEventType | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* ScheduleModel contains all relations on your model in addition to the scalars
|
||||
*
|
||||
* NOTE: Lazy required in case of potential circular dependencies within schema
|
||||
*/
|
||||
export const ScheduleModel: z.ZodSchema<CompleteSchedule> = z.lazy(() =>
|
||||
_ScheduleModel.extend({
|
||||
user: UserModel.nullish(),
|
||||
eventType: EventTypeModel.nullish(),
|
||||
})
|
||||
);
|
|
@ -0,0 +1,25 @@
|
|||
import * as z from "zod";
|
||||
|
||||
import * as imports from "../zod-utils";
|
||||
import { CompleteUser, UserModel } from "./index";
|
||||
|
||||
export const _SelectedCalendarModel = z.object({
|
||||
userId: z.number().int(),
|
||||
integration: z.string(),
|
||||
externalId: z.string(),
|
||||
});
|
||||
|
||||
export interface CompleteSelectedCalendar extends z.infer<typeof _SelectedCalendarModel> {
|
||||
user: CompleteUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* SelectedCalendarModel contains all relations on your model in addition to the scalars
|
||||
*
|
||||
* NOTE: Lazy required in case of potential circular dependencies within schema
|
||||
*/
|
||||
export const SelectedCalendarModel: z.ZodSchema<CompleteSelectedCalendar> = z.lazy(() =>
|
||||
_SelectedCalendarModel.extend({
|
||||
user: UserModel,
|
||||
})
|
||||
);
|
|
@ -0,0 +1,30 @@
|
|||
import * as z from "zod";
|
||||
|
||||
import * as imports from "../zod-utils";
|
||||
import { CompleteMembership, MembershipModel, CompleteEventType, EventTypeModel } from "./index";
|
||||
|
||||
export const _TeamModel = z.object({
|
||||
id: z.number().int(),
|
||||
name: z.string().nullish(),
|
||||
slug: z.string().nullish(),
|
||||
logo: z.string().nullish(),
|
||||
bio: z.string().nullish(),
|
||||
hideBranding: z.boolean(),
|
||||
});
|
||||
|
||||
export interface CompleteTeam extends z.infer<typeof _TeamModel> {
|
||||
members: CompleteMembership[];
|
||||
eventTypes: CompleteEventType[];
|
||||
}
|
||||
|
||||
/**
|
||||
* TeamModel contains all relations on your model in addition to the scalars
|
||||
*
|
||||
* NOTE: Lazy required in case of potential circular dependencies within schema
|
||||
*/
|
||||
export const TeamModel: z.ZodSchema<CompleteTeam> = z.lazy(() =>
|
||||
_TeamModel.extend({
|
||||
members: MembershipModel.array(),
|
||||
eventTypes: EventTypeModel.array(),
|
||||
})
|
||||
);
|
|
@ -0,0 +1,93 @@
|
|||
import * as z from "zod";
|
||||
|
||||
import { IdentityProvider, UserPlan } from "../../node_modules/@prisma/client";
|
||||
import * as imports from "../zod-utils";
|
||||
import {
|
||||
CompleteEventType,
|
||||
EventTypeModel,
|
||||
CompleteCredential,
|
||||
CredentialModel,
|
||||
CompleteMembership,
|
||||
MembershipModel,
|
||||
CompleteBooking,
|
||||
BookingModel,
|
||||
CompleteAvailability,
|
||||
AvailabilityModel,
|
||||
CompleteSelectedCalendar,
|
||||
SelectedCalendarModel,
|
||||
CompleteSchedule,
|
||||
ScheduleModel,
|
||||
CompleteWebhook,
|
||||
WebhookModel,
|
||||
CompleteDestinationCalendar,
|
||||
DestinationCalendarModel,
|
||||
} from "./index";
|
||||
|
||||
// Helper schema for JSON fields
|
||||
type Literal = boolean | number | string;
|
||||
type Json = Literal | { [key: string]: Json } | Json[];
|
||||
const literalSchema = z.union([z.string(), z.number(), z.boolean()]);
|
||||
const jsonSchema: z.ZodSchema<Json> = z.lazy(() =>
|
||||
z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)])
|
||||
);
|
||||
|
||||
export const _UserModel = z.object({
|
||||
id: z.number().int(),
|
||||
username: z.string().nullish(),
|
||||
name: z.string().nullish(),
|
||||
email: z.string().email(),
|
||||
emailVerified: z.date().nullish(),
|
||||
password: z.string().nullish(),
|
||||
bio: z.string().nullish(),
|
||||
avatar: z.string().nullish(),
|
||||
timeZone: z.string(),
|
||||
weekStart: z.string(),
|
||||
startTime: z.number().int(),
|
||||
endTime: z.number().int(),
|
||||
bufferTime: z.number().int(),
|
||||
hideBranding: z.boolean(),
|
||||
theme: z.string().nullish(),
|
||||
createdDate: z.date(),
|
||||
completedOnboarding: z.boolean(),
|
||||
locale: z.string().nullish(),
|
||||
twoFactorSecret: z.string().nullish(),
|
||||
twoFactorEnabled: z.boolean(),
|
||||
identityProvider: z.nativeEnum(IdentityProvider),
|
||||
identityProviderId: z.string().nullish(),
|
||||
invitedTo: z.number().int().nullish(),
|
||||
plan: z.nativeEnum(UserPlan),
|
||||
brandColor: z.string(),
|
||||
away: z.boolean(),
|
||||
metadata: jsonSchema,
|
||||
});
|
||||
|
||||
export interface CompleteUser extends z.infer<typeof _UserModel> {
|
||||
eventTypes: CompleteEventType[];
|
||||
credentials: CompleteCredential[];
|
||||
teams: CompleteMembership[];
|
||||
bookings: CompleteBooking[];
|
||||
availability: CompleteAvailability[];
|
||||
selectedCalendars: CompleteSelectedCalendar[];
|
||||
Schedule: CompleteSchedule[];
|
||||
webhooks: CompleteWebhook[];
|
||||
destinationCalendar?: CompleteDestinationCalendar | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* UserModel contains all relations on your model in addition to the scalars
|
||||
*
|
||||
* NOTE: Lazy required in case of potential circular dependencies within schema
|
||||
*/
|
||||
export const UserModel: z.ZodSchema<CompleteUser> = z.lazy(() =>
|
||||
_UserModel.extend({
|
||||
eventTypes: EventTypeModel.array(),
|
||||
credentials: CredentialModel.array(),
|
||||
teams: MembershipModel.array(),
|
||||
bookings: BookingModel.array(),
|
||||
availability: AvailabilityModel.array(),
|
||||
selectedCalendars: SelectedCalendarModel.array(),
|
||||
Schedule: ScheduleModel.array(),
|
||||
webhooks: WebhookModel.array(),
|
||||
destinationCalendar: DestinationCalendarModel.nullish(),
|
||||
})
|
||||
);
|
|
@ -0,0 +1,12 @@
|
|||
import * as z from "zod";
|
||||
|
||||
import * as imports from "../zod-utils";
|
||||
|
||||
export const _VerificationRequestModel = z.object({
|
||||
id: z.number().int(),
|
||||
identifier: z.string(),
|
||||
token: z.string(),
|
||||
expires: z.date(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
import * as z from "zod";
|
||||
|
||||
import { WebhookTriggerEvents } from "../../node_modules/@prisma/client";
|
||||
import * as imports from "../zod-utils";
|
||||
import { CompleteUser, UserModel } from "./index";
|
||||
|
||||
export const _WebhookModel = z.object({
|
||||
id: z.string(),
|
||||
userId: z.number().int(),
|
||||
subscriberUrl: z.string(),
|
||||
payloadTemplate: z.string().nullish(),
|
||||
createdAt: z.date(),
|
||||
active: z.boolean(),
|
||||
eventTriggers: z.nativeEnum(WebhookTriggerEvents).array(),
|
||||
});
|
||||
|
||||
export interface CompleteWebhook extends z.infer<typeof _WebhookModel> {
|
||||
user: CompleteUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* WebhookModel contains all relations on your model in addition to the scalars
|
||||
*
|
||||
* NOTE: Lazy required in case of potential circular dependencies within schema
|
||||
*/
|
||||
export const WebhookModel: z.ZodSchema<CompleteWebhook> = z.lazy(() =>
|
||||
_WebhookModel.extend({
|
||||
user: UserModel,
|
||||
})
|
||||
);
|
|
@ -20,6 +20,7 @@ import {
|
|||
import slugify from "@lib/slugify";
|
||||
import { Schedule } from "@lib/types/schedule";
|
||||
|
||||
import { eventTypesRouter } from "@server/routers/viewer/eventTypes";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { createProtectedRouter, createRouter } from "../createRouter";
|
||||
|
@ -61,45 +62,27 @@ const publicViewerRouter = createRouter()
|
|||
// routes only available to authenticated users
|
||||
const loggedInViewerRouter = createProtectedRouter()
|
||||
.query("me", {
|
||||
resolve({ ctx }) {
|
||||
const {
|
||||
// pick only the part we want to expose in the API
|
||||
id,
|
||||
name,
|
||||
username,
|
||||
email,
|
||||
startTime,
|
||||
endTime,
|
||||
bufferTime,
|
||||
locale,
|
||||
avatar,
|
||||
createdDate,
|
||||
completedOnboarding,
|
||||
twoFactorEnabled,
|
||||
identityProvider,
|
||||
brandColor,
|
||||
plan,
|
||||
away,
|
||||
} = ctx.user;
|
||||
const me = {
|
||||
id,
|
||||
name,
|
||||
username,
|
||||
email,
|
||||
startTime,
|
||||
endTime,
|
||||
bufferTime,
|
||||
locale,
|
||||
avatar,
|
||||
createdDate,
|
||||
completedOnboarding,
|
||||
twoFactorEnabled,
|
||||
identityProvider,
|
||||
brandColor,
|
||||
plan,
|
||||
away,
|
||||
resolve({ ctx: { user } }) {
|
||||
// Destructuring here only makes it more illegible
|
||||
// pick only the part we want to expose in the API
|
||||
return {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
startTime: user.startTime,
|
||||
endTime: user.endTime,
|
||||
bufferTime: user.bufferTime,
|
||||
locale: user.locale,
|
||||
avatar: user.avatar,
|
||||
createdDate: user.createdDate,
|
||||
completedOnboarding: user.completedOnboarding,
|
||||
twoFactorEnabled: user.twoFactorEnabled,
|
||||
identityProvider: user.identityProvider,
|
||||
brandColor: user.brandColor,
|
||||
plan: user.plan,
|
||||
away: user.away,
|
||||
};
|
||||
return me;
|
||||
},
|
||||
})
|
||||
.mutation("deleteMe", {
|
||||
|
@ -442,34 +425,40 @@ const loggedInViewerRouter = createProtectedRouter()
|
|||
};
|
||||
},
|
||||
})
|
||||
.mutation("setUserDestinationCalendar", {
|
||||
.mutation("setDestinationCalendar", {
|
||||
input: z.object({
|
||||
integration: z.string(),
|
||||
externalId: z.string(),
|
||||
eventTypeId: z.number().optional(),
|
||||
bookingId: z.number().optional(),
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
const { user } = ctx;
|
||||
const userId = ctx.user.id;
|
||||
const { integration, externalId, eventTypeId, bookingId } = input;
|
||||
const calendarCredentials = getCalendarCredentials(user.credentials, user.id);
|
||||
const connectedCalendars = await getConnectedCalendars(calendarCredentials, user.selectedCalendars);
|
||||
const allCals = connectedCalendars.map((cal) => cal.calendars ?? []).flat();
|
||||
|
||||
if (
|
||||
!allCals.find((cal) => cal.externalId === input.externalId && cal.integration === input.integration)
|
||||
) {
|
||||
if (!allCals.find((cal) => cal.externalId === externalId && cal.integration === integration)) {
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: `Could not find calendar ${input.externalId}` });
|
||||
}
|
||||
|
||||
let where;
|
||||
|
||||
if (eventTypeId) where = { eventTypeId };
|
||||
else if (bookingId) where = { bookingId };
|
||||
else where = { userId: user.id };
|
||||
|
||||
await ctx.prisma.destinationCalendar.upsert({
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
where,
|
||||
update: {
|
||||
...input,
|
||||
userId,
|
||||
integration,
|
||||
externalId,
|
||||
},
|
||||
create: {
|
||||
...input,
|
||||
userId,
|
||||
...where,
|
||||
integration,
|
||||
externalId,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
@ -782,5 +771,6 @@ const loggedInViewerRouter = createProtectedRouter()
|
|||
export const viewerRouter = createRouter()
|
||||
.merge(publicViewerRouter)
|
||||
.merge(loggedInViewerRouter)
|
||||
.merge("eventTypes.", eventTypesRouter)
|
||||
.merge("teams.", viewerTeamsRouter)
|
||||
.merge("webhook.", webhookRouter);
|
||||
|
|
|
@ -0,0 +1,249 @@
|
|||
import { EventTypeCustomInput, MembershipRole, PeriodType, Prisma } from "@prisma/client";
|
||||
import {
|
||||
_AvailabilityModel,
|
||||
_DestinationCalendarModel,
|
||||
_EventTypeCustomInputModel,
|
||||
_EventTypeModel,
|
||||
} from "prisma/zod";
|
||||
import { stringOrNumber } from "prisma/zod-utils";
|
||||
import { createEventTypeInput } from "prisma/zod/eventtypeCustom";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createProtectedRouter } from "@server/createRouter";
|
||||
import { viewerRouter } from "@server/routers/viewer";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
function isPeriodType(keyInput: string): keyInput is PeriodType {
|
||||
return Object.keys(PeriodType).includes(keyInput);
|
||||
}
|
||||
|
||||
function handlePeriodType(periodType: string | undefined): PeriodType | undefined {
|
||||
if (typeof periodType !== "string") return undefined;
|
||||
const passedPeriodType = periodType.toUpperCase();
|
||||
if (!isPeriodType(passedPeriodType)) return undefined;
|
||||
return PeriodType[passedPeriodType];
|
||||
}
|
||||
|
||||
function handleCustomInputs(customInputs: EventTypeCustomInput[], eventTypeId: number) {
|
||||
const cInputsIdsToDelete = customInputs.filter((input) => input.id > 0).map((e) => e.id);
|
||||
const cInputsToCreate = customInputs
|
||||
.filter((input) => input.id < 0)
|
||||
.map((input) => ({
|
||||
type: input.type,
|
||||
label: input.label,
|
||||
required: input.required,
|
||||
placeholder: input.placeholder,
|
||||
}));
|
||||
const cInputsToUpdate = customInputs
|
||||
.filter((input) => input.id > 0)
|
||||
.map((input) => ({
|
||||
data: {
|
||||
type: input.type,
|
||||
label: input.label,
|
||||
required: input.required,
|
||||
placeholder: input.placeholder,
|
||||
},
|
||||
where: {
|
||||
id: input.id,
|
||||
},
|
||||
}));
|
||||
|
||||
return {
|
||||
deleteMany: {
|
||||
eventTypeId,
|
||||
NOT: {
|
||||
id: { in: cInputsIdsToDelete },
|
||||
},
|
||||
},
|
||||
createMany: {
|
||||
data: cInputsToCreate,
|
||||
},
|
||||
update: cInputsToUpdate,
|
||||
};
|
||||
}
|
||||
|
||||
const AvailabilityInput = _AvailabilityModel.pick({
|
||||
days: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
});
|
||||
|
||||
const EventTypeUpdateInput = _EventTypeModel
|
||||
/** Optional fields */
|
||||
.extend({
|
||||
availability: z
|
||||
.object({
|
||||
openingHours: z.array(AvailabilityInput).optional(),
|
||||
dateOverrides: z.array(AvailabilityInput).optional(),
|
||||
})
|
||||
.optional(),
|
||||
customInputs: z.array(_EventTypeCustomInputModel),
|
||||
destinationCalendar: _DestinationCalendarModel.pick({
|
||||
integration: true,
|
||||
externalId: true,
|
||||
}),
|
||||
users: z.array(stringOrNumber).optional(),
|
||||
})
|
||||
.partial()
|
||||
.merge(
|
||||
_EventTypeModel
|
||||
/** Required fields */
|
||||
.pick({
|
||||
id: true,
|
||||
})
|
||||
);
|
||||
|
||||
export const eventTypesRouter = createProtectedRouter()
|
||||
.query("list", {
|
||||
async resolve({ ctx }) {
|
||||
return await ctx.prisma.webhook.findMany({
|
||||
where: {
|
||||
userId: ctx.user.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
})
|
||||
.mutation("create", {
|
||||
input: createEventTypeInput,
|
||||
async resolve({ ctx, input }) {
|
||||
const { schedulingType, teamId, ...rest } = input;
|
||||
const data: Prisma.EventTypeCreateInput = {
|
||||
...rest,
|
||||
users: {
|
||||
connect: {
|
||||
id: ctx.user.id,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (teamId && schedulingType) {
|
||||
data.team = {
|
||||
connect: {
|
||||
id: teamId,
|
||||
},
|
||||
};
|
||||
data.schedulingType = schedulingType;
|
||||
}
|
||||
|
||||
const eventType = await ctx.prisma.eventType.create({ data });
|
||||
|
||||
return { eventType };
|
||||
},
|
||||
})
|
||||
// Prevent non-owners to update/delete a team event
|
||||
.middleware(async ({ ctx, rawInput, next }) => {
|
||||
const event = await ctx.prisma.eventType.findUnique({
|
||||
where: { id: (rawInput as Record<"id", number>)?.id },
|
||||
include: {
|
||||
users: true,
|
||||
team: {
|
||||
select: {
|
||||
members: {
|
||||
select: {
|
||||
userId: true,
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!event) {
|
||||
throw new TRPCError({ code: "NOT_FOUND" });
|
||||
}
|
||||
|
||||
const isAuthorized = (function () {
|
||||
if (event.team) {
|
||||
return event.team.members
|
||||
.filter((member) => member.role === MembershipRole.OWNER || member.role === MembershipRole.ADMIN)
|
||||
.map((member) => member.userId)
|
||||
.includes(ctx.user.id);
|
||||
}
|
||||
return event.userId === ctx.user.id || event.users.find((user) => user.id === ctx.user.id);
|
||||
})();
|
||||
|
||||
if (!isAuthorized) {
|
||||
console.warn(`User ${ctx.user.id} attempted to an access an event ${event.id} they do not own.`);
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
|
||||
return next();
|
||||
})
|
||||
.mutation("update", {
|
||||
input: EventTypeUpdateInput.strict(),
|
||||
async resolve({ ctx, input }) {
|
||||
const { availability, periodType, locations, destinationCalendar, customInputs, users, id, ...rest } =
|
||||
input;
|
||||
const data: Prisma.EventTypeUpdateInput = rest;
|
||||
data.locations = locations ?? undefined;
|
||||
|
||||
if (periodType) {
|
||||
data.periodType = handlePeriodType(periodType);
|
||||
}
|
||||
|
||||
if (destinationCalendar) {
|
||||
/** We connect or create a destination calendar to the event type instead of the user */
|
||||
await viewerRouter.createCaller(ctx).mutation("setDestinationCalendar", {
|
||||
...destinationCalendar,
|
||||
eventTypeId: id,
|
||||
});
|
||||
}
|
||||
|
||||
if (customInputs) {
|
||||
data.customInputs = handleCustomInputs(customInputs, id);
|
||||
}
|
||||
|
||||
if (users) {
|
||||
data.users = {
|
||||
set: [],
|
||||
connect: users.map((userId) => ({ id: userId })),
|
||||
};
|
||||
}
|
||||
|
||||
if (availability?.openingHours) {
|
||||
await ctx.prisma.availability.deleteMany({
|
||||
where: {
|
||||
eventTypeId: input.id,
|
||||
},
|
||||
});
|
||||
|
||||
data.availability = {
|
||||
createMany: {
|
||||
data: availability.openingHours,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const eventType = await ctx.prisma.eventType.update({
|
||||
where: { id },
|
||||
data,
|
||||
});
|
||||
|
||||
return { eventType };
|
||||
},
|
||||
})
|
||||
.mutation("delete", {
|
||||
input: z.object({
|
||||
id: z.number(),
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
const { id } = input;
|
||||
|
||||
await ctx.prisma.eventTypeCustomInput.deleteMany({
|
||||
where: {
|
||||
eventTypeId: id,
|
||||
},
|
||||
});
|
||||
|
||||
await ctx.prisma.eventType.delete({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id,
|
||||
};
|
||||
},
|
||||
});
|
109
yarn.lock
109
yarn.lock
|
@ -1855,6 +1855,15 @@
|
|||
dependencies:
|
||||
"@prisma/engines-version" "2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db"
|
||||
|
||||
"@prisma/debug@3.8.1":
|
||||
version "3.8.1"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-3.8.1.tgz#3c6717d6e0501651709714774ea6d90127c6a2d3"
|
||||
integrity sha512-ft4VPTYME1UBJ7trfrBuF2w9jX1ipDy786T9fAEskNGb+y26gPDqz5fiEWc2kgHNeVdz/qTI/V3wXILRyEcgxQ==
|
||||
dependencies:
|
||||
"@types/debug" "4.1.7"
|
||||
ms "2.1.3"
|
||||
strip-ansi "6.0.1"
|
||||
|
||||
"@prisma/engines-version@2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db":
|
||||
version "2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db.tgz#c45323e420f47dd950b22c873bdcf38f75e65779"
|
||||
|
@ -1865,6 +1874,16 @@
|
|||
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db.tgz#b6cf70bc05dd2a62168a16f3ea58a1b011074621"
|
||||
integrity sha512-Q9CwN6e5E5Abso7J3A1fHbcF4NXGRINyMnf7WQ07fXaebxTTARY5BNUzy2Mo5uH82eRVO5v7ImNuR044KTjLJg==
|
||||
|
||||
"@prisma/generator-helper@~3.8.1":
|
||||
version "3.8.1"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/generator-helper/-/generator-helper-3.8.1.tgz#eb1dcc8382faa17c784a9d0e0d79fd207a222aa4"
|
||||
integrity sha512-3zSy+XTEjmjLj6NO+/YPN1Cu7or3xA11TOoOnLRJ9G4pTT67RJXjK0L9Xy5n+3I0Xlb7xrWCgo8MvQQLMWzxPA==
|
||||
dependencies:
|
||||
"@prisma/debug" "3.8.1"
|
||||
"@types/cross-spawn" "6.0.2"
|
||||
chalk "4.1.2"
|
||||
cross-spawn "7.0.3"
|
||||
|
||||
"@radix-ui/number@0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/number/-/number-0.1.0.tgz#73ad13d5cc5f75fa5e147d72e5d5d5e50d688256"
|
||||
|
@ -2380,6 +2399,16 @@
|
|||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
|
||||
"@ts-morph/common@~0.12.2":
|
||||
version "0.12.2"
|
||||
resolved "https://registry.yarnpkg.com/@ts-morph/common/-/common-0.12.2.tgz#61d07a47d622d231e833c44471ab306faaa41aed"
|
||||
integrity sha512-m5KjptpIf1K0t0QL38uE+ol1n+aNn9MgRq++G3Zym1FlqfN+rThsXlp3cAgib14pIeXF7jk3UtJQOviwawFyYg==
|
||||
dependencies:
|
||||
fast-glob "^3.2.7"
|
||||
minimatch "^3.0.4"
|
||||
mkdirp "^1.0.4"
|
||||
path-browserify "^1.0.1"
|
||||
|
||||
"@tsconfig/node10@^1.0.7":
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9"
|
||||
|
@ -2464,6 +2493,20 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/bcryptjs/-/bcryptjs-2.4.2.tgz#e3530eac9dd136bfdfb0e43df2c4c5ce1f77dfae"
|
||||
integrity sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==
|
||||
|
||||
"@types/cross-spawn@6.0.2":
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/cross-spawn/-/cross-spawn-6.0.2.tgz#168309de311cd30a2b8ae720de6475c2fbf33ac7"
|
||||
integrity sha512-KuwNhp3eza+Rhu8IFI5HUXRP0LIhqH5cAjubUvGXXthh4YYBuP2ntwEX+Cz8GJoZUHlKo247wPWOfA9LYEq4cw==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/debug@4.1.7":
|
||||
version "4.1.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82"
|
||||
integrity sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==
|
||||
dependencies:
|
||||
"@types/ms" "*"
|
||||
|
||||
"@types/engine.io@*":
|
||||
version "3.1.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/engine.io/-/engine.io-3.1.7.tgz#86e541a5dc52fb7e97735383564a6ae4cfe2e8f5"
|
||||
|
@ -2546,6 +2589,11 @@
|
|||
"@types/node" "*"
|
||||
"@types/socket.io" "2.1.13"
|
||||
|
||||
"@types/ms@*":
|
||||
version "0.7.31"
|
||||
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197"
|
||||
integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==
|
||||
|
||||
"@types/node@*", "@types/node@>=8.1.0":
|
||||
version "16.11.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae"
|
||||
|
@ -3679,7 +3727,7 @@ chalk@4.0.0:
|
|||
ansi-styles "^4.1.0"
|
||||
supports-color "^7.1.0"
|
||||
|
||||
chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2:
|
||||
chalk@4.1.2, chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
|
||||
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
|
||||
|
@ -3840,6 +3888,13 @@ co@^4.6.0:
|
|||
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
|
||||
integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=
|
||||
|
||||
code-block-writer@^11.0.0:
|
||||
version "11.0.0"
|
||||
resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-11.0.0.tgz#5956fb186617f6740e2c3257757fea79315dd7d4"
|
||||
integrity sha512-GEqWvEWWsOvER+g9keO4ohFoD3ymwyCnqY3hoTr7GZipYFwEhMHJw+TtV0rfgRhNImM6QWZGO2XYjlJVyYT62w==
|
||||
dependencies:
|
||||
tslib "2.3.1"
|
||||
|
||||
collect-v8-coverage@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59"
|
||||
|
@ -4062,6 +4117,15 @@ cross-fetch@3.1.4:
|
|||
dependencies:
|
||||
node-fetch "2.6.1"
|
||||
|
||||
cross-spawn@7.0.3, cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
|
||||
version "7.0.3"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
|
||||
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
|
||||
dependencies:
|
||||
path-key "^3.1.0"
|
||||
shebang-command "^2.0.0"
|
||||
which "^2.0.1"
|
||||
|
||||
cross-spawn@^6.0.0, cross-spawn@^6.0.5:
|
||||
version "6.0.5"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
|
||||
|
@ -4073,15 +4137,6 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5:
|
|||
shebang-command "^1.2.0"
|
||||
which "^1.2.9"
|
||||
|
||||
cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
|
||||
version "7.0.3"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
|
||||
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
|
||||
dependencies:
|
||||
path-key "^3.1.0"
|
||||
shebang-command "^2.0.0"
|
||||
which "^2.0.1"
|
||||
|
||||
crypto-browserify@3.12.0, crypto-browserify@^3.11.0:
|
||||
version "3.12.0"
|
||||
resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
|
||||
|
@ -8151,6 +8206,11 @@ parent-module@^1.0.0:
|
|||
dependencies:
|
||||
callsites "^3.0.0"
|
||||
|
||||
parenthesis@^3.1.8:
|
||||
version "3.1.8"
|
||||
resolved "https://registry.yarnpkg.com/parenthesis/-/parenthesis-3.1.8.tgz#3457fccb8f05db27572b841dad9d2630b912f125"
|
||||
integrity sha512-KF/U8tk54BgQewkJPvB4s/US3VQY68BRDpH638+7O/n58TpnwiwnOtGIOsT2/i+M78s61BBpeC83STB88d8sqw==
|
||||
|
||||
parse-asn1@^5.0.0, parse-asn1@^5.1.5:
|
||||
version "5.1.6"
|
||||
resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4"
|
||||
|
@ -8230,7 +8290,7 @@ pascalcase@^0.1.1:
|
|||
resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
|
||||
integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=
|
||||
|
||||
path-browserify@1.0.1:
|
||||
path-browserify@1.0.1, path-browserify@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd"
|
||||
integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==
|
||||
|
@ -10404,6 +10464,14 @@ ts-jest@^26.0.0:
|
|||
semver "7.x"
|
||||
yargs-parser "20.x"
|
||||
|
||||
ts-morph@^13.0.2:
|
||||
version "13.0.2"
|
||||
resolved "https://registry.yarnpkg.com/ts-morph/-/ts-morph-13.0.2.tgz#55546023493ef82389d9e4f28848a556c784bac4"
|
||||
integrity sha512-SjeeHaRf/mFsNeR3KTJnx39JyEOzT4e+DX28gQx5zjzEOuFs2eGrqeN2PLKs/+AibSxPmzV7RD8nJVKmFJqtLA==
|
||||
dependencies:
|
||||
"@ts-morph/common" "~0.12.2"
|
||||
code-block-writer "^11.0.0"
|
||||
|
||||
ts-node@^10.2.1:
|
||||
version "10.4.0"
|
||||
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.4.0.tgz#680f88945885f4e6cf450e7f0d6223dd404895f7"
|
||||
|
@ -10440,16 +10508,16 @@ tslib@2.0.1:
|
|||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.1.tgz#410eb0d113e5b6356490eec749603725b021b43e"
|
||||
integrity sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==
|
||||
|
||||
tslib@2.3.1, tslib@^2.0.0, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
|
||||
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
|
||||
|
||||
tslib@^1.0.0, tslib@^1.8.1, tslib@^1.9.3:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tslib@^2.0.0, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
|
||||
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
|
||||
|
||||
tslib@~2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
|
||||
|
@ -11192,6 +11260,15 @@ zen-observable@0.8.15:
|
|||
resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15"
|
||||
integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==
|
||||
|
||||
zod-prisma@^0.5.2:
|
||||
version "0.5.2"
|
||||
resolved "https://registry.yarnpkg.com/zod-prisma/-/zod-prisma-0.5.2.tgz#b089e531756073333f986db98190c55c44078db8"
|
||||
integrity sha512-uL7LDCum1LsJbxq4SrrQYkYG7cnAYJCWkLQWVW+e0AJo6UJRjjKb2tmRmU55BLAI6rBT72SWDyHrV28o/7O2pQ==
|
||||
dependencies:
|
||||
"@prisma/generator-helper" "~3.8.1"
|
||||
parenthesis "^3.1.8"
|
||||
ts-morph "^13.0.2"
|
||||
|
||||
zod@^3.8.2:
|
||||
version "3.11.6"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.11.6.tgz#e43a5e0c213ae2e02aefe7cb2b1a6fa3d7f1f483"
|
||||
|
|
Loading…
Reference in New Issue