Improvements for duration notice type logic

This commit is contained in:
Jeroen Reumkens 2022-11-09 15:34:03 +01:00
parent 35a0c74c7b
commit 6d75920c70
5 changed files with 84 additions and 59 deletions

@ -1 +1 @@
Subproject commit 0f5017010fd2f037aa053d04522048fa994eaf43
Subproject commit e0619d383ab976f0ddf8d92b97ade894f343ccce

@ -1 +1 @@
Subproject commit 2219900e06c3a683c85ce066e6ea3eb2d6ae14e9
Subproject commit 48c60a533d981cbad45eee0cd558f9c7c215f857

View File

@ -1,7 +1,7 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as RadioGroup from "@radix-ui/react-radio-group";
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react";
import { useMemo, useRef, useState } from "react";
import { useFormContext, Controller, useWatch } from "react-hook-form";
import { classNames } from "@calcom/lib";
@ -18,22 +18,16 @@ import DateRangePicker from "@calcom/ui/v2/core/form/date-range-picker/DateRange
type EventLimitType = {
eventType: Pick<EventTypeSetupInfered, "eventType">;
currentDurationType: Dispatch<SetStateAction<string>>;
};
export const EventLimitsTab = (props: EventLimitType) => {
const { t } = useLocale();
const formMethods = useFormContext<FormValues>();
const { eventType } = props.eventType;
const setCurrentDurationType = props.currentDurationType;
const minimumBookingNoticeType = useRef(findDurationType(eventType.minimumBookingNotice));
setCurrentDurationType(minimumBookingNoticeType.current);
const prevBookingNoticeType = useRef(minimumBookingNoticeType.current);
const displayValue = convertToNewDurationType(
"minutes",
minimumBookingNoticeType.current,
eventType.minimumBookingNotice
);
const minimumBookingNoticeInDurationTypeFormValue = formMethods.watch("minimumBookingNoticeInDurationType");
const PERIOD_TYPES = [
{
@ -50,10 +44,6 @@ export const EventLimitsTab = (props: EventLimitType) => {
},
];
useEffect(() => {
formMethods.setValue("minimumBookingNotice", displayValue);
}, []);
const periodType =
PERIOD_TYPES.find((s) => s.type === eventType.periodType) ||
PERIOD_TYPES.find((s) => s.type === "UNLIMITED");
@ -68,6 +58,57 @@ export const EventLimitsTab = (props: EventLimitType) => {
defaultValue: periodType?.type,
});
const onMinimumNoticeDurationTypeChange = useMemo(
() => (durationType?: string) => {
if (typeof durationType === "undefined") return;
// Store current selected type in ref to use in previous run for comparrison.
minimumBookingNoticeType.current = durationType;
// Uses the previous selected type and converts the value to the new type.
// Eg day was selected before, user selects hours now, so we multiple * 24 and round up.
const minimumBookingNoticeInDurationType = convertToNewDurationType(
prevBookingNoticeType.current,
durationType,
Number(minimumBookingNoticeInDurationTypeFormValue)
);
// Uses the round up value in the new type to calculate the value in minutes.
// We should NOT use minimumBookingNoticeInDurationTypeFormValue here, since that
// would break in the case that the user enters 90 minutes, changes the type to
// hours (that will show 2 hours), but the value in minutes would then remain 90,
// which is something else than the user things they save.
const minimumBookingNoticeInMinutes = convertToNewDurationType(
durationType,
"minutes",
Number(minimumBookingNoticeInDurationType)
);
// Updates both form values as well as stores the current type as previous type in ref.
formMethods.setValue("minimumBookingNotice", minimumBookingNoticeInMinutes);
formMethods.setValue("minimumBookingNoticeInDurationType", minimumBookingNoticeInDurationType);
prevBookingNoticeType.current = durationType;
},
[formMethods, minimumBookingNoticeInDurationTypeFormValue]
);
/**
* When the user inputs a new value for minimumBookingNoticeInDurationType,
* we calculate the value in minutes, and update this hidden field as well.
*/
const onMinimumNoticeChange = useMemo(
() => (duration: string) => {
const minimumBookingNoticeInMinutes = convertToNewDurationType(
minimumBookingNoticeType.current,
"minutes",
Number(duration)
);
formMethods.setValue("minimumBookingNotice", minimumBookingNoticeInMinutes);
},
[formMethods]
);
return (
<div>
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
@ -142,7 +183,6 @@ export const EventLimitsTab = (props: EventLimitType) => {
name="minimumBookingNotice"
control={formMethods.control}
render={() => {
const minBookingValue = formMethods.watch("minimumBookingNotice");
const durationTypeOptions = [
{
label: t("minutes"),
@ -167,16 +207,12 @@ export const EventLimitsTab = (props: EventLimitType) => {
type="number"
placeholder="120"
className="mr-2 rounded-[4px]"
defaultValue={displayValue}
{...formMethods.register("minimumBookingNotice", {
valueAsNumber: true,
onChange: (value) => {
value.target.value
? (eventType.minimumBookingNotice = JSON.parse(value.target.value))
: null;
},
{...formMethods.register("minimumBookingNoticeInDurationType", {
onChange: (event: React.ChangeEvent<HTMLInputElement>) =>
onMinimumNoticeChange(event.target.value),
})}
/>
<input type="hidden" {...formMethods.register("minimumBookingNotice")} />
</div>
<Select
isSearchable={false}
@ -184,17 +220,7 @@ export const EventLimitsTab = (props: EventLimitType) => {
defaultValue={durationTypeOptions.find(
(option) => option.value === minimumBookingNoticeType.current
)}
onChange={(val) => {
if (val) {
minimumBookingNoticeType.current = val.value;
setCurrentDurationType(val.value);
eventType.minimumBookingNotice = Math.ceil(
convertToNewDurationType(prevBookingNoticeType.current, val.value, minBookingValue)
);
formMethods.setValue("minimumBookingNotice", eventType.minimumBookingNotice);
prevBookingNoticeType.current = val.value;
}
}}
onChange={(input) => onMinimumNoticeDurationTypeChange(input?.value)}
options={durationTypeOptions}
/>
</>

View File

@ -3,7 +3,7 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import { EventTypeCustomInput, PeriodType, Prisma, SchedulingType } from "@prisma/client";
import { GetServerSidePropsContext } from "next";
import { useRouter } from "next/router";
import { useRef, useState } from "react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
@ -75,6 +75,7 @@ export type FormValues = {
seatsShowAttendees: boolean | null;
seatsPerTimeSlotEnabled: boolean;
minimumBookingNotice: number;
minimumBookingNoticeInDurationType: number;
beforeBufferTime: number;
afterBufferTime: number;
slotInterval: number | null;
@ -109,7 +110,6 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
// TODO: It isn't a good idea to maintain state using setEventType. If we want to connect the SSR'd data to tRPC, we should useQuery(["viewer.eventTypes.get"]) with initialData
// Due to this change, when Form is saved, there is no way to propagate that info to eventType (e.g. disabling stripe app doesn't allow recurring tab to be enabled without refresh).
const [eventType, setEventType] = useState(dbEventType);
const [currentDurationType, setCurrentDurationType] = useState("minutes");
const router = useRouter();
const { tabName } = querySchema.parse(router.query);
@ -154,12 +154,6 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
endDate: new Date(eventType.periodEndDate || Date.now()),
});
const minimumBookingNoticeInMinutes = convertToNewDurationType(
currentDurationType,
"minutes",
eventType.minimumBookingNotice
);
const formMethods = useForm<FormValues>({
defaultValues: {
title: eventType.title,
@ -174,7 +168,12 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
endDate: periodDates.endDate,
},
schedulingType: eventType.schedulingType,
minimumBookingNotice: minimumBookingNoticeInMinutes,
minimumBookingNotice: eventType.minimumBookingNotice,
minimumBookingNoticeInDurationType: convertToNewDurationType(
"minutes",
findDurationType(eventType.minimumBookingNotice),
eventType.minimumBookingNotice
),
metadata: eventType.metadata,
},
});
@ -211,9 +210,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
currentUserMembership={props.currentUserMembership}
/>
),
limits: (
<EventLimitsTab eventType={{ eventType: eventType }} currentDurationType={setCurrentDurationType} />
),
limits: <EventLimitsTab eventType={{ eventType: eventType }} />,
advanced: <EventAdvancedTab eventType={eventType} team={team} />,
recurring: <EventRecurringTab eventType={eventType} />,
apps: <EventAppsTab eventType={{ ...eventType, URL: permalink }} />,
@ -251,9 +248,11 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
recurringEvent,
locations,
metadata,
// We don't need to send it to the backend
// We don't need to send send these values to the backend
// eslint-disable-next-line @typescript-eslint/no-unused-vars
seatsPerTimeSlotEnabled,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
minimumBookingNoticeInDurationType,
...input
} = values;
@ -269,7 +268,6 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
periodStartDate: periodDates.startDate,
periodEndDate: periodDates.endDate,
periodCountCalendarDays: periodCountCalendarDays === "1",
minimumBookingNotice: minimumBookingNoticeInMinutes,
id: eventType.id,
beforeEventBuffer: beforeBufferTime,
afterEventBuffer: afterBufferTime,

View File

@ -7,36 +7,37 @@ export default function convertToNewDurationType(
newType: string | "minutes" | "hours" | "days",
prevValue: number
) {
const round = Math.ceil;
if (newType === "minutes") {
if (prevType === "hours") {
return prevValue * MINUTES_IN_HOUR;
return round(prevValue * MINUTES_IN_HOUR);
}
if (prevType === "days") {
return prevValue * MINUTES_IN_DAY;
return round(prevValue * MINUTES_IN_DAY);
}
if (prevType === "minutes") {
return prevValue;
return round(prevValue);
}
} else if (newType === "hours") {
if (prevType === "minutes") {
return prevValue / MINUTES_IN_HOUR;
return round(prevValue / MINUTES_IN_HOUR);
}
if (prevType === "days") {
return prevValue * HOURS_IN_DAY;
return round(prevValue * HOURS_IN_DAY);
}
if (prevType === "hours") {
return prevValue;
return round(prevValue);
}
} else if (newType === "days") {
if (prevType === "days") {
return prevValue;
return round(prevValue);
}
if (prevType === "minutes") {
return prevValue / MINUTES_IN_DAY;
return round(prevValue / MINUTES_IN_DAY);
}
if (prevType === "hours") {
return prevValue / HOURS_IN_DAY;
return round(prevValue / HOURS_IN_DAY);
}
}
return prevValue;
return round(prevValue);
}