* Reusable component * Fix limits not being toggleable * Remove custom input margin * addTestId * Limits+adv * Reccuring Tab * Remove console .log Co-authored-by: Omar López <zomars@me.com>
This commit is contained in:
co-authored by
Omar López
parent
5305f31266
commit
b18eabc6e8
@@ -18,6 +18,7 @@ import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
Label,
|
||||
SettingsToggle,
|
||||
showToast,
|
||||
Skeleton,
|
||||
Switch,
|
||||
@@ -43,7 +44,6 @@ 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 [seatsInputVisible, setSeatsInputVisible] = useState(!!eventType.seatsPerTimeSlot);
|
||||
const [customInputs, setCustomInputs] = useState<EventTypeCustomInput[]>(
|
||||
eventType.customInputs.sort((a, b) => a.id - b.id) || []
|
||||
);
|
||||
@@ -53,8 +53,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
||||
|
||||
const seatsEnabled = formMethods.getValues("seatsPerTimeSlotEnabled");
|
||||
|
||||
const [animationRef] = useAutoAnimate<HTMLUListElement>();
|
||||
|
||||
const removeCustom = (index: number) => {
|
||||
formMethods.getValues("customInputs").splice(index, 1);
|
||||
customInputs.splice(index, 1);
|
||||
@@ -120,80 +118,61 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
||||
</div>
|
||||
<hr />
|
||||
<div className="">
|
||||
<div className="flex space-x-3 ">
|
||||
<Switch
|
||||
checked={customInputs.length > 0}
|
||||
fitToHeight={true}
|
||||
onCheckedChange={(e) => {
|
||||
if (e && customInputs.length === 0) {
|
||||
// Push a placeholders
|
||||
setSelectedCustomInput(undefined);
|
||||
setSelectedCustomInputModalOpen(true);
|
||||
} else if (!e) {
|
||||
setCustomInputs([]);
|
||||
formMethods.setValue("customInputs", []);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<Skeleton as={Label} className="text-sm font-semibold leading-none text-black">
|
||||
{t("additional_inputs")}
|
||||
</Skeleton>
|
||||
<Skeleton as="p" className="-mt-2 text-sm leading-normal text-gray-600">
|
||||
{t("additional_input_description")}
|
||||
</Skeleton>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="my-4" ref={animationRef}>
|
||||
{customInputs.map((customInput: EventTypeCustomInput, idx: number) => (
|
||||
<CustomInputItem
|
||||
key={idx}
|
||||
question={customInput.label}
|
||||
type={customInput.type}
|
||||
required={customInput.required}
|
||||
editOnClick={() => {
|
||||
setSelectedCustomInput(customInput);
|
||||
setSelectedCustomInputModalOpen(true);
|
||||
}}
|
||||
deleteOnClick={() => removeCustom(idx)}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
{customInputs.length > 0 && (
|
||||
<Button
|
||||
StartIcon={Icon.FiPlus}
|
||||
color="minimal"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
<SettingsToggle
|
||||
title={t("additional_inputs")}
|
||||
description={t("additional_input_description")}
|
||||
checked={customInputs.length > 0}
|
||||
onCheckedChange={(e) => {
|
||||
if (e && customInputs.length === 0) {
|
||||
// Push a placeholders
|
||||
setSelectedCustomInput(undefined);
|
||||
setSelectedCustomInputModalOpen(true);
|
||||
}}>
|
||||
Add an input
|
||||
</Button>
|
||||
)}
|
||||
} else if (!e) {
|
||||
setCustomInputs([]);
|
||||
formMethods.setValue("customInputs", []);
|
||||
}
|
||||
}}>
|
||||
<ul className="my-4">
|
||||
{customInputs.map((customInput: EventTypeCustomInput, idx: number) => (
|
||||
<CustomInputItem
|
||||
key={idx}
|
||||
question={customInput.label}
|
||||
type={customInput.type}
|
||||
required={customInput.required}
|
||||
editOnClick={() => {
|
||||
setSelectedCustomInput(customInput);
|
||||
setSelectedCustomInputModalOpen(true);
|
||||
}}
|
||||
deleteOnClick={() => removeCustom(idx)}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
{customInputs.length > 0 && (
|
||||
<Button
|
||||
StartIcon={Icon.FiPlus}
|
||||
color="minimal"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSelectedCustomInput(undefined);
|
||||
setSelectedCustomInputModalOpen(true);
|
||||
}}>
|
||||
Add an input
|
||||
</Button>
|
||||
)}
|
||||
</SettingsToggle>
|
||||
</div>
|
||||
<hr />
|
||||
<Controller
|
||||
name="requiresConfirmation"
|
||||
defaultValue={eventType.requiresConfirmation}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="flex space-x-3">
|
||||
<Switch
|
||||
name="requireConfirmation"
|
||||
checked={value}
|
||||
onCheckedChange={(e) => onChange(e)}
|
||||
disabled={seatsEnabled}
|
||||
fitToHeight={true}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<Skeleton as={Label} className="text-sm font-semibold leading-none text-black">
|
||||
{t("requires_confirmation")}
|
||||
</Skeleton>
|
||||
<Skeleton as="p" className="-mt-2 text-sm leading-normal text-gray-600">
|
||||
{t("requires_confirmation_description")}
|
||||
</Skeleton>
|
||||
</div>
|
||||
</div>
|
||||
<SettingsToggle
|
||||
title={t("requires_confirmation")}
|
||||
description={t("requires_confirmation_description")}
|
||||
checked={value}
|
||||
onCheckedChange={(e) => onChange(e)}
|
||||
disabled={seatsEnabled}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<hr />
|
||||
@@ -202,23 +181,13 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
||||
control={formMethods.control}
|
||||
defaultValue={eventType.disableGuests}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="flex space-x-3 ">
|
||||
<Switch
|
||||
name="disableGuests"
|
||||
fitToHeight={true}
|
||||
checked={value}
|
||||
onCheckedChange={(e) => onChange(e)}
|
||||
disabled={seatsEnabled}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<Skeleton as={Label} className="text-sm font-semibold leading-none text-black">
|
||||
{t("disable_guests")}
|
||||
</Skeleton>
|
||||
<Skeleton as="p" className="-mt-2 text-sm leading-normal text-gray-600">
|
||||
{t("disable_guests_description")}
|
||||
</Skeleton>
|
||||
</div>
|
||||
</div>
|
||||
<SettingsToggle
|
||||
title={t("disable_guests")}
|
||||
description={t("disable_guests_description")}
|
||||
checked={value}
|
||||
onCheckedChange={(e) => onChange(e)}
|
||||
disabled={seatsEnabled}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -228,22 +197,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
||||
control={formMethods.control}
|
||||
defaultValue={eventType.hideCalendarNotes}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="flex space-x-3 ">
|
||||
<Switch
|
||||
name="hideCalendarNotes"
|
||||
fitToHeight={true}
|
||||
checked={value}
|
||||
onCheckedChange={(e) => onChange(e)}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<Skeleton as={Label} className="text-sm font-semibold leading-none text-black">
|
||||
{t("disable_notes")}
|
||||
</Skeleton>
|
||||
<Skeleton as="p" className="-mt-2 text-sm leading-normal text-gray-600">
|
||||
{t("disable_notes_description")}
|
||||
</Skeleton>
|
||||
</div>
|
||||
</div>
|
||||
<SettingsToggle
|
||||
title={t("disable_notes")}
|
||||
description={t("disable_notes_description")}
|
||||
checked={value}
|
||||
onCheckedChange={(e) => onChange(e)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<hr />
|
||||
@@ -253,20 +212,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
||||
defaultValue={!!eventType.metadata.additionalNotesRequired}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="flex space-x-3 ">
|
||||
<Switch
|
||||
name="additionalNotesRequired"
|
||||
fitToHeight={true}
|
||||
checked={value}
|
||||
<SettingsToggle
|
||||
title={t("require_additional_notes")}
|
||||
description={t("require_additional_notes_description")}
|
||||
checked={!!value}
|
||||
onCheckedChange={(e) => onChange(e)}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<Skeleton as={Label} className="text-sm font-semibold leading-none text-black">
|
||||
{t("require_additional_notes")}
|
||||
</Skeleton>
|
||||
<Skeleton as="p" className="-mt-2 text-sm leading-normal text-gray-600">
|
||||
{t("require_additional_notes_description")}
|
||||
</Skeleton>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
@@ -276,29 +227,19 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
||||
control={formMethods.control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<>
|
||||
<div className="flex space-x-3 ">
|
||||
<Switch
|
||||
name="successRedirectUrlCheck"
|
||||
fitToHeight={true}
|
||||
defaultChecked={redirectUrlVisible}
|
||||
onCheckedChange={(e) => {
|
||||
setRedirectUrlVisible(e);
|
||||
onChange(e ? value : "");
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<Skeleton as={Label} className="text-sm font-semibold leading-none text-black">
|
||||
{t("redirect_success_booking")}
|
||||
</Skeleton>
|
||||
<Skeleton as="p" className="-mt-2 text-sm leading-normal text-gray-600">
|
||||
{t("redirect_url_description")}
|
||||
</Skeleton>
|
||||
</div>
|
||||
</div>
|
||||
{redirectUrlVisible && (
|
||||
<div className="">
|
||||
<SettingsToggle
|
||||
title={t("redirect_success_booking")}
|
||||
description={t("redirect_url_description")}
|
||||
checked={redirectUrlVisible}
|
||||
onCheckedChange={(e) => {
|
||||
setRedirectUrlVisible(e);
|
||||
onChange(e ? value : "");
|
||||
}}>
|
||||
{/* Textfield has some margin by default we remove that so we can keep consitant aligment */}
|
||||
<div className="lg:-ml-2">
|
||||
<TextField
|
||||
label={t("redirect_success_booking")}
|
||||
labelSrOnly
|
||||
placeholder={t("external_redirect_url")}
|
||||
required={redirectUrlVisible}
|
||||
type="text"
|
||||
@@ -306,137 +247,107 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
||||
{...formMethods.register("successRedirectUrl")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</SettingsToggle>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<hr />
|
||||
<Controller
|
||||
name="hashedLink"
|
||||
control={formMethods.control}
|
||||
defaultValue={hashedUrl}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<>
|
||||
<div className="flex space-x-3 ">
|
||||
<Switch
|
||||
data-testid="hashedLinkCheck"
|
||||
name="hashedLinkCheck"
|
||||
fitToHeight={true}
|
||||
defaultChecked={!!value}
|
||||
onCheckedChange={(e) => {
|
||||
setHashedLinkVisible(e);
|
||||
onChange(e ? hashedUrl : undefined);
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<Skeleton as={Label} className="text-sm font-semibold leading-none text-black">
|
||||
{t("private_link")}
|
||||
</Skeleton>
|
||||
<Skeleton as="p" className="-mt-2 text-sm leading-normal text-gray-600">
|
||||
{t("private_link_description")}
|
||||
</Skeleton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hashedLinkVisible && (
|
||||
<div className="">
|
||||
<TextField
|
||||
disabled
|
||||
name="hashedLink"
|
||||
label={t("private_link_label")}
|
||||
data-testid="generated-hash-url"
|
||||
type="text"
|
||||
hint={t("private_link_hint")}
|
||||
defaultValue={placeholderHashedLink}
|
||||
addOnSuffix={
|
||||
<Tooltip
|
||||
content={eventType.hashedLink ? t("copy_to_clipboard") : t("enabled_after_update")}>
|
||||
<Button
|
||||
color="minimal"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(placeholderHashedLink);
|
||||
if (eventType.hashedLink) {
|
||||
showToast(t("private_link_copied"), "success");
|
||||
} else {
|
||||
showToast(t("enabled_after_update_description"), "warning");
|
||||
}
|
||||
}}
|
||||
className="hover:stroke-3 hover:bg-transparent hover:text-black"
|
||||
type="button">
|
||||
<Icon.FiCopy />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<SettingsToggle
|
||||
data-testid="hashedLinkCheck"
|
||||
title={t("private_link")}
|
||||
description={t("private_link_description")}
|
||||
checked={hashedLinkVisible}
|
||||
onCheckedChange={(e) => {
|
||||
formMethods.setValue("hashedLink", e ? hashedUrl : undefined);
|
||||
setHashedLinkVisible(e);
|
||||
}}>
|
||||
{/* Textfield has some margin by default we remove that so we can keep consitant aligment */}
|
||||
<div className="lg:-ml-2">
|
||||
<TextField
|
||||
disabled
|
||||
name="hashedLink"
|
||||
label={t("private_link_label")}
|
||||
data-testid="generated-hash-url"
|
||||
labelSrOnly
|
||||
type="text"
|
||||
hint={t("private_link_hint")}
|
||||
defaultValue={placeholderHashedLink}
|
||||
addOnSuffix={
|
||||
<Tooltip content={eventType.hashedLink ? t("copy_to_clipboard") : t("enabled_after_update")}>
|
||||
<Button
|
||||
color="minimal"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(placeholderHashedLink);
|
||||
if (eventType.hashedLink) {
|
||||
showToast(t("private_link_copied"), "success");
|
||||
} else {
|
||||
showToast(t("enabled_after_update_description"), "warning");
|
||||
}
|
||||
}}
|
||||
className="hover:stroke-3 hover:bg-transparent hover:text-black"
|
||||
type="button">
|
||||
<Icon.FiCopy />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</SettingsToggle>
|
||||
<hr />
|
||||
<Controller
|
||||
name="seatsPerTimeSlotEnabled"
|
||||
control={formMethods.control}
|
||||
defaultValue={!!eventType.seatsPerTimeSlot}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="flex space-x-3">
|
||||
<Switch
|
||||
name="seatsPerTimeSlotEnabled"
|
||||
checked={value}
|
||||
onCheckedChange={(e) => {
|
||||
setSeatsInputVisible(e);
|
||||
// Enabling seats will disable guests and requiring confimation until fully supported
|
||||
if (e) {
|
||||
formMethods.setValue("disableGuests", true);
|
||||
formMethods.setValue("requiresConfirmation", false);
|
||||
formMethods.setValue("seatsPerTimeSlot", 2);
|
||||
} else {
|
||||
formMethods.setValue("seatsPerTimeSlot", null);
|
||||
}
|
||||
onChange(e);
|
||||
}}
|
||||
fitToHeight={true}
|
||||
<SettingsToggle
|
||||
title={t("offer_seats")}
|
||||
description={t("offer_seats_description")}
|
||||
checked={value}
|
||||
onCheckedChange={(e) => {
|
||||
// Enabling seats will disable guests and requiring confimation until fully supported
|
||||
if (e) {
|
||||
formMethods.setValue("disableGuests", true);
|
||||
formMethods.setValue("requiresConfirmation", false);
|
||||
formMethods.setValue("seatsPerTimeSlot", 2);
|
||||
} else {
|
||||
formMethods.setValue("seatsPerTimeSlot", null);
|
||||
formMethods.setValue("disableGuests", false);
|
||||
}
|
||||
onChange(e);
|
||||
}}>
|
||||
<Controller
|
||||
name="seatsPerTimeSlot"
|
||||
control={formMethods.control}
|
||||
defaultValue={eventType.seatsPerTimeSlot}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="lg:-ml-2">
|
||||
<TextField
|
||||
required
|
||||
name="seatsPerTimeSlot"
|
||||
labelSrOnly
|
||||
label={t("number_of_seats")}
|
||||
type="number"
|
||||
defaultValue={value || 2}
|
||||
addOnSuffix={<>{t("seats")}</>}
|
||||
onChange={(e) => {
|
||||
onChange(Number(e.target.value));
|
||||
}}
|
||||
/>
|
||||
<div className="mt-2">
|
||||
<CheckboxField
|
||||
description={t("show_attendees")}
|
||||
onChange={(e) => formMethods.setValue("seatsShowAttendees", e.target.checked)}
|
||||
defaultChecked={!!eventType.seatsShowAttendees}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<Skeleton as={Label} className="text-sm font-semibold leading-none text-black">
|
||||
{t("offer_seats")}
|
||||
</Skeleton>
|
||||
<Skeleton as="p" className="-mt-2 text-sm leading-normal text-gray-600">
|
||||
{t("offer_seats_description")}
|
||||
</Skeleton>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsToggle>
|
||||
)}
|
||||
/>
|
||||
{seatsInputVisible && (
|
||||
<Controller
|
||||
name="seatsPerTimeSlot"
|
||||
control={formMethods.control}
|
||||
defaultValue={eventType.seatsPerTimeSlot}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="">
|
||||
<TextField
|
||||
required
|
||||
name="seatsPerTimeSlot"
|
||||
label={t("number_of_seats")}
|
||||
type="number"
|
||||
defaultValue={value || 2}
|
||||
addOnSuffix={<>{t("seats")}</>}
|
||||
onChange={(e) => {
|
||||
onChange(Number(e.target.value));
|
||||
}}
|
||||
/>
|
||||
<div className="mt-6">
|
||||
<CheckboxField
|
||||
description={t("show_attendees")}
|
||||
onChange={(e) => formMethods.setValue("seatsShowAttendees", e.target.checked)}
|
||||
defaultChecked={!!eventType.seatsShowAttendees}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showEventNameTip && (
|
||||
<Dialog open={showEventNameTip} onOpenChange={setShowEventNameTip}>
|
||||
<DialogContent
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { PeriodType } from "@calcom/prisma/client";
|
||||
import type { BookingLimit } from "@calcom/types/Calendar";
|
||||
import { Icon } from "@calcom/ui";
|
||||
import { Select, Switch, Label, Input, MinutesField, Button } from "@calcom/ui/v2";
|
||||
import { Select, Switch, Label, Input, MinutesField, Button, SettingsToggle } from "@calcom/ui/v2";
|
||||
import DateRangePicker from "@calcom/ui/v2/core/form/date-range-picker/DateRangePicker";
|
||||
|
||||
export const EventLimitsTab = (props: Pick<EventTypeSetupInfered, "eventType">) => {
|
||||
@@ -159,149 +159,106 @@ export const EventLimitsTab = (props: Pick<EventTypeSetupInfered, "eventType">)
|
||||
|
||||
<hr className="my-8" />
|
||||
|
||||
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
|
||||
<fieldset className="block flex-col sm:flex">
|
||||
<div className="flex space-x-3">
|
||||
<Controller
|
||||
name="bookingLimits"
|
||||
control={formMethods.control}
|
||||
render={({ field: { value } }) => (
|
||||
<Switch
|
||||
fitToHeight={true}
|
||||
checked={Object.keys(value ?? {}).length > 0}
|
||||
onCheckedChange={(active) => {
|
||||
if (active) {
|
||||
formMethods.setValue("bookingLimits", {
|
||||
PER_DAY: 1,
|
||||
});
|
||||
} else {
|
||||
formMethods.setValue("bookingLimits", undefined);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="">
|
||||
<Label className="text-sm font-semibold leading-none text-black">
|
||||
{t("limit_booking_frequency")}
|
||||
</Label>
|
||||
<p className="-mt-2 text-sm leading-normal text-gray-600">
|
||||
{t("limit_booking_frequency_description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 lg:ml-14">
|
||||
<Controller
|
||||
name="bookingLimits"
|
||||
control={formMethods.control}
|
||||
render={({ field: { value } }) => (
|
||||
<SettingsToggle
|
||||
title={t("limit_booking_frequency")}
|
||||
description={t("limit_booking_frequency_description")}
|
||||
checked={Object.keys(value ?? {}).length > 0}
|
||||
onCheckedChange={(active) => {
|
||||
if (active) {
|
||||
formMethods.setValue("bookingLimits", {
|
||||
PER_DAY: 1,
|
||||
});
|
||||
} else {
|
||||
formMethods.setValue("bookingLimits", {});
|
||||
}
|
||||
}}>
|
||||
<BookingLimits />
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</SettingsToggle>
|
||||
)}
|
||||
/>
|
||||
|
||||
<hr className="my-8" />
|
||||
|
||||
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
|
||||
<fieldset className="block flex-col sm:flex">
|
||||
<div className="flex space-x-3">
|
||||
<Controller
|
||||
name="periodType"
|
||||
control={formMethods.control}
|
||||
defaultValue={periodType?.type !== "UNLIMITED" ? "ROLLING" : "UNLIMITED"}
|
||||
render={({ field: { value } }) => (
|
||||
<Switch
|
||||
fitToHeight={true}
|
||||
checked={value !== "UNLIMITED"}
|
||||
onCheckedChange={(bool) =>
|
||||
formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED")
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="">
|
||||
<Label className="text-sm font-semibold leading-none text-black">Limit Future Bookings</Label>
|
||||
<p className="-mt-2 text-sm leading-normal text-gray-600">
|
||||
Limit how far in the future people can book a time
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 lg:ml-14">
|
||||
<Controller
|
||||
name="periodType"
|
||||
control={formMethods.control}
|
||||
defaultValue={periodType?.type}
|
||||
render={() => (
|
||||
<RadioGroup.Root
|
||||
defaultValue={watchPeriodType}
|
||||
value={watchPeriodType}
|
||||
onValueChange={(val) => formMethods.setValue("periodType", val as PeriodType)}>
|
||||
{PERIOD_TYPES.map((period) => {
|
||||
if (period.type === "UNLIMITED") return null;
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"mb-2 flex flex-wrap items-center text-sm",
|
||||
watchPeriodType === "UNLIMITED" && "pointer-events-none opacity-30"
|
||||
)}
|
||||
key={period.type}>
|
||||
<RadioGroup.Item
|
||||
id={period.type}
|
||||
value={period.type}
|
||||
className="min-w-4 flex h-4 w-4 cursor-pointer items-center rounded-full border border-black bg-white focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
|
||||
<RadioGroup.Indicator className="relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full after:bg-black" />
|
||||
</RadioGroup.Item>
|
||||
{period.prefix ? <span>{period.prefix} </span> : null}
|
||||
{period.type === "ROLLING" && (
|
||||
<div className="flex h-9">
|
||||
<Input
|
||||
type="number"
|
||||
className="block w-16 rounded-md border-gray-300 py-3 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
|
||||
placeholder="30"
|
||||
{...formMethods.register("periodDays", { valueAsNumber: true })}
|
||||
defaultValue={eventType.periodDays || 30}
|
||||
/>
|
||||
<select
|
||||
id=""
|
||||
className="block h-9 w-full rounded-md border-gray-300 py-2 pl-3 pr-10 text-sm focus:outline-none"
|
||||
{...formMethods.register("periodCountCalendarDays")}
|
||||
defaultValue={eventType.periodCountCalendarDays ? "1" : "0"}>
|
||||
<option value="1">{t("calendar_days")}</option>
|
||||
<option value="0">{t("business_days")}</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
{period.type === "RANGE" && (
|
||||
<div className="inline-flex space-x-2 ltr:ml-2 rtl:mr-2 rtl:space-x-reverse">
|
||||
<Controller
|
||||
name="periodDates"
|
||||
control={formMethods.control}
|
||||
defaultValue={periodDates}
|
||||
render={() => (
|
||||
<DateRangePicker
|
||||
startDate={formMethods.getValues("periodDates").startDate}
|
||||
endDate={formMethods.getValues("periodDates").endDate}
|
||||
onDatesChange={({ startDate, endDate }) => {
|
||||
formMethods.setValue("periodDates", {
|
||||
startDate,
|
||||
endDate,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{period.suffix ? (
|
||||
<span className="ltr:ml-2 rtl:mr-2"> {period.suffix}</span>
|
||||
) : null}
|
||||
<Controller
|
||||
name="periodType"
|
||||
control={formMethods.control}
|
||||
render={({ field: { value } }) => (
|
||||
<SettingsToggle
|
||||
title={t("limit_future_bookings")}
|
||||
description={t("limit_future_bookings_description")}
|
||||
checked={value !== "UNLIMITED"}
|
||||
onCheckedChange={(bool) => formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED")}>
|
||||
<RadioGroup.Root
|
||||
defaultValue={watchPeriodType}
|
||||
value={watchPeriodType}
|
||||
onValueChange={(val) => formMethods.setValue("periodType", val as PeriodType)}>
|
||||
{PERIOD_TYPES.map((period) => {
|
||||
if (period.type === "UNLIMITED") return null;
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"mb-2 flex flex-wrap items-center text-sm",
|
||||
watchPeriodType === "UNLIMITED" && "pointer-events-none opacity-30"
|
||||
)}
|
||||
key={period.type}>
|
||||
<RadioGroup.Item
|
||||
id={period.type}
|
||||
value={period.type}
|
||||
className="min-w-4 flex h-4 w-4 cursor-pointer items-center rounded-full border border-black bg-white focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
|
||||
<RadioGroup.Indicator className="relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full after:bg-black" />
|
||||
</RadioGroup.Item>
|
||||
{period.prefix ? <span>{period.prefix} </span> : null}
|
||||
{period.type === "ROLLING" && (
|
||||
<div className="flex h-9">
|
||||
<Input
|
||||
type="number"
|
||||
className="block w-16 rounded-md border-gray-300 py-3 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
|
||||
placeholder="30"
|
||||
{...formMethods.register("periodDays", { valueAsNumber: true })}
|
||||
defaultValue={eventType.periodDays || 30}
|
||||
/>
|
||||
<select
|
||||
id=""
|
||||
className="block h-9 w-full rounded-md border-gray-300 py-2 pl-3 pr-10 text-sm focus:outline-none"
|
||||
{...formMethods.register("periodCountCalendarDays")}
|
||||
defaultValue={eventType.periodCountCalendarDays ? "1" : "0"}>
|
||||
<option value="1">{t("calendar_days")}</option>
|
||||
<option value="0">{t("business_days")}</option>
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</RadioGroup.Root>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
)}
|
||||
{period.type === "RANGE" && (
|
||||
<div className="inline-flex space-x-2 ltr:ml-2 rtl:mr-2 rtl:space-x-reverse">
|
||||
<Controller
|
||||
name="periodDates"
|
||||
control={formMethods.control}
|
||||
defaultValue={periodDates}
|
||||
render={() => (
|
||||
<DateRangePicker
|
||||
startDate={formMethods.getValues("periodDates").startDate}
|
||||
endDate={formMethods.getValues("periodDates").endDate}
|
||||
onDatesChange={({ startDate, endDate }) => {
|
||||
formMethods.setValue("periodDates", {
|
||||
startDate,
|
||||
endDate,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{period.suffix ? <span className="ltr:ml-2 rtl:mr-2"> {period.suffix}</span> : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</RadioGroup.Root>
|
||||
</SettingsToggle>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Frequency } from "@calcom/prisma/zod-utils";
|
||||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||
import { Alert } from "@calcom/ui/Alert";
|
||||
import { Label, Select, Switch } from "@calcom/ui/v2";
|
||||
import { Label, Select, Switch, SettingsToggle } from "@calcom/ui/v2";
|
||||
|
||||
type RecurringEventControllerProps = {
|
||||
recurringEvent: RecurringEvent | null;
|
||||
@@ -36,95 +36,85 @@ export default function RecurringEventController({
|
||||
<Alert severity="warning" title={t("warning_payment_recurring_event")} />
|
||||
) : (
|
||||
<>
|
||||
<div className="flex space-x-3 ">
|
||||
<Switch
|
||||
name="requireConfirmation"
|
||||
data-testid="recurring-event-check"
|
||||
fitToHeight={true}
|
||||
checked={recurringEventState !== null}
|
||||
onCheckedChange={(e) => {
|
||||
if (!e) {
|
||||
formMethods.setValue("recurringEvent", null);
|
||||
setRecurringEventState(null);
|
||||
} else {
|
||||
const newVal = recurringEvent || {
|
||||
interval: 1,
|
||||
count: 12,
|
||||
freq: Frequency.WEEKLY,
|
||||
};
|
||||
formMethods.setValue("recurringEvent", newVal);
|
||||
setRecurringEventState(newVal);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<Label className="text-sm font-semibold leading-none text-black">
|
||||
{t("recurring_event")}
|
||||
</Label>
|
||||
<p className="-mt-2 text-sm leading-normal text-gray-600">
|
||||
{t("recurring_event_description")}
|
||||
</p>
|
||||
{recurringEventState && (
|
||||
<div data-testid="recurring-event-collapsible" className="mt-4 text-sm">
|
||||
<div className="flex items-center">
|
||||
<p className="mr-2 text-neutral-900">{t("repeats_every")}</p>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="20"
|
||||
className="block h-[36px] w-16 rounded-md border-gray-300 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
|
||||
defaultValue={recurringEventState.interval}
|
||||
onChange={(event) => {
|
||||
const newVal = {
|
||||
...recurringEventState,
|
||||
interval: parseInt(event?.target.value),
|
||||
};
|
||||
formMethods.setValue("recurringEvent", newVal);
|
||||
setRecurringEventState(newVal);
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
options={recurringEventFreqOptions}
|
||||
value={recurringEventFreqOptions[recurringEventState.freq]}
|
||||
isSearchable={false}
|
||||
className="w-18 block h-[36px] min-w-0 rounded-md text-sm"
|
||||
onChange={(event) => {
|
||||
const newVal = {
|
||||
...recurringEventState,
|
||||
freq: parseInt(event?.value || `${Frequency.WEEKLY}`),
|
||||
};
|
||||
formMethods.setValue("recurringEvent", newVal);
|
||||
setRecurringEventState(newVal);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center">
|
||||
<p className="mr-2 text-neutral-900">{t("for_a_maximum_of")}</p>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="20"
|
||||
className="block h-[36px] w-16 rounded-md border-gray-300 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
|
||||
defaultValue={recurringEventState.count}
|
||||
onChange={(event) => {
|
||||
const newVal = {
|
||||
...recurringEventState,
|
||||
count: parseInt(event?.target.value),
|
||||
};
|
||||
formMethods.setValue("recurringEvent", newVal);
|
||||
setRecurringEventState(newVal);
|
||||
}}
|
||||
/>
|
||||
<p className="mr-2 text-neutral-900">
|
||||
{t("events", {
|
||||
count: recurringEventState.count,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<SettingsToggle
|
||||
title={t("recurring_event")}
|
||||
description={t("recurring_event_description")}
|
||||
checked={recurringEventState !== null}
|
||||
data-testid="recurring-event-check"
|
||||
onCheckedChange={(e) => {
|
||||
if (!e) {
|
||||
formMethods.setValue("recurringEvent", null);
|
||||
setRecurringEventState(null);
|
||||
} else {
|
||||
const newVal = recurringEvent || {
|
||||
interval: 1,
|
||||
count: 12,
|
||||
freq: Frequency.WEEKLY,
|
||||
};
|
||||
formMethods.setValue("recurringEvent", newVal);
|
||||
setRecurringEventState(newVal);
|
||||
}
|
||||
}}>
|
||||
{recurringEventState && (
|
||||
<div data-testid="recurring-event-collapsible" className="text-sm">
|
||||
<div className="flex items-center">
|
||||
<p className="mr-2 text-neutral-900">{t("repeats_every")}</p>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="20"
|
||||
className="block h-[36px] w-16 rounded-md border-gray-300 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
|
||||
defaultValue={recurringEventState.interval}
|
||||
onChange={(event) => {
|
||||
const newVal = {
|
||||
...recurringEventState,
|
||||
interval: parseInt(event?.target.value),
|
||||
};
|
||||
formMethods.setValue("recurringEvent", newVal);
|
||||
setRecurringEventState(newVal);
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
options={recurringEventFreqOptions}
|
||||
value={recurringEventFreqOptions[recurringEventState.freq]}
|
||||
isSearchable={false}
|
||||
className="w-18 block h-[36px] min-w-0 rounded-md text-sm"
|
||||
onChange={(event) => {
|
||||
const newVal = {
|
||||
...recurringEventState,
|
||||
freq: parseInt(event?.value || `${Frequency.WEEKLY}`),
|
||||
};
|
||||
formMethods.setValue("recurringEvent", newVal);
|
||||
setRecurringEventState(newVal);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center">
|
||||
<p className="mr-2 text-neutral-900">{t("for_a_maximum_of")}</p>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="20"
|
||||
className="block h-[36px] w-16 rounded-md border-gray-300 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
|
||||
defaultValue={recurringEventState.count}
|
||||
onChange={(event) => {
|
||||
const newVal = {
|
||||
...recurringEventState,
|
||||
count: parseInt(event?.target.value),
|
||||
};
|
||||
formMethods.setValue("recurringEvent", newVal);
|
||||
setRecurringEventState(newVal);
|
||||
}}
|
||||
/>
|
||||
<p className="mr-2 text-neutral-900">
|
||||
{t("events", {
|
||||
count: recurringEventState.count,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</SettingsToggle>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1327,6 +1327,8 @@
|
||||
"saml_sp_entity_id_copied": "SP Entity ID copied!",
|
||||
"saml_btn_configure": "Configure",
|
||||
"add_calendar": "Add Calendar",
|
||||
"limit_future_bookings":"Limit future bookings",
|
||||
"limit_future_bookings_description":"Limit how far in the future this event can be booked",
|
||||
"no_event_types": "No event types setup",
|
||||
"no_event_types_description": "{{name}} has not setup any event types for you to book."
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"react": "^18.2.0",
|
||||
"react-colorful": "^5.6.0",
|
||||
"react-feather": "^2.0.10",
|
||||
"@formkit/auto-animate": "^1.0.0-beta.1",
|
||||
"react-hook-form": "^7.34.2",
|
||||
"react-icons": "^4.4.0",
|
||||
"react-select": "^5.4.0"
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import React, { ReactNode } from "react";
|
||||
|
||||
import Switch from "./Switch";
|
||||
import { Label } from "./form";
|
||||
|
||||
type Props = {
|
||||
children?: ReactNode;
|
||||
title: string;
|
||||
description?: string;
|
||||
checked: boolean;
|
||||
disabled?: boolean;
|
||||
onCheckedChange?: (checked: boolean) => void;
|
||||
"data-testid"?: string;
|
||||
};
|
||||
|
||||
function SettingsToggle({
|
||||
checked,
|
||||
onCheckedChange,
|
||||
description,
|
||||
title,
|
||||
children,
|
||||
disabled,
|
||||
...rest
|
||||
}: Props) {
|
||||
const [animateRef] = useAutoAnimate<HTMLDivElement>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex w-full flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
|
||||
<fieldset className="block w-full flex-col sm:flex">
|
||||
<div className="flex space-x-3">
|
||||
<Switch
|
||||
data-testid={rest["data-testid"]}
|
||||
fitToHeight={true}
|
||||
checked={checked}
|
||||
onCheckedChange={onCheckedChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<div className="">
|
||||
<Label className="text-sm font-semibold leading-none text-black">{title}</Label>
|
||||
{description && <p className="-mt-2 text-sm leading-normal text-gray-600">{description}</p>}
|
||||
</div>
|
||||
</div>
|
||||
{children && (
|
||||
<div className="mt-4 lg:ml-14" ref={animateRef}>
|
||||
{checked && children}
|
||||
</div>
|
||||
)}
|
||||
</fieldset>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SettingsToggle;
|
||||
@@ -31,3 +31,4 @@ export { default as HorizontalTabs, HorizontalTabItem } from "./navigation/tabs/
|
||||
export type { VerticalTabItemProps } from "./navigation/tabs/VerticalTabItem";
|
||||
export type { HorizontalTabItemProps } from "./navigation/tabs/HorizontalTabItem";
|
||||
export * from "./Portal";
|
||||
export { default as SettingsToggle } from "./SettingsToggle";
|
||||
|
||||
@@ -15,7 +15,7 @@ type Props = {
|
||||
function CustomInputItem({ required, deleteOnClick, editOnClick, type, question }: Props) {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<li className="border-1 flex border border-gray-200 bg-white px-6 py-4 first:rounded-t-md last:rounded-b-md only:rounded-md xl:ml-7 ">
|
||||
<li className="border-1 flex border border-gray-200 bg-white px-6 py-4 first:rounded-t-md last:rounded-b-md only:rounded-md">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center">
|
||||
<span className="pr-2 text-sm font-semibold leading-none text-black">{question}</span>
|
||||
|
||||
Reference in New Issue
Block a user