refactor: Only send dirty fields for event type update call (#13292)
* init * typefix maybe * fix type * fix e2e * Extract delete dialog to its own function to optimise rerender * wip * Delete isEqual * adds eslint tsignore comment * only update changed fields * fix type err * --WIP * fix type err isLoading—>isPending * isLoading—>isPending * post conflict resoution fix * shouldDirty minimumBookingNotice * adds shouldDirty to setValues * reset form to ensure updated default values * shouldDirty changes and fixed deep check of dirty * --WIP * should dirty recurring * shouldDirty team tab * multiple-fixes * adding console log to test something in e2e * shouldDirty apps tab * fixes managed event type children association * fixed managed events update * dirty managed fix * fixes some onChange versus setValue and periodCountCalendarDays * Seeing different behaviour explicitly unwrapping formState * only mutate if at least 1 field dirty * fix type error * bunch of fixes * --WIP * update only dirtyFields for Managed Events existing children * Fixes update old managed event type tests --WIP * Fix unit tests A bunch of unit test fixes which started failing due to update fields only requiring actual updated fields. some other small fixes also included * type fix * remove unnecessary comment * Adds dirtyField to instant event * fix: add shouldDirty * fixes availability and team tabs, and disable save button unless dirty * fix for hosts and bookerLayout * add shouldDirty when resetting hosts after changing schedulingType * add missing shouldDirty to useEventTypeDestinationCalendarEmail --------- Co-authored-by: Alex van Andel <me@alexvanandel.com> Co-authored-by: Udit Takkar <udit222001@gmail.com> Co-authored-by: CarinaWolli <wollencarina@gmail.com>
This commit is contained in:
co-authored by
Alex van Andel
Udit Takkar
CarinaWolli
parent
72c493ee21
commit
ee286a600f
@@ -59,7 +59,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
const selectedThemeIsDark =
|
||||
user?.theme === "dark" ||
|
||||
(!user?.theme && typeof document !== "undefined" && document.documentElement.classList.contains("dark"));
|
||||
|
||||
formMethods.getValues().bookingFields.forEach(({ name }) => {
|
||||
bookingFields[name] = `${name} input`;
|
||||
});
|
||||
@@ -101,7 +100,8 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
};
|
||||
}
|
||||
return field;
|
||||
})
|
||||
}),
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
};
|
||||
|
||||
@@ -183,7 +183,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
checked={useEventTypeDestinationCalendarEmail}
|
||||
onCheckedChange={(val) => {
|
||||
setUseEventTypeDestinationCalendarEmail(val);
|
||||
formMethods.setValue("useEventTypeDestinationCalendarEmail", val);
|
||||
formMethods.setValue("useEventTypeDestinationCalendarEmail", val, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -319,7 +319,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
description={t("private_link_description", { appName: APP_NAME })}
|
||||
checked={hashedLinkVisible}
|
||||
onCheckedChange={(e) => {
|
||||
formMethods.setValue("hashedLink", e ? hashedUrl : undefined);
|
||||
formMethods.setValue("hashedLink", e ? hashedUrl : undefined, { shouldDirty: true });
|
||||
setHashedLinkVisible(e);
|
||||
}}>
|
||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||
@@ -383,10 +383,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
// Enabling seats will disable guests and requiring confirmation until fully supported
|
||||
if (e) {
|
||||
toggleGuests(false);
|
||||
formMethods.setValue("requiresConfirmation", false);
|
||||
formMethods.setValue("requiresConfirmation", false, { shouldDirty: true });
|
||||
setRequiresConfirmation(false);
|
||||
formMethods.setValue("metadata.multipleDuration", undefined);
|
||||
formMethods.setValue("seatsPerTimeSlot", eventType.seatsPerTimeSlot ?? 2);
|
||||
formMethods.setValue("metadata.multipleDuration", undefined, { shouldDirty: true });
|
||||
formMethods.setValue("seatsPerTimeSlot", eventType.seatsPerTimeSlot ?? 2, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
} else {
|
||||
formMethods.setValue("seatsPerTimeSlot", null);
|
||||
toggleGuests(true);
|
||||
@@ -506,7 +508,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
{showEventNameTip && (
|
||||
<CustomEventTypeModal
|
||||
close={closeEventNameTip}
|
||||
setValue={(val: string) => formMethods.setValue("eventName", val)}
|
||||
setValue={(val: string) => formMethods.setValue("eventName", val, { shouldDirty: true })}
|
||||
defaultValue={formMethods.getValues("eventName")}
|
||||
placeHolder={eventNamePlaceholder}
|
||||
event={eventNameObject}
|
||||
|
||||
@@ -31,10 +31,14 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
const allAppsData = formMethods.watch("metadata")?.apps || {};
|
||||
|
||||
const setAllAppsData = (_allAppsData: typeof allAppsData) => {
|
||||
formMethods.setValue("metadata", {
|
||||
...formMethods.getValues("metadata"),
|
||||
apps: _allAppsData,
|
||||
});
|
||||
formMethods.setValue(
|
||||
"metadata",
|
||||
{
|
||||
...formMethods.getValues("metadata"),
|
||||
apps: _allAppsData,
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
};
|
||||
|
||||
const getAppDataGetter = (appId: EventTypeAppsList): GetAppData => {
|
||||
|
||||
@@ -230,7 +230,7 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||
: option.value === schedules.find((schedule) => schedule.isDefault)?.id
|
||||
);
|
||||
|
||||
setValue("availability", value);
|
||||
setValue("availability", value, { shouldDirty: true });
|
||||
},
|
||||
[data]
|
||||
);
|
||||
@@ -238,7 +238,7 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!availabilityValue?.value) return;
|
||||
setValue("schedule", availabilityValue.value);
|
||||
setValue("schedule", availabilityValue.value, { shouldDirty: true });
|
||||
}, [availabilityValue, setValue]);
|
||||
|
||||
return (
|
||||
@@ -260,7 +260,7 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||
isSearchable={false}
|
||||
onChange={(selected) => {
|
||||
field.onChange(selected?.value || null);
|
||||
if (selected?.value) setValue("availability", selected);
|
||||
if (selected?.value) setValue("availability", selected, { shouldDirty: true });
|
||||
}}
|
||||
className="block w-full min-w-0 flex-1 rounded-sm text-sm"
|
||||
value={availabilityValue}
|
||||
@@ -298,7 +298,7 @@ const UseCommonScheduleSettingsToggle = ({ eventType }: { eventType: EventTypeSe
|
||||
onCheckedChange={(checked) => {
|
||||
onChange(!checked);
|
||||
if (!checked) {
|
||||
setValue("schedule", null);
|
||||
setValue("schedule", null, { shouldDirty: true });
|
||||
}
|
||||
}}
|
||||
title={t("choose_common_schedule_team_event")}
|
||||
|
||||
@@ -61,7 +61,8 @@ const MinimumBookingNoticeInput = React.forwardRef<
|
||||
minimumBookingNoticeDisplayValues.type,
|
||||
"minutes",
|
||||
minimumBookingNoticeDisplayValues.value
|
||||
)
|
||||
),
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}, [minimumBookingNoticeDisplayValues, setValue, passThroughProps.name]);
|
||||
|
||||
@@ -230,7 +231,9 @@ export const EventLimitsTab = () => {
|
||||
<Select
|
||||
isSearchable={false}
|
||||
onChange={(val) => {
|
||||
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null);
|
||||
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
}}
|
||||
defaultValue={
|
||||
slotIntervalOptions.find(
|
||||
@@ -258,11 +261,15 @@ export const EventLimitsTab = () => {
|
||||
checked={isChecked}
|
||||
onCheckedChange={(active) => {
|
||||
if (active) {
|
||||
formMethods.setValue("bookingLimits", {
|
||||
PER_DAY: 1,
|
||||
});
|
||||
formMethods.setValue(
|
||||
"bookingLimits",
|
||||
{
|
||||
PER_DAY: 1,
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
} else {
|
||||
formMethods.setValue("bookingLimits", {});
|
||||
formMethods.setValue("bookingLimits", {}, { shouldDirty: true });
|
||||
}
|
||||
}}
|
||||
switchContainerClassName={classNames(
|
||||
@@ -279,7 +286,7 @@ export const EventLimitsTab = () => {
|
||||
/>
|
||||
<Controller
|
||||
name="onlyShowFirstAvailableSlot"
|
||||
render={({ field: { value } }) => {
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const isChecked = value;
|
||||
return (
|
||||
<SettingsToggle
|
||||
@@ -289,7 +296,7 @@ export const EventLimitsTab = () => {
|
||||
description={t("limit_booking_only_first_slot_description")}
|
||||
checked={isChecked}
|
||||
onCheckedChange={(active) => {
|
||||
formMethods.setValue("onlyShowFirstAvailableSlot", active ?? false);
|
||||
onChange(active ?? false);
|
||||
}}
|
||||
switchContainerClassName={classNames(
|
||||
"border-subtle mt-6 rounded-lg border py-6 px-4 sm:px-6",
|
||||
@@ -301,7 +308,7 @@ export const EventLimitsTab = () => {
|
||||
/>
|
||||
<Controller
|
||||
name="durationLimits"
|
||||
render={({ field: { value } }) => {
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const isChecked = Object.keys(value ?? {}).length > 0;
|
||||
return (
|
||||
<SettingsToggle
|
||||
@@ -317,11 +324,11 @@ export const EventLimitsTab = () => {
|
||||
checked={isChecked}
|
||||
onCheckedChange={(active) => {
|
||||
if (active) {
|
||||
formMethods.setValue("durationLimits", {
|
||||
onChange({
|
||||
PER_DAY: 60,
|
||||
});
|
||||
} else {
|
||||
formMethods.setValue("durationLimits", {});
|
||||
onChange({});
|
||||
}
|
||||
}}>
|
||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||
@@ -338,7 +345,7 @@ export const EventLimitsTab = () => {
|
||||
/>
|
||||
<Controller
|
||||
name="periodType"
|
||||
render={({ field: { value } }) => {
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const isChecked = value && value !== "UNLIMITED";
|
||||
|
||||
return (
|
||||
@@ -353,11 +360,16 @@ export const EventLimitsTab = () => {
|
||||
title={t("limit_future_bookings")}
|
||||
description={t("limit_future_bookings_description")}
|
||||
checked={isChecked}
|
||||
onCheckedChange={(bool) => formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED")}>
|
||||
onCheckedChange={(bool) =>
|
||||
// formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED", { shouldDirty: true })
|
||||
onChange(bool ? "ROLLING" : "UNLIMITED")
|
||||
}>
|
||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||
<RadioGroup.Root
|
||||
value={watchPeriodType}
|
||||
onValueChange={(val) => formMethods.setValue("periodType", val as PeriodType)}>
|
||||
onValueChange={(val) =>
|
||||
formMethods.setValue("periodType", val as PeriodType, { shouldDirty: true })
|
||||
}>
|
||||
{PERIOD_TYPES.map((period) => {
|
||||
if (period.type === "UNLIMITED") return null;
|
||||
return (
|
||||
@@ -388,17 +400,21 @@ export const EventLimitsTab = () => {
|
||||
options={optionsPeriod}
|
||||
isSearchable={false}
|
||||
onChange={(opt) =>
|
||||
formMethods.setValue("periodCountCalendarDays", opt?.value === 1 ? "1" : "0")
|
||||
formMethods.setValue(
|
||||
"periodCountCalendarDays",
|
||||
opt?.value === 1 ? true : false,
|
||||
{ shouldDirty: true }
|
||||
)
|
||||
}
|
||||
name="periodCoundCalendarDays"
|
||||
value={optionsPeriod.find((opt) => {
|
||||
opt.value ===
|
||||
(formMethods.getValues("periodCountCalendarDays") === "1" ? 1 : 0);
|
||||
(formMethods.getValues("periodCountCalendarDays") === true ? 1 : 0);
|
||||
})}
|
||||
defaultValue={optionsPeriod.find(
|
||||
(opt) =>
|
||||
opt.value ===
|
||||
(formMethods.getValues("periodCountCalendarDays") === "1" ? 1 : 0)
|
||||
(formMethods.getValues("periodCountCalendarDays") === true ? 1 : 0)
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -407,12 +423,12 @@ export const EventLimitsTab = () => {
|
||||
<div className="me-2 ms-2 inline-flex space-x-2 rtl:space-x-reverse">
|
||||
<Controller
|
||||
name="periodDates"
|
||||
render={() => (
|
||||
render={({ field: { onChange } }) => (
|
||||
<DateRangePicker
|
||||
startDate={formMethods.getValues("periodDates").startDate}
|
||||
endDate={formMethods.getValues("periodDates").endDate}
|
||||
onDatesChange={({ startDate, endDate }) => {
|
||||
formMethods.setValue("periodDates", {
|
||||
onChange({
|
||||
startDate,
|
||||
endDate,
|
||||
});
|
||||
@@ -446,7 +462,7 @@ export const EventLimitsTab = () => {
|
||||
onCheckedChange={(active) => {
|
||||
setOffsetToggle(active);
|
||||
if (!active) {
|
||||
formMethods.setValue("offsetStart", 0);
|
||||
formMethods.setValue("offsetStart", 0, { shouldDirty: true });
|
||||
}
|
||||
}}>
|
||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||
@@ -577,11 +593,16 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
|
||||
);
|
||||
if (!rest || !currentKeys.length) return;
|
||||
//currentDurationLimits is always defined so can be casted
|
||||
// @ts-expect-error FIXME Fix these typings
|
||||
setValue(propertyName, {
|
||||
...watchIntervalLimits,
|
||||
[rest.value]: defaultLimit,
|
||||
});
|
||||
|
||||
setValue(
|
||||
propertyName,
|
||||
// @ts-expect-error FIXME Fix these typings
|
||||
{
|
||||
...watchIntervalLimits,
|
||||
[rest.value]: defaultLimit,
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -611,7 +632,7 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
|
||||
)}
|
||||
onLimitChange={(intervalLimitKey, val) =>
|
||||
// @ts-expect-error FIXME Fix these typings
|
||||
setValue(`${propertyName}.${intervalLimitKey}`, val)
|
||||
setValue(`${propertyName}.${intervalLimitKey}`, val, { shouldDirty: true })
|
||||
}
|
||||
onDelete={(intervalLimitKey) => {
|
||||
const current = currentIntervalLimits;
|
||||
|
||||
@@ -83,7 +83,7 @@ const DescriptionEditor = ({ isEditable }: { isEditable: boolean }) => {
|
||||
return mounted ? (
|
||||
<Editor
|
||||
getText={() => md.render(formMethods.getValues("description") || "")}
|
||||
setText={(value: string) => formMethods.setValue("description", turndown(value))}
|
||||
setText={(value: string) => formMethods.setValue("description", turndown(value), { shouldDirty: true })}
|
||||
excludedToolbarItems={["blockType"]}
|
||||
placeholder={t("quick_video_meeting")}
|
||||
editable={isEditable}
|
||||
@@ -512,16 +512,16 @@ export const EventSetupTab = (
|
||||
if (!newOptions.find((opt) => opt.value === defaultDuration?.value)) {
|
||||
if (newOptions.length > 0) {
|
||||
setDefaultDuration(newOptions[0]);
|
||||
formMethods.setValue("length", newOptions[0].value);
|
||||
formMethods.setValue("length", newOptions[0].value, { shouldDirty: true });
|
||||
} else {
|
||||
setDefaultDuration(null);
|
||||
}
|
||||
}
|
||||
if (newOptions.length === 1 && defaultDuration === null) {
|
||||
setDefaultDuration(newOptions[0]);
|
||||
formMethods.setValue("length", newOptions[0].value);
|
||||
formMethods.setValue("length", newOptions[0].value, { shouldDirty: true });
|
||||
}
|
||||
formMethods.setValue("metadata.multipleDuration", values);
|
||||
formMethods.setValue("metadata.multipleDuration", values, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -542,7 +542,7 @@ export const EventSetupTab = (
|
||||
setDefaultDuration(
|
||||
selectedMultipleDuration.find((opt) => opt.value === option?.value) ?? null
|
||||
);
|
||||
if (option) formMethods.setValue("length", option.value);
|
||||
if (option) formMethods.setValue("length", option.value, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -571,12 +571,12 @@ export const EventSetupTab = (
|
||||
setMultipleDuration(undefined);
|
||||
setSelectedMultipleDuration([]);
|
||||
setDefaultDuration(null);
|
||||
formMethods.setValue("metadata.multipleDuration", undefined);
|
||||
formMethods.setValue("length", eventType.length);
|
||||
formMethods.setValue("metadata.multipleDuration", undefined, { shouldDirty: true });
|
||||
formMethods.setValue("length", eventType.length, { shouldDirty: true });
|
||||
} else {
|
||||
setMultipleDuration([]);
|
||||
formMethods.setValue("metadata.multipleDuration", []);
|
||||
formMethods.setValue("length", 0);
|
||||
formMethods.setValue("metadata.multipleDuration", [], { shouldDirty: true });
|
||||
formMethods.setValue("length", 0, { shouldDirty: true });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -127,7 +127,7 @@ const AssignAllTeamMembers = ({
|
||||
labelClassName="mt-0.5 font-normal"
|
||||
checked={assignAllTeamMembers}
|
||||
onCheckedChange={(active) => {
|
||||
setValue("assignAllTeamMembers", active);
|
||||
setValue("assignAllTeamMembers", active, { shouldDirty: true });
|
||||
setAssignAllTeamMembers(active);
|
||||
if (active) {
|
||||
onActive();
|
||||
@@ -250,7 +250,8 @@ const FixedHosts = ({
|
||||
isFixed: true,
|
||||
userId: parseInt(teamMember.value, 10),
|
||||
priority: 2,
|
||||
}))
|
||||
})),
|
||||
{ shouldDirty: true }
|
||||
)
|
||||
}
|
||||
/>
|
||||
@@ -269,7 +270,7 @@ const FixedHosts = ({
|
||||
const rrHosts = getValues("hosts")
|
||||
.filter((host) => !host.isFixed)
|
||||
.sort((a, b) => (b.priority ?? 2) - (a.priority ?? 2));
|
||||
setValue("hosts", rrHosts);
|
||||
setValue("hosts", rrHosts, { shouldDirty: true });
|
||||
}
|
||||
setIsDisabled(checked);
|
||||
}}
|
||||
@@ -290,7 +291,8 @@ const FixedHosts = ({
|
||||
isFixed: true,
|
||||
userId: parseInt(teamMember.value, 10),
|
||||
priority: 2,
|
||||
}))
|
||||
})),
|
||||
{ shouldDirty: true }
|
||||
)
|
||||
}
|
||||
/>
|
||||
@@ -332,7 +334,7 @@ const AddMembersWithSwitch = ({
|
||||
assignAllTeamMembers={assignAllTeamMembers}
|
||||
setAssignAllTeamMembers={setAssignAllTeamMembers}
|
||||
onActive={onActive}
|
||||
onInactive={() => setValue("hosts", [])}
|
||||
onInactive={() => setValue("hosts", [], { shouldDirty: true })}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
@@ -395,7 +397,8 @@ const RoundRobinHosts = ({
|
||||
userId: parseInt(teamMember.value, 10),
|
||||
priority: 2,
|
||||
}))
|
||||
.sort((a, b) => b.priority - a.priority)
|
||||
.sort((a, b) => b.priority - a.priority),
|
||||
{ shouldDirty: true }
|
||||
)
|
||||
}
|
||||
/>
|
||||
@@ -420,7 +423,7 @@ const ChildrenEventTypes = ({
|
||||
<AssignAllTeamMembers
|
||||
assignAllTeamMembers={assignAllTeamMembers}
|
||||
setAssignAllTeamMembers={setAssignAllTeamMembers}
|
||||
onActive={() => setValue("children", childrenEventTypeOptions)}
|
||||
onActive={() => setValue("children", childrenEventTypeOptions, { shouldDirty: true })}
|
||||
/>
|
||||
{!assignAllTeamMembers ? (
|
||||
<Controller<FormValues>
|
||||
@@ -449,7 +452,7 @@ const Hosts = ({
|
||||
const { t } = useLocale();
|
||||
const {
|
||||
control,
|
||||
resetField,
|
||||
setValue,
|
||||
getValues,
|
||||
formState: { submitCount },
|
||||
} = useFormContext<FormValues>();
|
||||
@@ -469,10 +472,12 @@ const Hosts = ({
|
||||
initialValue.current = { hosts: getValues("hosts"), schedulingType, submitCount };
|
||||
return;
|
||||
}
|
||||
resetField("hosts", {
|
||||
defaultValue: initialValue.current.schedulingType === schedulingType ? initialValue.current.hosts : [],
|
||||
});
|
||||
}, [schedulingType, resetField, getValues, submitCount]);
|
||||
setValue(
|
||||
"hosts",
|
||||
initialValue.current.schedulingType === schedulingType ? initialValue.current.hosts : [],
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}, [schedulingType, setValue, getValues, submitCount]);
|
||||
|
||||
return (
|
||||
<Controller<FormValues>
|
||||
@@ -586,7 +591,7 @@ export const EventTeamTab = ({
|
||||
className="w-full"
|
||||
onChange={(val) => {
|
||||
onChange(val?.value);
|
||||
setValue("assignAllTeamMembers", false);
|
||||
setValue("assignAllTeamMembers", false, { shouldDirty: true });
|
||||
setAssignAllTeamMembers(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { trpc, TRPCClientError } from "@calcom/trpc/react";
|
||||
import type { DialogProps } from "@calcom/ui";
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
@@ -133,6 +134,60 @@ function getNavigation({
|
||||
];
|
||||
}
|
||||
|
||||
function DeleteDialog({
|
||||
isManagedEvent,
|
||||
eventTypeId,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: { isManagedEvent: string; eventTypeId: number } & Pick<DialogProps, "open" | "onOpenChange">) {
|
||||
const utils = trpc.useContext();
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
|
||||
onSuccess: async () => {
|
||||
await utils.viewer.eventTypes.invalidate();
|
||||
showToast(t("event_type_deleted_successfully"), "success");
|
||||
router.push("/event-types");
|
||||
onOpenChange?.(false);
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err instanceof HttpError) {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
onOpenChange?.(false);
|
||||
} else if (err instanceof TRPCClientError) {
|
||||
showToast(err.message, "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<ConfirmationDialogContent
|
||||
isPending={deleteMutation.isPending}
|
||||
variety="danger"
|
||||
title={t(`delete${isManagedEvent}_event_type`)}
|
||||
confirmBtnText={t(`confirm_delete_event_type`)}
|
||||
loadingText={t(`confirm_delete_event_type`)}
|
||||
onConfirm={(e) => {
|
||||
e.preventDefault();
|
||||
deleteMutation.mutate({ id: eventTypeId });
|
||||
}}>
|
||||
<p className="mt-5">
|
||||
<Trans
|
||||
i18nKey={`delete${isManagedEvent}_event_type_description`}
|
||||
components={{ li: <li />, ul: <ul className="ml-4 list-disc" /> }}>
|
||||
<ul>
|
||||
<li>Members assigned to this event type will also have their event types deleted.</li>
|
||||
<li>Anyone who they've shared their link with will no longer be able to book using it.</li>
|
||||
</ul>
|
||||
</Trans>
|
||||
</p>
|
||||
</ConfirmationDialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function EventTypeSingleLayout({
|
||||
children,
|
||||
eventType,
|
||||
@@ -149,9 +204,7 @@ function EventTypeSingleLayout({
|
||||
bookerUrl,
|
||||
activeWebhooksNumber,
|
||||
}: Props) {
|
||||
const utils = trpc.useContext();
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
|
||||
const hasPermsToDelete =
|
||||
@@ -160,24 +213,6 @@ function EventTypeSingleLayout({
|
||||
formMethods.getValues("schedulingType") === SchedulingType.MANAGED ||
|
||||
isUserOrganizationAdmin;
|
||||
|
||||
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
|
||||
onSuccess: async () => {
|
||||
await utils.viewer.eventTypes.invalidate();
|
||||
showToast(t("event_type_deleted_successfully"), "success");
|
||||
router.push("/event-types");
|
||||
setDeleteDialogOpen(false);
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err instanceof HttpError) {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
setDeleteDialogOpen(false);
|
||||
} else if (err instanceof TRPCClientError) {
|
||||
showToast(err.message, "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
|
||||
formMethods.getValues(),
|
||||
t("locked_fields_admin_description"),
|
||||
@@ -308,7 +343,7 @@ function EventTypeSingleLayout({
|
||||
id="hiddenSwitch"
|
||||
checked={!formMethods.watch("hidden")}
|
||||
onCheckedChange={(e) => {
|
||||
formMethods.setValue("hidden", !e);
|
||||
formMethods.setValue("hidden", !e, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -424,7 +459,7 @@ function EventTypeSingleLayout({
|
||||
id="hiddenSwitch"
|
||||
checked={!formMethods.watch("hidden")}
|
||||
onCheckedChange={(e) => {
|
||||
formMethods.setValue("hidden", !e);
|
||||
formMethods.setValue("hidden", !e, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -435,6 +470,7 @@ function EventTypeSingleLayout({
|
||||
className="ml-4 lg:ml-0"
|
||||
type="submit"
|
||||
loading={isUpdateMutationLoading}
|
||||
disabled={!formMethods.formState.isDirty}
|
||||
data-testid="update-eventtype"
|
||||
form="event-type-form">
|
||||
{t("save")}
|
||||
@@ -467,31 +503,13 @@ function EventTypeSingleLayout({
|
||||
</div>
|
||||
</div>
|
||||
</Suspense>
|
||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<ConfirmationDialogContent
|
||||
isPending={deleteMutation.isPending}
|
||||
variety="danger"
|
||||
title={t(`delete${isManagedEvent}_event_type`)}
|
||||
confirmBtnText={t(`confirm_delete_event_type`)}
|
||||
loadingText={t(`confirm_delete_event_type`)}
|
||||
onConfirm={(e) => {
|
||||
e.preventDefault();
|
||||
deleteMutation.mutate({ id: formMethods.getValues("id") });
|
||||
}}>
|
||||
<p className="mt-5">
|
||||
<Trans
|
||||
i18nKey={`delete${isManagedEvent}_event_type_description`}
|
||||
components={{ li: <li />, ul: <ul className="ml-4 list-disc" /> }}>
|
||||
<ul>
|
||||
<li>Members assigned to this event type will also have their event types deleted.</li>
|
||||
<li>
|
||||
Anyone who they've shared their link with will no longer be able to book using it.
|
||||
</li>
|
||||
</ul>
|
||||
</Trans>
|
||||
</p>
|
||||
</ConfirmationDialogContent>
|
||||
</Dialog>
|
||||
<DeleteDialog
|
||||
eventTypeId={eventType.id}
|
||||
isManagedEvent={isManagedEvent}
|
||||
open={deleteDialogOpen}
|
||||
onOpenChange={setDeleteDialogOpen}
|
||||
/>
|
||||
|
||||
<EventTypeEmbedDialog />
|
||||
</Shell>
|
||||
);
|
||||
|
||||
@@ -81,10 +81,10 @@ export default function InstantEventController({
|
||||
data-testid="instant-event-check"
|
||||
onCheckedChange={(e) => {
|
||||
if (!e) {
|
||||
formMethods.setValue("isInstantEvent", false);
|
||||
formMethods.setValue("isInstantEvent", false, { shouldDirty: true });
|
||||
setInstantEventState(false);
|
||||
} else {
|
||||
formMethods.setValue("isInstantEvent", true);
|
||||
formMethods.setValue("isInstantEvent", true, { shouldDirty: true });
|
||||
setInstantEventState(true);
|
||||
}
|
||||
}}>
|
||||
|
||||
@@ -66,7 +66,7 @@ export default function RecurringEventController({
|
||||
data-testid="recurring-event-check"
|
||||
onCheckedChange={(e) => {
|
||||
if (!e) {
|
||||
formMethods.setValue("recurringEvent", null);
|
||||
formMethods.setValue("recurringEvent", null, { shouldDirty: true });
|
||||
setRecurringEventState(null);
|
||||
} else {
|
||||
const newVal = eventType.recurringEvent || {
|
||||
@@ -74,7 +74,7 @@ export default function RecurringEventController({
|
||||
count: 12,
|
||||
freq: Frequency.WEEKLY,
|
||||
};
|
||||
formMethods.setValue("recurringEvent", newVal);
|
||||
formMethods.setValue("recurringEvent", newVal, { shouldDirty: true });
|
||||
setRecurringEventState(newVal);
|
||||
}
|
||||
}}>
|
||||
@@ -95,7 +95,7 @@ export default function RecurringEventController({
|
||||
...recurringEventState,
|
||||
interval: parseInt(event?.target.value),
|
||||
};
|
||||
formMethods.setValue("recurringEvent", newVal);
|
||||
formMethods.setValue("recurringEvent", newVal, { shouldDirty: true });
|
||||
setRecurringEventState(newVal);
|
||||
}}
|
||||
/>
|
||||
@@ -110,7 +110,7 @@ export default function RecurringEventController({
|
||||
...recurringEventState,
|
||||
freq: parseInt(event?.value || `${Frequency.WEEKLY}`),
|
||||
};
|
||||
formMethods.setValue("recurringEvent", newVal);
|
||||
formMethods.setValue("recurringEvent", newVal, { shouldDirty: true });
|
||||
setRecurringEventState(newVal);
|
||||
}}
|
||||
/>
|
||||
@@ -129,7 +129,7 @@ export default function RecurringEventController({
|
||||
...recurringEventState,
|
||||
count: parseInt(event?.target.value),
|
||||
};
|
||||
formMethods.setValue("recurringEvent", newVal);
|
||||
formMethods.setValue("recurringEvent", newVal, { shouldDirty: true });
|
||||
setRecurringEventState(newVal);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function RequiresConfirmationController({
|
||||
|
||||
useEffect(() => {
|
||||
if (!requiresConfirmation) {
|
||||
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined);
|
||||
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined, { shouldDirty: true });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [requiresConfirmation]);
|
||||
@@ -82,7 +82,7 @@ export default function RequiresConfirmationController({
|
||||
checked={requiresConfirmation}
|
||||
LockedIcon={requiresConfirmationLockedProps.LockedIcon}
|
||||
onCheckedChange={(val) => {
|
||||
formMethods.setValue("requiresConfirmation", val);
|
||||
formMethods.setValue("requiresConfirmation", val, { shouldDirty: true });
|
||||
onRequiresConfirmation(val);
|
||||
}}>
|
||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||
@@ -96,16 +96,19 @@ export default function RequiresConfirmationController({
|
||||
}
|
||||
onValueChange={(val) => {
|
||||
if (val === "always") {
|
||||
formMethods.setValue("requiresConfirmation", true);
|
||||
formMethods.setValue("requiresConfirmation", true, { shouldDirty: true });
|
||||
onRequiresConfirmation(true);
|
||||
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined);
|
||||
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
setRequiresConfirmationSetup(undefined);
|
||||
} else if (val === "notice") {
|
||||
formMethods.setValue("requiresConfirmation", true);
|
||||
formMethods.setValue("requiresConfirmation", true, { shouldDirty: true });
|
||||
onRequiresConfirmation(true);
|
||||
formMethods.setValue(
|
||||
"metadata.requiresConfirmationThreshold",
|
||||
requiresConfirmationSetup || defaultRequiresConfirmationSetup
|
||||
requiresConfirmationSetup || defaultRequiresConfirmationSetup,
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}
|
||||
}}>
|
||||
@@ -146,7 +149,8 @@ export default function RequiresConfirmationController({
|
||||
});
|
||||
formMethods.setValue(
|
||||
"metadata.requiresConfirmationThreshold.time",
|
||||
val
|
||||
val,
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}}
|
||||
className="border-default !m-0 block w-16 rounded-r-none border-r-0 text-sm [appearance:textfield] focus:z-10 focus:border-r"
|
||||
@@ -171,7 +175,8 @@ export default function RequiresConfirmationController({
|
||||
});
|
||||
formMethods.setValue(
|
||||
"metadata.requiresConfirmationThreshold.unit",
|
||||
opt?.value as UnitTypeLongPlural
|
||||
opt?.value as UnitTypeLongPlural,
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}}
|
||||
defaultValue={defaultValue}
|
||||
|
||||
@@ -119,7 +119,7 @@ export type FormValues = {
|
||||
schedule: number | null;
|
||||
periodType: PeriodType;
|
||||
periodDays: number;
|
||||
periodCountCalendarDays: "1" | "0";
|
||||
periodCountCalendarDays: boolean;
|
||||
periodDates: { startDate: Date; endDate: Date };
|
||||
seatsPerTimeSlot: number | null;
|
||||
seatsShowAttendees: boolean | null;
|
||||
@@ -192,14 +192,17 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
|
||||
const updateMutation = trpc.viewer.eventTypes.update.useMutation({
|
||||
onSuccess: async () => {
|
||||
formMethods.setValue(
|
||||
"children",
|
||||
formMethods.getValues().children.map((child) => ({
|
||||
...child,
|
||||
created: true,
|
||||
}))
|
||||
);
|
||||
formMethods.setValue("assignAllTeamMembers", formMethods.getValues("assignAllTeamMembers") || false);
|
||||
const currentValues = formMethods.getValues();
|
||||
|
||||
currentValues.children = currentValues.children.map((child) => ({
|
||||
...child,
|
||||
created: true,
|
||||
}));
|
||||
currentValues.assignAllTeamMembers = currentValues.assignAllTeamMembers || false;
|
||||
|
||||
// Reset the form with these values as new default values to ensure the correct comparison for dirtyFields eval
|
||||
formMethods.reset(currentValues);
|
||||
|
||||
showToast(t("event_type_updated_successfully", { eventTypeTitle: eventType.title }), "success");
|
||||
},
|
||||
async onSettled() {
|
||||
@@ -253,7 +256,6 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
eventType.bookingFields.forEach(({ name }) => {
|
||||
bookingFields[name] = name;
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const defaultValues: any = useMemo(() => {
|
||||
return {
|
||||
@@ -290,7 +292,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
offsetStart: eventType.offsetStart,
|
||||
bookingFields: eventType.bookingFields,
|
||||
periodType: eventType.periodType,
|
||||
periodCountCalendarDays: eventType.periodCountCalendarDays ? "1" : "0",
|
||||
periodCountCalendarDays: eventType.periodCountCalendarDays ? true : false,
|
||||
schedulingType: eventType.schedulingType,
|
||||
requiresConfirmation: eventType.requiresConfirmation,
|
||||
slotInterval: eventType.slotInterval,
|
||||
@@ -316,7 +318,6 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
assignAllTeamMembers: eventType.assignAllTeamMembers,
|
||||
};
|
||||
}, [eventType, periodDates, metadata]);
|
||||
|
||||
const formMethods = useForm<FormValues>({
|
||||
defaultValues,
|
||||
resolver: zodResolver(
|
||||
@@ -405,14 +406,9 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
.passthrough()
|
||||
),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!formMethods.formState.isDirty) {
|
||||
//TODO: What's the best way to sync the form with backend
|
||||
formMethods.setValue("bookingFields", defaultValues.bookingFields);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [defaultValues]);
|
||||
const {
|
||||
formState: { isDirty: isFormDirty, dirtyFields },
|
||||
} = formMethods;
|
||||
|
||||
const appsMetadata = formMethods.getValues("metadata")?.apps;
|
||||
const availability = formMethods.watch("availability");
|
||||
@@ -453,8 +449,100 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
),
|
||||
webhooks: <EventWebhooksTab eventType={eventType} />,
|
||||
} as const;
|
||||
const isObject = <T,>(value: T): boolean => {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
};
|
||||
|
||||
const isArray = <T,>(value: T): boolean => {
|
||||
return Array.isArray(value);
|
||||
};
|
||||
|
||||
const isFieldDirty = (fieldName: keyof FormValues) => {
|
||||
// If the field itself is directly marked as dirty
|
||||
if (dirtyFields[fieldName] === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the field is an object or an array
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const fieldValue: any = getNestedField(dirtyFields, fieldName);
|
||||
if (isObject(fieldValue)) {
|
||||
for (const key in fieldValue) {
|
||||
if (fieldValue[key] === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isObject(fieldValue[key]) || isArray(fieldValue[key])) {
|
||||
const nestedFieldName = `${fieldName}.${key}` as keyof FormValues;
|
||||
// Recursive call for nested objects or arrays
|
||||
if (isFieldDirty(nestedFieldName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isArray(fieldValue)) {
|
||||
for (const element of fieldValue) {
|
||||
// If element is an object, check each property of the object
|
||||
if (isObject(element)) {
|
||||
for (const key in element) {
|
||||
if (element[key] === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isObject(element[key]) || isArray(element[key])) {
|
||||
const nestedFieldName = `${fieldName}.${key}` as keyof FormValues;
|
||||
// Recursive call for nested objects or arrays within each element
|
||||
if (isFieldDirty(nestedFieldName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (element === true) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const getNestedField = (obj: typeof dirtyFields, path: string) => {
|
||||
const keys = path.split(".");
|
||||
let current = obj;
|
||||
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
// @ts-expect-error /—— currentKey could be any deeply nested fields thanks to recursion
|
||||
const currentKey = current[keys[i]];
|
||||
if (currentKey === undefined) return undefined;
|
||||
current = currentKey;
|
||||
}
|
||||
|
||||
return current;
|
||||
};
|
||||
|
||||
const getDirtyFields = (values: FormValues): Partial<FormValues> => {
|
||||
if (!isFormDirty) {
|
||||
return {};
|
||||
}
|
||||
const updatedFields: Partial<FormValues> = {};
|
||||
Object.keys(dirtyFields).forEach((key) => {
|
||||
const typedKey = key as keyof typeof dirtyFields;
|
||||
updatedFields[typedKey] = undefined;
|
||||
const isDirty = isFieldDirty(typedKey);
|
||||
if (isDirty) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
updatedFields[typedKey] = values[typedKey];
|
||||
}
|
||||
});
|
||||
return updatedFields;
|
||||
};
|
||||
|
||||
const handleSubmit = async (values: FormValues) => {
|
||||
const { children } = values;
|
||||
const dirtyValues = getDirtyFields(values);
|
||||
const dirtyFieldExists = Object.keys(dirtyValues).length !== 0;
|
||||
const {
|
||||
periodDates,
|
||||
periodCountCalendarDays,
|
||||
@@ -470,7 +558,6 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
locations,
|
||||
metadata,
|
||||
customInputs,
|
||||
children,
|
||||
assignAllTeamMembers,
|
||||
// We don't need to send send these values to the backend
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@@ -483,8 +570,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
multipleDurationEnabled,
|
||||
length,
|
||||
...input
|
||||
} = values;
|
||||
|
||||
} = dirtyValues;
|
||||
if (!Number(length)) throw new Error(t("event_setup_length_error"));
|
||||
|
||||
if (bookingLimits) {
|
||||
@@ -504,8 +590,12 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
if (metadata?.multipleDuration.length < 1) {
|
||||
throw new Error(t("event_setup_multiple_duration_error"));
|
||||
} else {
|
||||
if (!length && !metadata?.multipleDuration?.includes(length)) {
|
||||
throw new Error(t("event_setup_multiple_duration_default_error"));
|
||||
// if length is unchanged, we skip this check
|
||||
if (length !== undefined) {
|
||||
if (!length && !metadata?.multipleDuration?.includes(length)) {
|
||||
//This would work but it leaves the potential of this check being useless. Need to check against length and not eventType.length, but length can be undefined
|
||||
throw new Error(t("event_setup_multiple_duration_default_error"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -521,14 +611,14 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { availability, users, scheduleName, ...rest } = input;
|
||||
updateMutation.mutate({
|
||||
const payload = {
|
||||
...rest,
|
||||
length,
|
||||
locations,
|
||||
recurringEvent,
|
||||
periodStartDate: periodDates.startDate,
|
||||
periodEndDate: periodDates.endDate,
|
||||
periodCountCalendarDays: periodCountCalendarDays === "1",
|
||||
periodStartDate: periodDates?.startDate,
|
||||
periodEndDate: periodDates?.endDate,
|
||||
periodCountCalendarDays,
|
||||
id: eventType.id,
|
||||
beforeEventBuffer,
|
||||
afterEventBuffer,
|
||||
@@ -542,7 +632,19 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
customInputs,
|
||||
children,
|
||||
assignAllTeamMembers,
|
||||
});
|
||||
};
|
||||
// Filter out undefined values
|
||||
const filteredPayload = Object.entries(payload).reduce((acc, [key, value]) => {
|
||||
if (value !== undefined) {
|
||||
// @ts-expect-error Element implicitly has any type
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (dirtyFieldExists) {
|
||||
updateMutation.mutate({ ...filteredPayload, id: eventType.id });
|
||||
}
|
||||
};
|
||||
|
||||
const [slugExistsChildrenDialogOpen, setSlugExistsChildrenDialogOpen] = useState<ChildrenEventType[]>([]);
|
||||
@@ -595,6 +697,9 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
form={formMethods}
|
||||
id="event-type-form"
|
||||
handleSubmit={async (values) => {
|
||||
const { children } = values;
|
||||
const dirtyValues = getDirtyFields(values);
|
||||
const dirtyFieldExists = Object.keys(dirtyValues).length !== 0;
|
||||
const {
|
||||
periodDates,
|
||||
periodCountCalendarDays,
|
||||
@@ -617,9 +722,9 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
multipleDurationEnabled,
|
||||
length,
|
||||
...input
|
||||
} = values;
|
||||
} = dirtyValues;
|
||||
|
||||
if (!Number(length)) throw new Error(t("event_setup_length_error"));
|
||||
if (length && !Number(length)) throw new Error(t("event_setup_length_error"));
|
||||
|
||||
if (bookingLimits) {
|
||||
const isValid = validateIntervalLimitOrder(bookingLimits);
|
||||
@@ -638,8 +743,11 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
if (metadata?.multipleDuration.length < 1) {
|
||||
throw new Error(t("event_setup_multiple_duration_error"));
|
||||
} else {
|
||||
if (!length && !metadata?.multipleDuration?.includes(length)) {
|
||||
throw new Error(t("event_setup_multiple_duration_default_error"));
|
||||
if (length !== undefined) {
|
||||
if (!length && !metadata?.multipleDuration?.includes(length)) {
|
||||
//This would work but it leaves the potential of this check being useless. Need to check against length and not eventType.length, but length can be undefined
|
||||
throw new Error(t("event_setup_multiple_duration_default_error"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -651,14 +759,15 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { availability, users, scheduleName, ...rest } = input;
|
||||
updateMutation.mutate({
|
||||
const payload = {
|
||||
...rest,
|
||||
children,
|
||||
length,
|
||||
locations,
|
||||
recurringEvent,
|
||||
periodStartDate: periodDates.startDate,
|
||||
periodEndDate: periodDates.endDate,
|
||||
periodCountCalendarDays: periodCountCalendarDays === "1",
|
||||
periodStartDate: periodDates?.startDate,
|
||||
periodEndDate: periodDates?.endDate,
|
||||
periodCountCalendarDays,
|
||||
id: eventType.id,
|
||||
beforeEventBuffer,
|
||||
afterEventBuffer,
|
||||
@@ -670,7 +779,19 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
seatsShowAvailabilityCount,
|
||||
metadata,
|
||||
customInputs,
|
||||
});
|
||||
};
|
||||
// Filter out undefined values
|
||||
const filteredPayload = Object.entries(payload).reduce((acc, [key, value]) => {
|
||||
if (value !== undefined) {
|
||||
// @ts-expect-error Element implicitly has any type
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (dirtyFieldExists) {
|
||||
updateMutation.mutate({ ...filteredPayload, id: eventType.id });
|
||||
}
|
||||
}}>
|
||||
<div ref={animationParentRef}>{tabMap[tabName]}</div>
|
||||
</Form>
|
||||
|
||||
@@ -42,6 +42,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
updatedValues: {},
|
||||
});
|
||||
expect(result.newUserIds).toEqual(undefined);
|
||||
expect(result.oldUserIds).toEqual(undefined);
|
||||
@@ -62,6 +63,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
updatedValues: {},
|
||||
});
|
||||
expect(result.newUserIds).toEqual(undefined);
|
||||
expect(result.oldUserIds).toEqual(undefined);
|
||||
@@ -88,6 +90,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
updatedValues: {},
|
||||
});
|
||||
expect(result.newUserIds).toEqual(undefined);
|
||||
expect(result.oldUserIds).toEqual(undefined);
|
||||
@@ -125,6 +128,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
updatedValues: {},
|
||||
});
|
||||
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
@@ -173,13 +177,14 @@ describe("handleChildrenEventTypes", () => {
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
updatedValues: {
|
||||
bookingLimits: undefined,
|
||||
},
|
||||
});
|
||||
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
|
||||
data: {
|
||||
...evType,
|
||||
hidden: false,
|
||||
bookingLimits: undefined,
|
||||
durationLimits: undefined,
|
||||
recurringEvent: undefined,
|
||||
hashedLink: { create: { link: expect.any(String) } },
|
||||
},
|
||||
where: {
|
||||
@@ -207,6 +212,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
updatedValues: {},
|
||||
});
|
||||
expect(result.newUserIds).toEqual([]);
|
||||
expect(result.oldUserIds).toEqual([]);
|
||||
@@ -232,6 +238,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
updatedValues: {},
|
||||
});
|
||||
// Have been called
|
||||
expect(result.newUserIds).toEqual([5]);
|
||||
@@ -270,6 +277,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
updatedValues: {},
|
||||
});
|
||||
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
@@ -318,14 +326,15 @@ describe("handleChildrenEventTypes", () => {
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
updatedValues: {
|
||||
length: 30,
|
||||
},
|
||||
});
|
||||
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
|
||||
data: {
|
||||
...evType,
|
||||
bookingLimits: undefined,
|
||||
durationLimits: undefined,
|
||||
recurringEvent: undefined,
|
||||
scheduleId: undefined,
|
||||
hidden: false,
|
||||
length: 30,
|
||||
hashedLink: undefined,
|
||||
},
|
||||
where: {
|
||||
userId_parentId: {
|
||||
@@ -379,6 +388,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
updatedValues: {},
|
||||
});
|
||||
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
@@ -404,13 +414,8 @@ describe("handleChildrenEventTypes", () => {
|
||||
});
|
||||
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
|
||||
data: {
|
||||
...evType,
|
||||
bookingLimits: undefined,
|
||||
durationLimits: undefined,
|
||||
recurringEvent: undefined,
|
||||
hidden: false,
|
||||
hashedLink: undefined,
|
||||
workflows: undefined,
|
||||
scheduleId: undefined,
|
||||
},
|
||||
where: {
|
||||
userId_parentId: {
|
||||
|
||||
@@ -36,6 +36,7 @@ interface handleChildrenEventTypesProps {
|
||||
}[]
|
||||
| undefined;
|
||||
prisma: PrismaClient | DeepMockProxy<PrismaClient>;
|
||||
updatedValues: Prisma.EventTypeUpdateInput;
|
||||
}
|
||||
|
||||
const sendAllSlugReplacementEmails = async (
|
||||
@@ -95,6 +96,7 @@ export default async function handleChildrenEventTypes({
|
||||
children,
|
||||
prisma,
|
||||
profileId,
|
||||
updatedValues,
|
||||
}: handleChildrenEventTypesProps) {
|
||||
// Check we are dealing with a managed event type
|
||||
if (updatedEventType?.schedulingType !== SchedulingType.MANAGED)
|
||||
@@ -114,8 +116,12 @@ export default async function handleChildrenEventTypes({
|
||||
message: "Missing event type",
|
||||
};
|
||||
|
||||
// Define what values are expected to be changed from a managed event type
|
||||
const allManagedEventTypePropsZod = _EventTypeModel.pick(allManagedEventTypeProps);
|
||||
// bookingFields is expected to be filled by the _EventTypeModel but is null at create event
|
||||
const _ManagedEventTypeModel = _EventTypeModel.extend({
|
||||
bookingFields: _EventTypeModel.shape.bookingFields.nullish(),
|
||||
});
|
||||
|
||||
const allManagedEventTypePropsZod = _ManagedEventTypeModel.pick(allManagedEventTypeProps); //FIXME
|
||||
const managedEventTypeValues = allManagedEventTypePropsZod
|
||||
.omit(unlockedManagedEventTypeProps)
|
||||
.parse(eventType);
|
||||
@@ -130,14 +136,12 @@ export default async function handleChildrenEventTypes({
|
||||
const unlockedEventTypeValues = allManagedEventTypePropsZod
|
||||
.pick(unlockedManagedEventTypeProps)
|
||||
.parse(eventType);
|
||||
|
||||
// Calculate if there are new/existent/deleted children users for which the event type needs to be created/updated/deleted
|
||||
const previousUserIds = oldEventType.children?.flatMap((ch) => ch.userId ?? []);
|
||||
const currentUserIds = children?.map((ch) => ch.owner.id);
|
||||
const deletedUserIds = previousUserIds?.filter((id) => !currentUserIds?.includes(id));
|
||||
const newUserIds = currentUserIds?.filter((id) => !previousUserIds?.includes(id));
|
||||
const oldUserIds = currentUserIds?.filter((id) => previousUserIds?.includes(id));
|
||||
|
||||
// Calculate if there are new workflows for which assigned members will get too
|
||||
const currentWorkflowIds = eventType.workflows?.map((wf) => wf.workflowId);
|
||||
|
||||
@@ -227,18 +231,9 @@ export default async function handleChildrenEventTypes({
|
||||
},
|
||||
},
|
||||
data: {
|
||||
...managedEventTypeValues,
|
||||
profileId: profileId ?? null,
|
||||
hidden: children?.find((ch) => ch.owner.id === userId)?.hidden ?? false,
|
||||
bookingLimits:
|
||||
(managedEventTypeValues.bookingLimits as unknown as Prisma.InputJsonObject) ?? undefined,
|
||||
onlyShowFirstAvailableSlot: managedEventTypeValues.onlyShowFirstAvailableSlot ?? false,
|
||||
recurringEvent:
|
||||
(managedEventTypeValues.recurringEvent as unknown as Prisma.InputJsonValue) ?? undefined,
|
||||
metadata: (managedEventTypeValues.metadata as Prisma.InputJsonValue) ?? undefined,
|
||||
bookingFields: (managedEventTypeValues.bookingFields as Prisma.InputJsonValue) ?? undefined,
|
||||
durationLimits: (managedEventTypeValues.durationLimits as Prisma.InputJsonValue) ?? undefined,
|
||||
...updatedValues,
|
||||
hashedLink: hashedLinkQuery(userId),
|
||||
hidden: children?.find((ch) => ch.owner.id === userId)?.hidden ?? false,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -436,7 +436,7 @@ function FieldEditDialog({
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
fieldForm.setValue("type", value);
|
||||
fieldForm.setValue("type", value, { shouldDirty: true });
|
||||
}}
|
||||
value={fieldTypesConfigMap[fieldForm.getValues("type")]}
|
||||
options={fieldTypes.filter((f) => !f.systemOnly)}
|
||||
@@ -451,7 +451,9 @@ function FieldEditDialog({
|
||||
{...fieldForm.register("name")}
|
||||
containerClassName="mt-6"
|
||||
onChange={(e) => {
|
||||
fieldForm.setValue("name", getFieldIdentifier(e.target.value || ""));
|
||||
fieldForm.setValue("name", getFieldIdentifier(e.target.value || ""), {
|
||||
shouldDirty: true,
|
||||
});
|
||||
}}
|
||||
disabled={
|
||||
fieldForm.getValues("editable") === "system" ||
|
||||
|
||||
@@ -341,6 +341,13 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
const updatedValues = Object.entries(data).reduce((acc, [key, value]) => {
|
||||
if (value !== undefined) {
|
||||
// @ts-expect-error Element implicitly has any type
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Handling updates to children event types (managed events types)
|
||||
await updateChildrenEventTypes({
|
||||
@@ -353,6 +360,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
children,
|
||||
profileId: ctx.user.profile.id,
|
||||
prisma: ctx.prisma,
|
||||
updatedValues,
|
||||
});
|
||||
|
||||
const res = ctx.res as NextApiResponse;
|
||||
|
||||
Reference in New Issue
Block a user