Skip to content

Commit

Permalink
5444 cal 339 radio option in additional questions on public booking p…
Browse files Browse the repository at this point in the history
…age (calcom#5804)

* Use field array intro

* WIP - form submitting wrong form

* WIP with fake useFormHook

* WORKING! OMG

Co-authored-by: Alex <[email protected]>
Co-authored-by: Jeroen Reumkens <[email protected]>

* 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 <[email protected]>
Co-authored-by: Jeroen Reumkens <[email protected]>
Co-authored-by: Alex van Andel <[email protected]>
Co-authored-by: alannnc <[email protected]>
  • Loading branch information
5 people authored Dec 1, 2022
1 parent 7f461bc commit 3ab002e
Show file tree
Hide file tree
Showing 20 changed files with 259 additions and 184 deletions.
24 changes: 22 additions & 2 deletions apps/web/components/booking/pages/BookingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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))) {
Expand Down Expand Up @@ -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 && (
Expand Down
115 changes: 102 additions & 13 deletions apps/web/components/eventtype/CustomInputTypeForm.tsx
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -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 });
Expand All @@ -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")}
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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;
31 changes: 20 additions & 11 deletions apps/web/components/eventtype/EventAdvancedTab.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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}`;

Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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={() => {
Expand Down
Loading

0 comments on commit 3ab002e

Please sign in to comment.