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 <alex@cal.com>
Co-authored-by: Jeroen Reumkens <hello@jeroenreumkens.nl>

* 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 <alex@cal.com>
Co-authored-by: Jeroen Reumkens <hello@jeroenreumkens.nl>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: alannnc <alannnc@gmail.com>
This commit is contained in:
sean-brydon 2022-12-01 21:53:52 +00:00 committed by GitHub
parent 7f461bc275
commit 3ab002e547
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 259 additions and 184 deletions

View File

@ -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 = ({
</div>
</div>
)}
{input.options && input.type === EventTypeCustomInputType.RADIO && (
<div className="">
<div className="flex">
<Group
onValueChange={(e) => {
bookingForm.setValue(`customInputs.${input.id}`, e);
}}>
<>
{input.options.map((option, i) => (
<RadioField
label={option.label}
key={`option.${i}.radio`}
value={option.label}
id={`option.${i}.radio`}
/>
))}
</>
</Group>
</div>
</div>
)}
</div>
))}
{!eventType.disableGuests && guestToggle && (

View File

@ -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<IFormInput>;
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> = (props) => {
const { t } = useLocale();
@ -26,10 +36,22 @@ const CustomInputTypeForm: FC<Props> = (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<IFormInput>({
const defaultValues = selectedCustomInput
? { ...selectedCustomInput, id: selectedCustomInput?.id || randomId() }
: {
id: randomId(),
type: EventTypeCustomInputType.TEXT,
};
const { register, control, getValues } = useForm<IFormInput>({
defaultValues,
});
const selectedInputType = useWatch({ name: "type", control });
@ -40,7 +62,7 @@ const CustomInputTypeForm: FC<Props> = (props) => {
};
return (
<form className="flex flex-col space-y-4">
<div className="flex flex-col space-y-4">
<div>
<label htmlFor="type" className="block text-sm font-medium text-gray-700">
{t("input_type")}
@ -83,6 +105,10 @@ const CustomInputTypeForm: FC<Props> = (props) => {
{...register("placeholder")}
/>
)}
{selectedInputType === EventTypeCustomInputType.RADIO && (
<RadioInputHandler control={control} register={register} />
)}
<div className="flex h-5 items-center">
<input
id="required"
@ -111,12 +137,75 @@ const CustomInputTypeForm: FC<Props> = (props) => {
<Button onClick={onCancel} type="button" color="secondary" className="ltr:mr-2">
{t("cancel")}
</Button>
<Button onClick={handleSubmit(props.onSubmit)} form="custom-input">
<Button
type="button"
onClick={() => {
props.onSubmit(getValues());
}}>
{t("save")}
</Button>
</div>
</form>
</div>
);
};
function RadioInputHandler({
register,
control,
}: {
register: UseFormRegister<IFormInput>;
control: Control<IFormInput>;
}) {
const { t } = useLocale();
const { fields, append, remove } = useFieldArray<IFormInput>({
control,
name: "options",
shouldUnregister: true,
});
const [animateRef] = useAutoAnimate<HTMLUListElement>();
return (
<div className="flex flex-col ">
<Label htmlFor="radio_options">{t("options")}</Label>
<ul
className="flex max-h-80 w-full flex-col space-y-1 overflow-y-scroll rounded-md bg-gray-50 p-4"
ref={animateRef}>
<>
{fields.map((option, index) => (
<li key={`${option.id}`}>
<TextField
id={option.id}
placeholder={t("enter_option", { index: index + 1 })}
addOnFilled={false}
label={t("option", { index: index + 1 })}
labelSrOnly
{...register(`options.${index}.label` as const, { required: true })}
addOnSuffix={
<Button
size="icon"
color="minimal"
StartIcon={Icon.FiX}
onClick={() => {
remove(index);
}}
/>
}
/>
</li>
))}
<Button
color="minimal"
StartIcon={Icon.FiPlus}
className="!text-sm !font-medium"
onClick={() => {
append({ label: "", type: "text" });
}}>
{t("add_an_option")}
</Button>
</>
</ul>
</div>
);
}
export default CustomInputTypeForm;

View File

@ -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<EventTypeSetupInfered
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink);
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!eventType.successRedirectUrl);
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
const [customInputs, setCustomInputs] = useState<EventTypeCustomInput[]>(
const [customInputs, setCustomInputs] = useState<CustomInputParsed[]>(
eventType.customInputs.sort((a, b) => a.id - b.id) || []
);
const [selectedCustomInput, setSelectedCustomInput] = useState<EventTypeCustomInput | undefined>(undefined);
const [selectedCustomInput, setSelectedCustomInput] = useState<CustomInputParsed | undefined>(undefined);
const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false);
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
@ -125,15 +124,13 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
onCheckedChange={(e) => {
if (e && customInputs.length === 0) {
// Push a placeholders
setSelectedCustomInput(undefined);
setSelectedCustomInputModalOpen(true);
} else if (!e) {
setCustomInputs([]);
formMethods.setValue("customInputs", []);
}
}}>
<ul className="my-4 rounded-md border">
{customInputs.map((customInput: EventTypeCustomInput, idx: number) => (
{customInputs.map((customInput, idx) => (
<CustomInputItem
key={idx}
question={customInput.label}
@ -391,24 +388,36 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
<CustomInputTypeForm
selectedCustomInput={selectedCustomInput}
onSubmit={(values) => {
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={() => {

View File

@ -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<IFormInput>;
onCancel: () => void;
selectedCustomInput?: EventTypeCustomInput;
}
type IFormInput = EventTypeCustomInput;
const CustomInputTypeForm: FC<Props> = (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<IFormInput>({
defaultValues,
});
const selectedInputType = useWatch({ name: "type", control });
const selectedInputOption = inputOptions.find((e) => selectedInputType === e.value);
const onCancel = () => {
props.onCancel();
};
return (
<form onSubmit={handleSubmit(props.onSubmit)}>
<div className="mb-2">
<label htmlFor="type" className="block text-sm font-medium text-gray-700">
{t("input_type")}
</label>
<Controller
name="type"
control={control}
render={({ field }) => (
<Select
id="type"
defaultValue={selectedInputOption}
options={inputOptions}
isSearchable={false}
className="mt-1 mb-2 block w-full min-w-0 flex-1 text-sm"
onChange={(option) => option && field.onChange(option.value)}
value={selectedInputOption}
onBlur={field.onBlur}
name={field.name}
/>
)}
/>
</div>
<div className="mb-2">
<label htmlFor="label" className="block text-sm font-medium text-gray-700">
{t("label")}
</label>
<div className="mt-1">
<input
type="text"
id="label"
required
className="block w-full rounded-sm border-gray-300 text-sm"
defaultValue={selectedCustomInput?.label}
{...register("label", { required: true })}
/>
</div>
</div>
{(selectedInputType === EventTypeCustomInputType.TEXT ||
selectedInputType === EventTypeCustomInputType.TEXTLONG) && (
<div className="mb-2">
<label htmlFor="placeholder" className="block text-sm font-medium text-gray-700">
{t("placeholder")}
</label>
<div className="mt-1">
<input
type="text"
id="placeholder"
className="block w-full rounded-sm border-gray-300 text-sm"
defaultValue={selectedCustomInput?.placeholder}
{...register("placeholder")}
/>
</div>
</div>
)}
<div className="flex h-5 items-center">
<input
id="required"
type="checkbox"
className="text-primary-600 focus:ring-primary-500 h-4 w-4 rounded border-gray-300 ltr:mr-2 rtl:ml-2"
defaultChecked={selectedCustomInput?.required ?? true}
{...register("required")}
/>
<label htmlFor="required" className="block text-sm font-medium text-gray-700">
{t("is_required")}
</label>
</div>
<input
type="hidden"
id="eventTypeId"
value={selectedCustomInput?.eventTypeId || -1}
{...register("eventTypeId", { valueAsNumber: true })}
/>
<input
type="hidden"
id="id"
value={selectedCustomInput?.id || -1}
{...register("id", { valueAsNumber: true })}
/>
<div className="mt-5 flex space-x-2 sm:mt-4">
<Button onClick={onCancel} type="button" color="secondary" className="ltr:mr-2">
{t("cancel")}
</Button>
<Button type="submit">{t("save")}</Button>
</div>
</form>
);
};
export default CustomInputTypeForm;

View File

@ -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,

View File

@ -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" && (
<>
<div className="col-span-3 mt-8 border-t pt-8 pr-3 font-medium">
{eventTypeCustomFound.label}
</div>
<div className="col-span-3 mt-1 mb-2">
{eventTypeCustomFound.options &&
eventTypeCustomFound.options.map((option) => {
const selected = option.label == customInput;
return (
<div
key={option.label}
className={classNames(
"flex space-x-1",
!selected && "text-gray-500"
)}>
<p>{option.label}</p>
<span>{option.label === customInput && "✅"}</span>
</div>
);
})}
</div>
</>
)}
{eventTypeCustomFound?.type !== "RADIO" && customInput !== "" && (
<>
<div className="col-span-3 mt-8 border-t pt-8 pr-3 font-medium">{key}</div>
<div className="col-span-3 mt-2 mb-2">
@ -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 = {

View File

@ -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,

View File

@ -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<typeof getServerSideProps>) => {
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<typeof getServerSideProps>) => {
seatsPerTimeSlot,
seatsShowAttendees,
metadata,
customInputs,
});
}}>
<div ref={animationParentRef} className="space-y-6">
@ -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

View File

@ -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,

View File

@ -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"
}

View File

@ -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 {

View File

@ -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<typeof userSelect>;
@ -50,7 +48,7 @@ const user: User = {
allowDynamicBooking: true,
};
const customInputs: EventTypeCustomInput[] = [];
const customInputs: CustomInputSchema[] = [];
const commons = {
isDynamic: true,

View File

@ -0,0 +1,5 @@
-- AlterEnum
ALTER TYPE "EventTypeCustomInputType" ADD VALUE 'radio';
-- AlterTable
ALTER TABLE "EventTypeCustomInput" ADD COLUMN "options" JSONB;

View File

@ -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("")
}

View File

@ -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<typeof customInputSchema>;
/**
* Ensures that it is a valid HTTP URL
* It automatically avoids

View File

@ -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,

View File

@ -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";

View File

@ -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";

View File

@ -29,7 +29,7 @@ export const Label = (props: JSX.IntrinsicElements["label"] & { disabled?: boole
<label
{...props}
className={classNames(
"ml-2 text-sm font-medium leading-5 text-gray-900",
"ml-2 text-sm font-medium leading-5 text-gray-900 dark:text-white",
props.disabled && "text-gray-500"
)}
/>

View File

@ -1,3 +1,3 @@
export * as RadioGroup from "./RadioAreaGroup";
export { default as Select } from "./Select";
export * as Radio from "./Radio";
export { Group, Indicator, Label, Radio, RadioField } from "./Radio";