From 3f822e814be9dd5ba9d8e378d1d419e5102d4ec5 Mon Sep 17 00:00:00 2001 From: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Date: Mon, 26 Dec 2022 16:25:58 +0530 Subject: [PATCH] event type setting refactor (#5959) Co-authored-by: alannnc Co-authored-by: zomars Co-authored-by: Peer Richelsen --- .../components/eventtype/EventAdvancedTab.tsx | 29 +- .../web/components/eventtype/EventAppsTab.tsx | 4 +- .../components/eventtype/EventLimitsTab.tsx | 4 +- .../eventtype/EventRecurringTab.tsx | 5 +- .../components/eventtype/EventSetupTab.tsx | 4 +- .../web/components/eventtype/EventTeamTab.tsx | 52 +-- .../eventtype/EventTeamWebhooksTab.tsx | 4 +- .../eventtype/EventTypeSingleLayout.tsx | 8 +- apps/web/pages/event-types/[type]/index.tsx | 308 +++--------------- packages/lib/getEventTypeById.ts | 275 ++++++++++++++++ packages/prisma/zod-utils.ts | 1 + .../trpc/server/routers/viewer/eventTypes.tsx | 50 +-- 12 files changed, 388 insertions(+), 356 deletions(-) create mode 100644 packages/lib/getEventTypeById.ts diff --git a/apps/web/components/eventtype/EventAdvancedTab.tsx b/apps/web/components/eventtype/EventAdvancedTab.tsx index 2a10f17e1e..b35bcfa721 100644 --- a/apps/web/components/eventtype/EventAdvancedTab.tsx +++ b/apps/web/components/eventtype/EventAdvancedTab.tsx @@ -1,5 +1,5 @@ import Link from "next/link"; -import type { CustomInputParsed, EventTypeSetupInfered, FormValues } from "pages/event-types/[type]"; +import type { CustomInputParsed, EventTypeSetupProps, FormValues } from "pages/event-types/[type]"; import { useEffect, useState } from "react"; import { Controller, useFormContext } from "react-hook-form"; import short from "short-uuid"; @@ -36,7 +36,19 @@ const generateHashedLink = (id: number) => { return uid; }; -export const EventAdvancedTab = ({ eventType, team }: Pick) => { +const getRandomId = (length = 8) => { + return ( + -1 * + parseInt( + Math.ceil(Math.random() * Date.now()) + .toPrecision(length) + .toString() + .replace(".", "") + ) + ); +}; + +export const EventAdvancedTab = ({ eventType, team }: Pick) => { const connectedCalendarsQuery = trpc.viewer.connectedCalendars.useQuery(); const formMethods = useFormContext(); const { t } = useLocale(); @@ -51,7 +63,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick { @@ -64,6 +75,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick { + if (eventType.customInputs) { + setCustomInputs(eventType.customInputs.sort((a, b) => a.id - b.id)); + } + }, [eventType.customInputs]); + return (
{/** @@ -374,7 +391,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick a.id - b.id) || []} + defaultValue={customInputs} render={() => ( { const customInput: CustomInputParsed = { - id: -1, + id: getRandomId(), eventTypeId: -1, label: values.label, placeholder: values.placeholder, required: values.required, type: values.type, options: values.options, + hasToBeCreated: true, }; if (selectedCustomInput) { selectedCustomInput.label = customInput.label; @@ -400,6 +418,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick input.id === values.id); customInputs[inputIndex] = selectedCustomInput; diff --git a/apps/web/components/eventtype/EventAppsTab.tsx b/apps/web/components/eventtype/EventAppsTab.tsx index 19fead5016..de5dc16b84 100644 --- a/apps/web/components/eventtype/EventAppsTab.tsx +++ b/apps/web/components/eventtype/EventAppsTab.tsx @@ -1,4 +1,4 @@ -import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]"; +import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]"; import { useFormContext } from "react-hook-form"; import EventTypeAppContext, { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext"; @@ -9,7 +9,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale"; import { RouterOutputs, trpc } from "@calcom/trpc/react"; import { Button, EmptyScreen, ErrorBoundary, Icon } from "@calcom/ui"; -type EventType = Pick["eventType"] & +type EventType = Pick["eventType"] & EventTypeAppCardComponentProps["eventType"]; function AppCardWrapper({ diff --git a/apps/web/components/eventtype/EventLimitsTab.tsx b/apps/web/components/eventtype/EventLimitsTab.tsx index 21f5d273ce..eb07ad9910 100644 --- a/apps/web/components/eventtype/EventLimitsTab.tsx +++ b/apps/web/components/eventtype/EventLimitsTab.tsx @@ -1,6 +1,6 @@ import { useAutoAnimate } from "@formkit/auto-animate/react"; import * as RadioGroup from "@radix-ui/react-radio-group"; -import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]"; +import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]"; import { useMemo, useRef, useState } from "react"; import { Controller, useFormContext, useWatch } from "react-hook-form"; @@ -12,7 +12,7 @@ import { PeriodType } from "@calcom/prisma/client"; import type { BookingLimit } from "@calcom/types/Calendar"; import { Button, DateRangePicker, Icon, Input, InputField, Label, Select, SettingsToggle } from "@calcom/ui"; -export const EventLimitsTab = ({ eventType }: Pick) => { +export const EventLimitsTab = ({ eventType }: Pick) => { const { t } = useLocale(); const formMethods = useFormContext(); const minimumBookingNoticeType = useRef(findDurationType(eventType.minimumBookingNotice)); diff --git a/apps/web/components/eventtype/EventRecurringTab.tsx b/apps/web/components/eventtype/EventRecurringTab.tsx index e6945ed7ea..4c2da8000b 100644 --- a/apps/web/components/eventtype/EventRecurringTab.tsx +++ b/apps/web/components/eventtype/EventRecurringTab.tsx @@ -1,11 +1,10 @@ -import { EventTypeSetupInfered } from "pages/event-types/[type]"; -import { useState } from "react"; +import { EventTypeSetupProps } from "pages/event-types/[type]"; import getStripeAppData from "@calcom/lib/getStripeAppData"; import RecurringEventController from "./RecurringEventController"; -export const EventRecurringTab = ({ eventType }: Pick) => { +export const EventRecurringTab = ({ eventType }: Pick) => { const stripeAppData = getStripeAppData(eventType); const requirePayment = stripeAppData.price > 0; diff --git a/apps/web/components/eventtype/EventSetupTab.tsx b/apps/web/components/eventtype/EventSetupTab.tsx index 46035b4d44..6837634c92 100644 --- a/apps/web/components/eventtype/EventSetupTab.tsx +++ b/apps/web/components/eventtype/EventSetupTab.tsx @@ -1,7 +1,7 @@ import { useAutoAnimate } from "@formkit/auto-animate/react"; import { zodResolver } from "@hookform/resolvers/zod"; import { isValidPhoneNumber } from "libphonenumber-js"; -import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]"; +import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]"; import { useState } from "react"; import { Controller, useForm, useFormContext } from "react-hook-form"; import { MultiValue } from "react-select"; @@ -23,7 +23,7 @@ type OptionTypeBase = { }; export const EventSetupTab = ( - props: Pick + props: Pick ) => { const { t } = useLocale(); const formMethods = useFormContext(); diff --git a/apps/web/components/eventtype/EventTeamTab.tsx b/apps/web/components/eventtype/EventTeamTab.tsx index e3e166889e..90cc17a0f6 100644 --- a/apps/web/components/eventtype/EventTeamTab.tsx +++ b/apps/web/components/eventtype/EventTeamTab.tsx @@ -1,12 +1,12 @@ import { SchedulingType } from "@prisma/client/"; -import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]"; +import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]"; import { useMemo } from "react"; import { Controller, useFormContext } from "react-hook-form"; import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTeamSelect"; import { WEBAPP_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { Avatar, Button, Icon, Label, Select } from "@calcom/ui"; +import { Label, Select } from "@calcom/ui"; interface IMemberToValue { id: number | null; @@ -26,8 +26,7 @@ export const EventTeamTab = ({ eventType, team, teamMembers, - currentUserMembership, -}: Pick) => { +}: Pick) => { const formMethods = useFormContext(); const { t } = useLocale(); @@ -102,48 +101,3 @@ export const EventTeamTab = ({
); }; - -const TeamProfileCard = ({ - id, - name, - hasPermsToDelete, - email, - username, -}: { - id: number; - name: string | null; - email: string; - username: string | null; - hasPermsToDelete: boolean; -}) => { - const formMethods = useFormContext(); - const avatar = `${WEBAPP_URL}/${username}/avatar.png`; - - return ( -
- -
-
- {name} -
-

{email}

-
- {hasPermsToDelete && ( -
-
- )} -
- ); -}; diff --git a/apps/web/components/eventtype/EventTeamWebhooksTab.tsx b/apps/web/components/eventtype/EventTeamWebhooksTab.tsx index 5abbda8a68..46e5029d71 100644 --- a/apps/web/components/eventtype/EventTeamWebhooksTab.tsx +++ b/apps/web/components/eventtype/EventTeamWebhooksTab.tsx @@ -1,4 +1,4 @@ -import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]"; +import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]"; import { useState } from "react"; import { TbWebhook } from "react-icons/tb"; @@ -14,7 +14,7 @@ import { Button, Dialog, DialogContent, EmptyScreen, Icon, showToast } from "@ca export const EventTeamWebhooksTab = ({ eventType, team, -}: Pick) => { +}: Pick) => { const { t } = useLocale(); const utils = trpc.useContext(); diff --git a/apps/web/components/eventtype/EventTypeSingleLayout.tsx b/apps/web/components/eventtype/EventTypeSingleLayout.tsx index 236e7735c9..21154459bd 100644 --- a/apps/web/components/eventtype/EventTypeSingleLayout.tsx +++ b/apps/web/components/eventtype/EventTypeSingleLayout.tsx @@ -1,6 +1,6 @@ import { TFunction } from "next-i18next"; import { useRouter } from "next/router"; -import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]"; +import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]"; import { useMemo, useState } from "react"; import { UseFormReturn } from "react-hook-form"; import { TbWebhook } from "react-icons/tb"; @@ -37,9 +37,9 @@ import { EmbedButton, EmbedDialog } from "@components/Embed"; type Props = { children: React.ReactNode; - eventType: EventTypeSetupInfered["eventType"]; - currentUserMembership: EventTypeSetupInfered["currentUserMembership"]; - team: EventTypeSetupInfered["team"]; + eventType: EventTypeSetupProps["eventType"]; + currentUserMembership: EventTypeSetupProps["currentUserMembership"]; + team: EventTypeSetupProps["team"]; disableBorder?: boolean; enabledAppsNumber: number; installedAppsNumber: number; diff --git a/apps/web/pages/event-types/[type]/index.tsx b/apps/web/pages/event-types/[type]/index.tsx index 44a4a4eace..4ea852d4bc 100644 --- a/apps/web/pages/event-types/[type]/index.tsx +++ b/apps/web/pages/event-types/[type]/index.tsx @@ -1,27 +1,24 @@ /* eslint-disable @typescript-eslint/no-empty-function */ import { useAutoAnimate } from "@formkit/auto-animate/react"; import { zodResolver } from "@hookform/resolvers/zod"; -import { PeriodType, Prisma, SchedulingType } from "@prisma/client"; +import { PeriodType, SchedulingType } from "@prisma/client"; import { GetServerSidePropsContext } from "next"; import { useRouter } from "next/router"; import { useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; -import { StripeData } from "@calcom/app-store/stripepayment/lib/server"; -import { getEventTypeAppData, getLocationOptions } from "@calcom/app-store/utils"; -import { EventLocationType, LocationObject } from "@calcom/core/location"; -import { parseBookingLimit, parseRecurringEvent, validateBookingLimitOrder } from "@calcom/lib"; -import getEnabledApps from "@calcom/lib/apps/getEnabledApps"; +import { EventLocationType } from "@calcom/core/location"; +import { validateBookingLimitOrder } from "@calcom/lib"; import { CAL_URL } from "@calcom/lib/constants"; import convertToNewDurationType from "@calcom/lib/convertToNewDurationType"; import findDurationType from "@calcom/lib/findDurationType"; -import getStripeAppData from "@calcom/lib/getStripeAppData"; +import getEventTypeById from "@calcom/lib/getEventTypeById"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { HttpError } from "@calcom/lib/http-error"; import prisma from "@calcom/prisma"; import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; -import { trpc } from "@calcom/trpc/react"; +import { trpc, RouterOutputs } from "@calcom/trpc/react"; import type { BookingLimit, RecurringEvent } from "@calcom/types/Calendar"; import { Form, showToast } from "@calcom/ui"; @@ -41,7 +38,6 @@ import { EventTeamWebhooksTab } from "@components/eventtype/EventTeamWebhooksTab import { EventTypeSingleLayout } from "@components/eventtype/EventTypeSingleLayout"; import EventWorkflowsTab from "@components/eventtype/EventWorkfowsTab"; -import { getTranslation } from "@server/lib/i18n"; import { ssrInit } from "@server/lib/ssr"; export type FormValues = { @@ -110,27 +106,24 @@ const querySchema = z.object({ .default("setup"), }); -export type EventTypeSetupInfered = inferSSRProps; +export type EventTypeSetupProps = RouterOutputs["viewer"]["eventTypes"]["get"]; -const EventTypePage = (props: inferSSRProps) => { +const EventTypePage = (props: EventTypeSetupProps) => { const { t } = useLocale(); + const utils = trpc.useContext(); + const router = useRouter(); + const { tabName } = querySchema.parse(router.query); + const { data: eventTypeApps } = trpc.viewer.apps.useQuery({ extendsFeature: "EventType", }); - const { eventType: dbEventType, locationOptions, team, teamMembers } = props; - // TODO: It isn't a good idea to maintain state using setEventType. If we want to connect the SSR'd data to tRPC, we should useQuery(["viewer.eventTypes.get"]) with initialData - // Due to this change, when Form is saved, there is no way to propagate that info to eventType (e.g. disabling stripe app doesn't allow recurring tab to be enabled without refresh). - const [eventType, setEventType] = useState(dbEventType); - - const router = useRouter(); - const { tabName } = querySchema.parse(router.query); + const { eventType, locationOptions, team, teamMembers, currentUserMembership } = props; const [animationParentRef] = useAutoAnimate(); const updateMutation = trpc.viewer.eventTypes.update.useMutation({ - onSuccess: async ({ eventType: newEventType }) => { - setEventType({ ...eventType, slug: newEventType.slug }); + onSuccess: async () => { showToast( t("event_type_updated_successfully", { eventTypeTitle: eventType.title, @@ -138,6 +131,9 @@ const EventTypePage = (props: inferSSRProps) => { "success" ); }, + async onSettled() { + await utils.viewer.eventTypes.get.invalidate(); + }, onError: (err) => { let message = ""; if (err instanceof HttpError) { @@ -241,14 +237,7 @@ const EventTypePage = (props: inferSSRProps) => { /> ), availability: , - team: ( - - ), + team: , limits: , advanced: , recurring: , @@ -272,7 +261,7 @@ const EventTypePage = (props: inferSSRProps) => { isUpdateMutationLoading={updateMutation.isLoading} formMethods={formMethods} disableBorder={tabName === "apps" || tabName === "workflows" || tabName === "webhooks"} - currentUserMembership={props.currentUserMembership}> + currentUserMembership={currentUserMembership}>
) => { ); }; +const EventTypePageWrapper = (props: inferSSRProps) => { + const { data, isLoading } = trpc.viewer.eventTypes.get.useQuery( + { id: props.type }, + { + initialData: props.initialData, + } + ); + + if (isLoading || !data) return null; + return ; +}; + export const getServerSideProps = async (context: GetServerSidePropsContext) => { const { req, query } = context; const session = await getSession({ req }); @@ -358,244 +359,27 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => }; } - const userSelect = Prisma.validator()({ - name: true, - username: true, - id: true, - avatar: true, - email: true, - locale: true, - defaultScheduleId: true, - }); - - const rawEventType = await prisma.eventType.findFirst({ - where: { - AND: [ - { - OR: [ - { - users: { - some: { - id: session.user.id, - }, - }, - }, - { - team: { - members: { - some: { - userId: session.user.id, - }, - }, - }, - }, - { - userId: session.user.id, - }, - ], - }, - { - id: typeParam, - }, - ], - }, - select: { - id: true, - title: true, - slug: true, - description: true, - length: true, - hidden: true, - locations: true, - eventName: true, - customInputs: true, - timeZone: true, - periodType: true, - metadata: true, - periodDays: true, - periodStartDate: true, - periodEndDate: true, - periodCountCalendarDays: true, - requiresConfirmation: true, - recurringEvent: true, - hideCalendarNotes: true, - disableGuests: true, - minimumBookingNotice: true, - beforeEventBuffer: true, - afterEventBuffer: true, - slotInterval: true, - hashedLink: true, - bookingLimits: true, - successRedirectUrl: true, - currency: true, - team: { - select: { - id: true, - slug: true, - members: { - where: { - accepted: true, - }, - select: { - role: true, - user: { - select: userSelect, - }, - }, - }, + try { + const res = await getEventTypeById({ eventTypeId: typeParam, userId: session.user.id, prisma }); + return { + props: { + session, + type: typeParam, + trpcState: ssr.dehydrate(), + initialData: { + eventType: res.eventType, + locationOptions: res.locationOptions, + team: res.team, + teamMembers: res.teamMembers, + currentUserMembership: res.currentUserMembership, }, }, - users: { - select: userSelect, - }, - schedulingType: true, - schedule: { - select: { - id: true, - }, - }, - userId: true, - price: true, - destinationCalendar: true, - seatsPerTimeSlot: true, - seatsShowAttendees: true, - webhooks: { - select: { - id: true, - subscriberUrl: true, - payloadTemplate: true, - active: true, - eventTriggers: true, - secret: true, - eventTypeId: true, - }, - }, - workflows: { - include: { - workflow: { - include: { - activeOn: { - select: { - eventType: { - select: { - id: true, - title: true, - }, - }, - }, - }, - steps: true, - }, - }, - }, - }, - }, - }); - - if (!rawEventType) { + }; + } catch (err) { return { notFound: true, }; } - - const credentials = await prisma.credential.findMany({ - where: { - userId: session.user.id, - app: { - enabled: true, - }, - }, - select: { - id: true, - type: true, - key: true, - userId: true, - appId: true, - invalid: true, - }, - }); - - const { locations, metadata, ...restEventType } = rawEventType; - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const newMetadata = EventTypeMetaDataSchema.parse(metadata || {})!; - const apps = newMetadata.apps || {}; - const eventTypeWithParsedMetadata = { ...rawEventType, metadata: newMetadata }; - newMetadata.apps = { - ...apps, - stripe: { - ...getStripeAppData(eventTypeWithParsedMetadata, true), - currency: - ( - credentials.find((integration) => integration.type === "stripe_payment") - ?.key as unknown as StripeData - )?.default_currency || "usd", - }, - giphy: getEventTypeAppData(eventTypeWithParsedMetadata, "giphy", true), - rainbow: getEventTypeAppData(eventTypeWithParsedMetadata, "rainbow", true), - }; - - // TODO: How to extract metadata schema from _EventTypeModel to be able to parse it? - // const parsedMetaData = _EventTypeModel.parse(newMetadata); - const parsedMetaData = newMetadata; - - const parsedCustomInputs = (rawEventType.customInputs || []).map((input) => customInputSchema.parse(input)); - - const eventType = { - ...restEventType, - schedule: rawEventType.schedule?.id || rawEventType.users[0]?.defaultScheduleId || null, - recurringEvent: parseRecurringEvent(restEventType.recurringEvent), - bookingLimits: parseBookingLimit(restEventType.bookingLimits), - locations: locations as unknown as LocationObject[], - metadata: parsedMetaData, - customInputs: parsedCustomInputs, - }; - - // backwards compat - if (eventType.users.length === 0 && !eventType.team) { - const fallbackUser = await prisma.user.findUnique({ - where: { - id: session.user.id, - }, - select: userSelect, - }); - if (!fallbackUser) throw Error("The event type doesn't have user and no fallback user was found"); - eventType.users.push(fallbackUser); - } - const currentUser = eventType.users.find((u) => u.id === session.user.id); - const t = await getTranslation(currentUser?.locale ?? "en", "common"); - const integrations = await getEnabledApps(credentials); - const locationOptions = getLocationOptions(integrations, t); - - const eventTypeObject = Object.assign({}, eventType, { - periodStartDate: eventType.periodStartDate?.toString() ?? null, - periodEndDate: eventType.periodEndDate?.toString() ?? null, - }); - - const teamMembers = eventTypeObject.team - ? eventTypeObject.team.members.map((member) => { - const user = member.user; - user.avatar = `${CAL_URL}/${user.username}/avatar.png`; - return user; - }) - : []; - - // Find the current users memebership so we can check role to enable/disable deletion. - // Sets to null if no membership is found - this must mean we are in a none team event type - const currentUserMembership = - eventTypeObject.team?.members.find((el) => el.user.id === session.user.id) ?? null; - - return { - props: { - session, - eventType: eventTypeObject, - locationOptions, - team: eventTypeObject.team || null, - teamMembers, - currentUserMembership, - trpcState: ssr.dehydrate(), - }, - }; }; -export default EventTypePage; +export default EventTypePageWrapper; diff --git a/packages/lib/getEventTypeById.ts b/packages/lib/getEventTypeById.ts new file mode 100644 index 0000000000..435eed911e --- /dev/null +++ b/packages/lib/getEventTypeById.ts @@ -0,0 +1,275 @@ +import { Prisma, PrismaClient } from "@prisma/client"; + +import { StripeData } from "@calcom/app-store/stripepayment/lib/server"; +import { getEventTypeAppData, getLocationOptions } from "@calcom/app-store/utils"; +import { LocationObject } from "@calcom/core/location"; +import { parseBookingLimit, parseRecurringEvent } from "@calcom/lib"; +import getEnabledApps from "@calcom/lib/apps/getEnabledApps"; +import { CAL_URL } from "@calcom/lib/constants"; +import getStripeAppData from "@calcom/lib/getStripeAppData"; +import { getTranslation } from "@calcom/lib/server/i18n"; +import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; + +import { TRPCError } from "@trpc/server"; + +interface getEventTypeByIdProps { + eventTypeId: number; + userId: number; + prisma: PrismaClient< + Prisma.PrismaClientOptions, + never, + Prisma.RejectOnNotFound | Prisma.RejectPerOperation | undefined + >; + isTrpcCall?: boolean; +} + +export default async function getEventTypeById({ + eventTypeId, + userId, + prisma, + isTrpcCall = false, +}: getEventTypeByIdProps) { + const userSelect = Prisma.validator()({ + name: true, + username: true, + id: true, + avatar: true, + email: true, + locale: true, + defaultScheduleId: true, + }); + + const rawEventType = await prisma.eventType.findFirst({ + where: { + AND: [ + { + OR: [ + { + users: { + some: { + id: userId, + }, + }, + }, + { + team: { + members: { + some: { + userId: userId, + }, + }, + }, + }, + { + userId: userId, + }, + ], + }, + { + id: eventTypeId, + }, + ], + }, + select: { + id: true, + title: true, + slug: true, + description: true, + length: true, + hidden: true, + locations: true, + eventName: true, + customInputs: true, + timeZone: true, + periodType: true, + metadata: true, + periodDays: true, + periodStartDate: true, + periodEndDate: true, + periodCountCalendarDays: true, + requiresConfirmation: true, + recurringEvent: true, + hideCalendarNotes: true, + disableGuests: true, + minimumBookingNotice: true, + beforeEventBuffer: true, + afterEventBuffer: true, + slotInterval: true, + hashedLink: true, + bookingLimits: true, + successRedirectUrl: true, + currency: true, + team: { + select: { + id: true, + slug: true, + members: { + where: { + accepted: true, + }, + select: { + role: true, + user: { + select: userSelect, + }, + }, + }, + }, + }, + users: { + select: userSelect, + }, + schedulingType: true, + schedule: { + select: { + id: true, + }, + }, + userId: true, + price: true, + destinationCalendar: true, + seatsPerTimeSlot: true, + seatsShowAttendees: true, + webhooks: { + select: { + id: true, + subscriberUrl: true, + payloadTemplate: true, + active: true, + eventTriggers: true, + secret: true, + eventTypeId: true, + }, + }, + workflows: { + include: { + workflow: { + include: { + activeOn: { + select: { + eventType: { + select: { + id: true, + title: true, + }, + }, + }, + }, + steps: true, + }, + }, + }, + }, + }, + }); + + if (!rawEventType) { + if (isTrpcCall) { + throw new TRPCError({ code: "NOT_FOUND" }); + } else { + throw new Error("Event type noy found"); + } + } + + const credentials = await prisma.credential.findMany({ + where: { + userId, + app: { + enabled: true, + }, + }, + select: { + id: true, + type: true, + key: true, + userId: true, + appId: true, + invalid: true, + }, + }); + + const { locations, metadata, ...restEventType } = rawEventType; + const newMetadata = EventTypeMetaDataSchema.parse(metadata || {})!; + const apps = newMetadata.apps || {}; + const eventTypeWithParsedMetadata = { ...rawEventType, metadata: newMetadata }; + newMetadata.apps = { + ...apps, + stripe: { + ...getStripeAppData(eventTypeWithParsedMetadata, true), + currency: + ( + credentials.find((integration) => integration.type === "stripe_payment") + ?.key as unknown as StripeData + )?.default_currency || "usd", + }, + giphy: getEventTypeAppData(eventTypeWithParsedMetadata, "giphy", true), + rainbow: getEventTypeAppData(eventTypeWithParsedMetadata, "rainbow", true), + }; + + // TODO: How to extract metadata schema from _EventTypeModel to be able to parse it? + // const parsedMetaData = _EventTypeModel.parse(newMetadata); + const parsedMetaData = newMetadata; + + const parsedCustomInputs = (rawEventType.customInputs || []).map((input) => customInputSchema.parse(input)); + + const eventType = { + ...restEventType, + schedule: rawEventType.schedule?.id || rawEventType.users[0]?.defaultScheduleId || null, + recurringEvent: parseRecurringEvent(restEventType.recurringEvent), + bookingLimits: parseBookingLimit(restEventType.bookingLimits), + locations: locations as unknown as LocationObject[], + metadata: parsedMetaData, + customInputs: parsedCustomInputs, + }; + + // backwards compat + if (eventType.users.length === 0 && !eventType.team) { + const fallbackUser = await prisma.user.findUnique({ + where: { + id: userId, + }, + select: userSelect, + }); + if (!fallbackUser) { + if (isTrpcCall) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "The event type doesn't have user and no fallback user was found", + }); + } else { + throw Error("The event type doesn't have user and no fallback user was found"); + } + } + eventType.users.push(fallbackUser); + } + const currentUser = eventType.users.find((u) => u.id === userId); + const t = await getTranslation(currentUser?.locale ?? "en", "common"); + const integrations = await getEnabledApps(credentials); + const locationOptions = getLocationOptions(integrations, t); + + const eventTypeObject = Object.assign({}, eventType, { + periodStartDate: eventType.periodStartDate?.toString() ?? null, + periodEndDate: eventType.periodEndDate?.toString() ?? null, + }); + + const teamMembers = eventTypeObject.team + ? eventTypeObject.team.members.map((member) => { + const user = member.user; + user.avatar = `${CAL_URL}/${user.username}/avatar.png`; + return user; + }) + : []; + + // Find the current users memebership so we can check role to enable/disable deletion. + // Sets to null if no membership is found - this must mean we are in a none team event type + const currentUserMembership = eventTypeObject.team?.members.find((el) => el.user.id === userId) ?? null; + + const finalObj = { + eventType: eventTypeObject, + locationOptions, + team: eventTypeObject.team || null, + teamMembers, + currentUserMembership, + }; + return finalObj; +} diff --git a/packages/prisma/zod-utils.ts b/packages/prisma/zod-utils.ts index 43f0f8bb7e..83c86cae9e 100644 --- a/packages/prisma/zod-utils.ts +++ b/packages/prisma/zod-utils.ts @@ -241,6 +241,7 @@ export const customInputSchema = z.object({ options: customInputOptionSchema.optional().nullable(), required: z.boolean(), placeholder: z.string(), + hasToBeCreated: z.boolean().optional(), }); export type CustomInputSchema = z.infer; diff --git a/packages/trpc/server/routers/viewer/eventTypes.tsx b/packages/trpc/server/routers/viewer/eventTypes.tsx index 5bf4169b91..54cd8a0808 100644 --- a/packages/trpc/server/routers/viewer/eventTypes.tsx +++ b/packages/trpc/server/routers/viewer/eventTypes.tsx @@ -9,6 +9,7 @@ import { DailyLocationType } from "@calcom/app-store/locations"; import { stripeDataSchema } from "@calcom/app-store/stripepayment/lib/server"; import { validateBookingLimitOrder } from "@calcom/lib"; import { CAL_URL } from "@calcom/lib/constants"; +import getEventTypeById from "@calcom/lib/getEventTypeById"; import { baseEventTypeSelect, baseUserSelect } from "@calcom/prisma"; import { _DestinationCalendarModel, _EventTypeModel } from "@calcom/prisma/zod"; import { @@ -36,9 +37,10 @@ function handlePeriodType(periodType: string | undefined): PeriodType | undefine } function handleCustomInputs(customInputs: CustomInputSchema[], eventTypeId: number) { - const cInputsIdsToDelete = customInputs.filter((input) => input.id > 0).map((e) => e.id); + const cInputsIdsToDeleteOrUpdated = customInputs.filter((input) => !input.hasToBeCreated); + const cInputsIdsToDelete = cInputsIdsToDeleteOrUpdated.map((e) => e.id); const cInputsToCreate = customInputs - .filter((input) => input.id < 0) + .filter((input) => input.hasToBeCreated) .map((input) => ({ type: input.type, label: input.label, @@ -46,20 +48,18 @@ function handleCustomInputs(customInputs: CustomInputSchema[], eventTypeId: numb placeholder: input.placeholder, options: input.options || undefined, })); - const cInputsToUpdate = customInputs - .filter((input) => input.id > 0) - .map((input) => ({ - data: { - type: input.type, - label: input.label, - required: input.required, - placeholder: input.placeholder, - options: input.options || undefined, - }, - where: { - id: input.id, - }, - })); + const cInputsToUpdate = cInputsIdsToDeleteOrUpdated.map((input) => ({ + data: { + type: input.type, + label: input.label, + required: input.required, + placeholder: input.placeholder, + options: input.options || undefined, + }, + where: { + id: input.id, + }, + })); return { deleteMany: { @@ -436,7 +436,7 @@ export const eventTypesRouter = router({ throw new TRPCError({ code: "BAD_REQUEST", message: "URL Slug already exists for given user." }); } } - throw e; + throw new TRPCError({ code: "BAD_REQUEST" }); } }), get: eventOwnerProcedure @@ -463,15 +463,15 @@ export const eventTypesRouter = router({ if (!user) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); } - return await ctx.prisma.eventType.findUnique({ - where: { - id: input.id, - }, - include: { - team: true, - users: true, - }, + + const res = await getEventTypeById({ + eventTypeId: input.id, + userId: ctx.user.id, + prisma: ctx.prisma, + isTrpcCall: true, }); + + return res; }), update: eventOwnerProcedure.input(EventTypeUpdateInput.strict()).mutation(async ({ ctx, input }) => { const {