From 3ab002e5470c5c528a3a64c54355d7417c4b9673 Mon Sep 17 00:00:00 2001 From: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Date: Thu, 1 Dec 2022 21:53:52 +0000 Subject: [PATCH] 5444 cal 339 radio option in additional questions on public booking page (#5804) * Use field array intro * WIP - form submitting wrong form * WIP with fake useFormHook * WORKING! OMG Co-authored-by: Alex Co-authored-by: Jeroen Reumkens * Booking Page styling * Fix duplicate fields * Radio string * Type error * Linting errors * Remove unused duplicate file * Fixed user related type error * remove log * Remove console logs * remove console log * fix dark mode text and comment style Co-authored-by: Alex Co-authored-by: Jeroen Reumkens Co-authored-by: Alex van Andel Co-authored-by: alannnc --- .../components/booking/pages/BookingPage.tsx | 24 +++- .../eventtype/CustomInputTypeForm.tsx | 115 +++++++++++++-- .../components/eventtype/EventAdvancedTab.tsx | 31 ++-- .../pages/eventtypes/CustomInputTypeForm.tsx | 134 ------------------ apps/web/pages/[user]/book.tsx | 3 +- apps/web/pages/booking/[uid].tsx | 39 ++++- apps/web/pages/d/[link]/book.tsx | 3 +- apps/web/pages/event-types/[type]/index.tsx | 15 +- apps/web/pages/team/[slug]/book.tsx | 3 +- apps/web/public/static/locales/en/common.json | 6 +- .../features/bookings/lib/handleNewBooking.ts | 9 +- packages/lib/defaultEvents.ts | 6 +- .../migration.sql | 5 + packages/prisma/schema.prisma | 3 + packages/prisma/zod-utils.ts | 20 +++ .../trpc/server/routers/viewer/eventTypes.tsx | 13 +- packages/ui/index.tsx | 2 +- packages/ui/v2/core/form/index.ts | 8 +- packages/ui/v2/core/form/radio-area/Radio.tsx | 2 +- packages/ui/v2/core/form/radio-area/index.ts | 2 +- 20 files changed, 259 insertions(+), 184 deletions(-) delete mode 100644 apps/web/components/pages/eventtypes/CustomInputTypeForm.tsx create mode 100644 packages/prisma/migrations/20221129112344_adding_radio_custom_input/migration.sql diff --git a/apps/web/components/booking/pages/BookingPage.tsx b/apps/web/components/booking/pages/BookingPage.tsx index 43b16796b1..596e5dc0c4 100644 --- a/apps/web/components/booking/pages/BookingPage.tsx +++ b/apps/web/components/booking/pages/BookingPage.tsx @@ -39,11 +39,11 @@ import { HttpError } from "@calcom/lib/http-error"; import { getEveryFreqFor } from "@calcom/lib/recurringStrings"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; import { AddressInput, Button, EmailInput, Form, Icon, PhoneInput, Tooltip } from "@calcom/ui"; +import { Group, RadioField } from "@calcom/ui"; import { asStringOrNull } from "@lib/asStringOrNull"; import { timeZone } from "@lib/clock"; import { ensureArray } from "@lib/ensureArray"; -import useMeQuery from "@lib/hooks/useMeQuery"; import useRouterQuery from "@lib/hooks/useRouterQuery"; import createBooking from "@lib/mutations/bookings/create-booking"; import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking"; @@ -103,7 +103,6 @@ const BookingPage = ({ {} ); const stripeAppData = getStripeAppData(eventType); - // Define duration now that we support multiple duration eventTypes let duration = eventType.length; if (queryDuration && !isNaN(Number(queryDuration))) { @@ -745,6 +744,27 @@ const BookingPage = ({ )} + {input.options && input.type === EventTypeCustomInputType.RADIO && ( +
+
+ { + bookingForm.setValue(`customInputs.${input.id}`, e); + }}> + <> + {input.options.map((option, i) => ( + + ))} + + +
+
+ )} ))} {!eventType.disableGuests && guestToggle && ( diff --git a/apps/web/components/eventtype/CustomInputTypeForm.tsx b/apps/web/components/eventtype/CustomInputTypeForm.tsx index 07510bcaa1..1a7d7fee20 100644 --- a/apps/web/components/eventtype/CustomInputTypeForm.tsx +++ b/apps/web/components/eventtype/CustomInputTypeForm.tsx @@ -1,23 +1,33 @@ -import { EventTypeCustomInput, EventTypeCustomInputType } from "@prisma/client"; +import { useAutoAnimate } from "@formkit/auto-animate/react"; +import { EventTypeCustomInputType } from "@prisma/client"; +import type { CustomInputParsed } from "pages/event-types/[type]"; import { FC } from "react"; -import { Controller, SubmitHandler, useForm, useWatch } from "react-hook-form"; +import { Control, Controller, useFieldArray, useForm, UseFormRegister, useWatch } from "react-hook-form"; -import { Button, Select, TextField } from "@calcom/ui"; - -import { useLocale } from "@lib/hooks/useLocale"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Button, Icon, Label, Select, TextField } from "@calcom/ui"; interface OptionTypeBase { label: string; value: EventTypeCustomInputType; + options?: { label: string; type: string }[]; } interface Props { - onSubmit: SubmitHandler; + onSubmit: (output: CustomInputParsed) => void; onCancel: () => void; - selectedCustomInput?: EventTypeCustomInput; + selectedCustomInput?: CustomInputParsed; } -type IFormInput = EventTypeCustomInput; +type IFormInput = CustomInputParsed; + +/** + * Getting a random ID gives us the option to know WHICH field is changed + * when the user edits a custom field. + * This UUID is only used to check for changes in the UI and not the ID we use in the DB + * There is very very very slim chance that this will cause a collision + * */ +const randomId = () => Math.floor(Math.random() * 1000000 + new Date().getTime()); const CustomInputTypeForm: FC = (props) => { const { t } = useLocale(); @@ -26,10 +36,22 @@ const CustomInputTypeForm: FC = (props) => { { value: EventTypeCustomInputType.TEXTLONG, label: t("multiline_text") }, { value: EventTypeCustomInputType.NUMBER, label: t("number") }, { value: EventTypeCustomInputType.BOOL, label: t("checkbox") }, + { + value: EventTypeCustomInputType.RADIO, + label: t("radio"), + }, ]; + const { selectedCustomInput } = props; - const defaultValues = selectedCustomInput || { type: inputOptions[0].value }; - const { register, control, handleSubmit } = useForm({ + + const defaultValues = selectedCustomInput + ? { ...selectedCustomInput, id: selectedCustomInput?.id || randomId() } + : { + id: randomId(), + type: EventTypeCustomInputType.TEXT, + }; + + const { register, control, getValues } = useForm({ defaultValues, }); const selectedInputType = useWatch({ name: "type", control }); @@ -40,7 +62,7 @@ const CustomInputTypeForm: FC = (props) => { }; return ( -
+
); }; +function RadioInputHandler({ + register, + control, +}: { + register: UseFormRegister; + control: Control; +}) { + const { t } = useLocale(); + const { fields, append, remove } = useFieldArray({ + control, + name: "options", + shouldUnregister: true, + }); + const [animateRef] = useAutoAnimate(); + + return ( +
+ +
    + <> + {fields.map((option, index) => ( +
  • + { + remove(index); + }} + /> + } + /> +
  • + ))} + + +
+
+ ); +} + export default CustomInputTypeForm; diff --git a/apps/web/components/eventtype/EventAdvancedTab.tsx b/apps/web/components/eventtype/EventAdvancedTab.tsx index 7b0e7f0cbc..98ac176216 100644 --- a/apps/web/components/eventtype/EventAdvancedTab.tsx +++ b/apps/web/components/eventtype/EventAdvancedTab.tsx @@ -1,6 +1,5 @@ -import { EventTypeCustomInput } from "@prisma/client/"; import Link from "next/link"; -import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]"; +import type { CustomInputParsed, EventTypeSetupInfered, FormValues } from "pages/event-types/[type]"; import { useEffect, useState } from "react"; import { Controller, useFormContext } from "react-hook-form"; import short from "short-uuid"; @@ -43,10 +42,10 @@ export const EventAdvancedTab = ({ eventType, team }: Pick( + const [customInputs, setCustomInputs] = useState( eventType.customInputs.sort((a, b) => a.id - b.id) || [] ); - const [selectedCustomInput, setSelectedCustomInput] = useState(undefined); + const [selectedCustomInput, setSelectedCustomInput] = useState(undefined); const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false); const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`; @@ -125,15 +124,13 @@ export const EventAdvancedTab = ({ eventType, team }: Pick { if (e && customInputs.length === 0) { // Push a placeholders - setSelectedCustomInput(undefined); setSelectedCustomInputModalOpen(true); } else if (!e) { - setCustomInputs([]); formMethods.setValue("customInputs", []); } }}>
    - {customInputs.map((customInput: EventTypeCustomInput, idx: number) => ( + {customInputs.map((customInput, idx) => ( { - const customInput: EventTypeCustomInput = { + const customInput: CustomInputParsed = { id: -1, eventTypeId: -1, label: values.label, placeholder: values.placeholder, required: values.required, type: values.type, + options: values.options, }; - if (selectedCustomInput) { selectedCustomInput.label = customInput.label; selectedCustomInput.placeholder = customInput.placeholder; selectedCustomInput.required = customInput.required; selectedCustomInput.type = customInput.type; + selectedCustomInput.options = customInput.options || undefined; + // Update by id + const inputIndex = customInputs.findIndex((input) => input.id === values.id); + customInputs[inputIndex] = selectedCustomInput; + setCustomInputs(customInputs); + formMethods.setValue("customInputs", customInputs); } else { - setCustomInputs(customInputs.concat(customInput)); - formMethods.setValue("customInputs", customInputs.concat(customInput)); + const concatted = customInputs.concat({ + ...customInput, + options: customInput.options, + }); + console.log(concatted); + setCustomInputs(concatted); + formMethods.setValue("customInputs", concatted); } + setSelectedCustomInputModalOpen(false); }} onCancel={() => { diff --git a/apps/web/components/pages/eventtypes/CustomInputTypeForm.tsx b/apps/web/components/pages/eventtypes/CustomInputTypeForm.tsx deleted file mode 100644 index b342c279d7..0000000000 --- a/apps/web/components/pages/eventtypes/CustomInputTypeForm.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { EventTypeCustomInput, EventTypeCustomInputType } from "@prisma/client"; -import React, { FC } from "react"; -import { Controller, SubmitHandler, useForm, useWatch } from "react-hook-form"; - -import { Button } from "@calcom/ui"; - -import { useLocale } from "@lib/hooks/useLocale"; - -import Select from "@components/ui/form/Select"; - -interface OptionTypeBase { - label: string; - value: EventTypeCustomInputType; -} - -interface Props { - onSubmit: SubmitHandler; - onCancel: () => void; - selectedCustomInput?: EventTypeCustomInput; -} - -type IFormInput = EventTypeCustomInput; - -const CustomInputTypeForm: FC = (props) => { - const { t } = useLocale(); - const inputOptions: OptionTypeBase[] = [ - { value: EventTypeCustomInputType.TEXT, label: t("text") }, - { value: EventTypeCustomInputType.TEXTLONG, label: t("multiline_text") }, - { value: EventTypeCustomInputType.NUMBER, label: t("number") }, - { value: EventTypeCustomInputType.BOOL, label: t("checkbox") }, - ]; - const { selectedCustomInput } = props; - const defaultValues = selectedCustomInput || { type: inputOptions[0].value }; - const { register, control, handleSubmit } = useForm({ - defaultValues, - }); - const selectedInputType = useWatch({ name: "type", control }); - const selectedInputOption = inputOptions.find((e) => selectedInputType === e.value); - - const onCancel = () => { - props.onCancel(); - }; - - return ( -
    -
    - - ( - -
    -
- {(selectedInputType === EventTypeCustomInputType.TEXT || - selectedInputType === EventTypeCustomInputType.TEXTLONG) && ( -
- -
- -
-
- )} -
- - -
- - -
- - -
- - ); -}; - -export default CustomInputTypeForm; diff --git a/apps/web/pages/[user]/book.tsx b/apps/web/pages/[user]/book.tsx index 7cccc1972f..c791ddc1ea 100644 --- a/apps/web/pages/[user]/book.tsx +++ b/apps/web/pages/[user]/book.tsx @@ -12,7 +12,7 @@ import { import { useLocale } from "@calcom/lib/hooks/useLocale"; import { bookEventTypeSelect } from "@calcom/prisma"; import prisma from "@calcom/prisma"; -import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; +import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull"; import getBooking, { GetBookingType } from "@lib/getBooking"; @@ -127,6 +127,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { periodStartDate: e.periodStartDate?.toString() ?? null, periodEndDate: e.periodEndDate?.toString() ?? null, schedulingType: null, + customInputs: customInputSchema.array().parse(e.customInputs || []), users: users.map((u) => ({ id: u.id, name: u.name, diff --git a/apps/web/pages/booking/[uid].tsx b/apps/web/pages/booking/[uid].tsx index 2f783caf76..c934cdf337 100644 --- a/apps/web/pages/booking/[uid].tsx +++ b/apps/web/pages/booking/[uid].tsx @@ -34,7 +34,7 @@ import { getIs24hClockFromLocalStorage, isBrowserLocale24h } from "@calcom/lib/t import { localStorage } from "@calcom/lib/webstorage"; import prisma, { baseUserSelect } from "@calcom/prisma"; import { Prisma } from "@calcom/prisma/client"; -import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; +import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import { Button, EmailInput, Icon } from "@calcom/ui"; import { timeZone } from "@lib/clock"; @@ -456,10 +456,37 @@ export default function Success(props: SuccessProps) { )} {customInputs && Object.keys(customInputs).map((key) => { + // This breaks if you have two label that are the same. + // TODO: Fix this in another PR const customInput = customInputs[key as keyof typeof customInputs]; + const eventTypeCustomFound = eventType.customInputs?.find((ci) => ci.label === key); return ( <> - {customInput !== "" && ( + {eventTypeCustomFound?.type === "RADIO" && ( + <> +
+ {eventTypeCustomFound.label} +
+
+ {eventTypeCustomFound.options && + eventTypeCustomFound.options.map((option) => { + const selected = option.label == customInput; + return ( +
+

{option.label}

+ {option.label === customInput && "✅"} +
+ ); + })} +
+ + )} + {eventTypeCustomFound?.type !== "RADIO" && customInput !== "" && ( <>
{key}
@@ -787,6 +814,7 @@ const getEventTypesFromDB = async (id: number) => { requiresConfirmation: true, userId: true, successRedirectUrl: true, + customInputs: true, locations: true, price: true, currency: true, @@ -947,7 +975,11 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { select: baseUserSelect, }); if (user) { - eventTypeRaw.users.push(user); + eventTypeRaw.users.push({ + ...user, + avatar: "", + allowDynamicBooking: true, + }); } } @@ -963,6 +995,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { periodEndDate: eventTypeRaw.periodEndDate?.toString() ?? null, metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata), recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent), + customInputs: customInputSchema.array().parse(eventTypeRaw.customInputs), }; const profile = { diff --git a/apps/web/pages/d/[link]/book.tsx b/apps/web/pages/d/[link]/book.tsx index 8add273c06..6ad36d1ed0 100644 --- a/apps/web/pages/d/[link]/book.tsx +++ b/apps/web/pages/d/[link]/book.tsx @@ -4,7 +4,7 @@ import { JSONObject } from "superjson/dist/types"; import { parseRecurringEvent } from "@calcom/lib"; import prisma from "@calcom/prisma"; import { bookEventTypeSelect } from "@calcom/prisma/selects"; -import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; +import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull"; import { inferSSRProps } from "@lib/types/inferSSRProps"; @@ -79,6 +79,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { ...e, periodStartDate: e.periodStartDate?.toString() ?? null, periodEndDate: e.periodEndDate?.toString() ?? null, + customInputs: customInputSchema.array().parse(e.customInputs || []), schedulingType: null, users: users.map((u) => ({ id: u.id, diff --git a/apps/web/pages/event-types/[type]/index.tsx b/apps/web/pages/event-types/[type]/index.tsx index 764a1b2806..d2dd047930 100644 --- a/apps/web/pages/event-types/[type]/index.tsx +++ b/apps/web/pages/event-types/[type]/index.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-empty-function */ import { useAutoAnimate } from "@formkit/auto-animate/react"; -import { EventTypeCustomInput, PeriodType, Prisma, SchedulingType } from "@prisma/client"; +import { PeriodType, Prisma, SchedulingType } from "@prisma/client"; import { GetServerSidePropsContext } from "next"; import { useRouter } from "next/router"; import { useState } from "react"; @@ -16,15 +16,15 @@ import convertToNewDurationType from "@calcom/lib/convertToNewDurationType"; import findDurationType from "@calcom/lib/findDurationType"; import getStripeAppData from "@calcom/lib/getStripeAppData"; import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { HttpError } from "@calcom/lib/http-error"; import prisma from "@calcom/prisma"; -import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; +import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import { trpc } from "@calcom/trpc/react"; import type { BookingLimit, RecurringEvent } from "@calcom/types/Calendar"; import { Form, showToast } from "@calcom/ui"; import { asStringOrThrow } from "@lib/asStringOrNull"; import { getSession } from "@lib/auth"; -import { HttpError } from "@lib/core/http/error"; import { inferSSRProps } from "@lib/types/inferSSRProps"; import { AvailabilityTab } from "@components/eventtype/AvailabilityTab"; @@ -63,7 +63,7 @@ export type FormValues = { displayLocationPublicly?: boolean; phone?: string; }[]; - customInputs: EventTypeCustomInput[]; + customInputs: CustomInputParsed[]; users: string[]; schedule: number; periodType: PeriodType; @@ -87,6 +87,8 @@ export type FormValues = { bookingLimits?: BookingLimit; }; +export type CustomInputParsed = typeof customInputSchema._output; + const querySchema = z.object({ tabName: z .enum(["setup", "availability", "apps", "limits", "recurring", "team", "advanced", "workflows"]) @@ -261,6 +263,7 @@ const EventTypePage = (props: inferSSRProps) => { recurringEvent, locations, metadata, + customInputs, // We don't need to send send these values to the backend // eslint-disable-next-line @typescript-eslint/no-unused-vars seatsPerTimeSlotEnabled, @@ -298,6 +301,7 @@ const EventTypePage = (props: inferSSRProps) => { seatsPerTimeSlot, seatsShowAttendees, metadata, + customInputs, }); }}>
@@ -495,6 +499,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => // 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, @@ -502,6 +508,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => bookingLimits: parseBookingLimit(restEventType.bookingLimits), locations: locations as unknown as LocationObject[], metadata: parsedMetaData, + customInputs: parsedCustomInputs, }; // backwards compat diff --git a/apps/web/pages/team/[slug]/book.tsx b/apps/web/pages/team/[slug]/book.tsx index cdeed9c326..e225b75724 100644 --- a/apps/web/pages/team/[slug]/book.tsx +++ b/apps/web/pages/team/[slug]/book.tsx @@ -4,7 +4,7 @@ import { JSONObject } from "superjson/dist/types"; import { LocationObject, privacyFilteredLocations } from "@calcom/app-store/locations"; import { parseRecurringEvent } from "@calcom/lib"; import prisma from "@calcom/prisma"; -import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; +import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull"; import getBooking, { GetBookingType } from "@lib/getBooking"; @@ -96,6 +96,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}), periodStartDate: e.periodStartDate?.toString() ?? null, periodEndDate: e.periodEndDate?.toString() ?? null, + customInputs: customInputSchema.array().parse(e.customInputs || []), users: eventType.users.map((u) => ({ id: u.id, name: u.name, diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 517dd3c31a..5051a76f6e 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1388,5 +1388,9 @@ "test_preview_description": "Test your routing form without submitting any data", "test_routing": "Test Routing", "booking_limit_reached":"Booking Limit for this event type has been reached", - "fill_this_field": "Please fill in this field" + "fill_this_field": "Please fill in this field", + "options": "Options", + "enter_option": "Enter Option {{index}}", + "add_an_option": "Add an option", + "radio": "Radio" } diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 0d7032f306..6951dc36e6 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -42,7 +42,11 @@ import { checkBookingLimits, getLuckyUser } from "@calcom/lib/server"; import { getTranslation } from "@calcom/lib/server/i18n"; import { updateWebUser as syncServicesUpdateWebUser } from "@calcom/lib/sync/SyncServiceManager"; import prisma, { userSelect } from "@calcom/prisma"; -import { EventTypeMetaDataSchema, extendedBookingCreateBody } from "@calcom/prisma/zod-utils"; +import { + customInputSchema, + EventTypeMetaDataSchema, + extendedBookingCreateBody, +} from "@calcom/prisma/zod-utils"; import type { BufferedBusyTime } from "@calcom/types/BufferedBusyTime"; import type { AdditionalInformation, AppsStatus, CalendarEvent } from "@calcom/types/Calendar"; import type { EventResult, PartialReference } from "@calcom/types/EventManager"; @@ -203,6 +207,7 @@ const getEventTypesFromDB = async (eventTypeId: number) => { ...eventType, metadata: EventTypeMetaDataSchema.parse(eventType.metadata), recurringEvent: parseRecurringEvent(eventType.recurringEvent), + customInputs: customInputSchema.array().parse(eventType.customInputs), locations: (eventType.locations ?? []) as LocationObject[], }; }; @@ -310,7 +315,7 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) { const stripeAppData = getStripeAppData(eventType); // Check if required custom inputs exist - handleCustomInputs(eventType.customInputs, reqBody.customInputs); + handleCustomInputs(eventType.customInputs as EventTypeCustomInput[], reqBody.customInputs); let timeOutOfBounds = false; try { diff --git a/packages/lib/defaultEvents.ts b/packages/lib/defaultEvents.ts index 1968bc2c4c..8d33a1a375 100644 --- a/packages/lib/defaultEvents.ts +++ b/packages/lib/defaultEvents.ts @@ -1,10 +1,8 @@ -import type { EventTypeCustomInput } from "@prisma/client"; import { PeriodType, Prisma, SchedulingType, UserPlan } from "@prisma/client"; import { DailyLocationType } from "@calcom/app-store/locations"; import { userSelect } from "@calcom/prisma/selects"; -import { _EventTypeModel } from "@calcom/prisma/zod"; -import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; +import { CustomInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; type User = Prisma.UserGetPayload; @@ -50,7 +48,7 @@ const user: User = { allowDynamicBooking: true, }; -const customInputs: EventTypeCustomInput[] = []; +const customInputs: CustomInputSchema[] = []; const commons = { isDynamic: true, diff --git a/packages/prisma/migrations/20221129112344_adding_radio_custom_input/migration.sql b/packages/prisma/migrations/20221129112344_adding_radio_custom_input/migration.sql new file mode 100644 index 0000000000..d1d3b7c3ed --- /dev/null +++ b/packages/prisma/migrations/20221129112344_adding_radio_custom_input/migration.sql @@ -0,0 +1,5 @@ +-- AlterEnum +ALTER TYPE "EventTypeCustomInputType" ADD VALUE 'radio'; + +-- AlterTable +ALTER TABLE "EventTypeCustomInput" ADD COLUMN "options" JSONB; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 44b6cedbe4..261f11391b 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -347,6 +347,7 @@ enum EventTypeCustomInputType { TEXTLONG @map("textLong") NUMBER @map("number") BOOL @map("bool") + RADIO @map("radio") } model EventTypeCustomInput { @@ -355,6 +356,8 @@ model EventTypeCustomInput { eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade) label String type EventTypeCustomInputType + // @zod.custom(imports.customInputOptionSchema) + options Json? required Boolean placeholder String @default("") } diff --git a/packages/prisma/zod-utils.ts b/packages/prisma/zod-utils.ts index 4841a31f41..d1470c4690 100644 --- a/packages/prisma/zod-utils.ts +++ b/packages/prisma/zod-utils.ts @@ -1,3 +1,4 @@ +import { EventTypeCustomInputType } from "@prisma/client"; import z, { ZodNullable, ZodObject, ZodOptional } from "zod"; /* eslint-disable no-underscore-dangle */ @@ -212,6 +213,25 @@ export const teamMetadataSchema = z .partial() .nullable(); +export const customInputOptionSchema = z.array( + z.object({ + label: z.string(), + type: z.string(), + }) +); + +export const customInputSchema = z.object({ + id: z.number(), + eventTypeId: z.number(), + label: z.string(), + type: z.nativeEnum(EventTypeCustomInputType), + options: customInputOptionSchema.optional().nullable(), + required: z.boolean(), + placeholder: z.string(), +}); + +export type CustomInputSchema = z.infer; + /** * Ensures that it is a valid HTTP URL * It automatically avoids diff --git a/packages/trpc/server/routers/viewer/eventTypes.tsx b/packages/trpc/server/routers/viewer/eventTypes.tsx index 9c27ba1aa6..d5640eef47 100644 --- a/packages/trpc/server/routers/viewer/eventTypes.tsx +++ b/packages/trpc/server/routers/viewer/eventTypes.tsx @@ -11,7 +11,12 @@ import { validateBookingLimitOrder } from "@calcom/lib"; import { CAL_URL } from "@calcom/lib/constants"; import { baseEventTypeSelect, baseUserSelect } from "@calcom/prisma"; import { _DestinationCalendarModel, _EventTypeCustomInputModel, _EventTypeModel } from "@calcom/prisma/zod"; -import { EventTypeMetaDataSchema, stringOrNumber } from "@calcom/prisma/zod-utils"; +import { + customInputSchema, + EventTypeMetaDataSchema, + stringOrNumber, + CustomInputSchema, +} from "@calcom/prisma/zod-utils"; import { createEventTypeInput } from "@calcom/prisma/zod/custom/eventtype"; import { TRPCError } from "@trpc/server"; @@ -30,7 +35,7 @@ function handlePeriodType(periodType: string | undefined): PeriodType | undefine return PeriodType[passedPeriodType]; } -function handleCustomInputs(customInputs: EventTypeCustomInput[], eventTypeId: number) { +function handleCustomInputs(customInputs: CustomInputSchema[], eventTypeId: number) { const cInputsIdsToDelete = customInputs.filter((input) => input.id > 0).map((e) => e.id); const cInputsToCreate = customInputs .filter((input) => input.id < 0) @@ -39,6 +44,7 @@ function handleCustomInputs(customInputs: EventTypeCustomInput[], eventTypeId: n label: input.label, required: input.required, placeholder: input.placeholder, + options: input.options || undefined, })); const cInputsToUpdate = customInputs .filter((input) => input.id > 0) @@ -48,6 +54,7 @@ function handleCustomInputs(customInputs: EventTypeCustomInput[], eventTypeId: n label: input.label, required: input.required, placeholder: input.placeholder, + options: input.options || undefined, }, where: { id: input.id, @@ -71,7 +78,7 @@ function handleCustomInputs(customInputs: EventTypeCustomInput[], eventTypeId: n const EventTypeUpdateInput = _EventTypeModel /** Optional fields */ .extend({ - customInputs: z.array(_EventTypeCustomInputModel), + customInputs: z.array(customInputSchema).optional(), destinationCalendar: _DestinationCalendarModel.pick({ integration: true, externalId: true, diff --git a/packages/ui/index.tsx b/packages/ui/index.tsx index 8e1662a407..6830c765da 100644 --- a/packages/ui/index.tsx +++ b/packages/ui/index.tsx @@ -86,7 +86,7 @@ export { DropdownMenuSeparator, DropdownMenuTrigger, } from "./v2/core/Dropdown"; -export { RadioGroup } from "./v2/core/form"; +export { RadioGroup, Radio, Group, RadioField } from "./v2/core/form"; export { BooleanToggleGroupField } from "./v2/core/form/BooleanToggleGroup"; export { DateRangePickerLazy as DateRangePicker } from "./v2/core/form/date-range-picker"; export { default as DatePickerField } from "./v2/core/form/DatePicker"; diff --git a/packages/ui/v2/core/form/index.ts b/packages/ui/v2/core/form/index.ts index 93e21c7c8d..81bdd04ac4 100644 --- a/packages/ui/v2/core/form/index.ts +++ b/packages/ui/v2/core/form/index.ts @@ -1,4 +1,10 @@ -export { RadioGroup, /* TODO: solve this conflict -> Select, */ Radio } from "./radio-area"; +export { + RadioGroup, + /* TODO: solve this conflict -> Select, */ + Radio, + Group, + RadioField, +} from "./radio-area"; export { default as Checkbox } from "../../../components/form/checkbox/Checkbox"; export { default as DatePicker } from "./DatePicker"; export { default as Select, SelectWithValidation, SelectField, getReactSelectProps } from "./select"; diff --git a/packages/ui/v2/core/form/radio-area/Radio.tsx b/packages/ui/v2/core/form/radio-area/Radio.tsx index 4f7351f9ce..8b892c900b 100644 --- a/packages/ui/v2/core/form/radio-area/Radio.tsx +++ b/packages/ui/v2/core/form/radio-area/Radio.tsx @@ -29,7 +29,7 @@ export const Label = (props: JSX.IntrinsicElements["label"] & { disabled?: boole