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:
parent
7f461bc275
commit
3ab002e547
|
@ -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 && (
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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={() => {
|
||||
|
|
|
@ -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;
|
|
@ -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,
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
-- AlterEnum
|
||||
ALTER TYPE "EventTypeCustomInputType" ADD VALUE 'radio';
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "EventTypeCustomInput" ADD COLUMN "options" JSONB;
|
|
@ -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("")
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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"
|
||||
)}
|
||||
/>
|
||||
|
|
|
@ -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";
|
||||
|
|
Loading…
Reference in New Issue
Block a user