From ee286a600f3b0104d8862cf7db9470a9eb087f8a Mon Sep 17 00:00:00 2001
From: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Date: Fri, 16 Feb 2024 02:44:20 +0400
Subject: [PATCH] refactor: Only send dirty fields for event type update call
(#13292)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* 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
Co-authored-by: Udit Takkar
Co-authored-by: CarinaWolli
---
.../components/eventtype/EventAdvancedTab.tsx | 18 +-
.../web/components/eventtype/EventAppsTab.tsx | 12 +-
.../eventtype/EventAvailabilityTab.tsx | 8 +-
.../components/eventtype/EventLimitsTab.tsx | 73 ++++---
.../components/eventtype/EventSetupTab.tsx | 18 +-
.../web/components/eventtype/EventTeamTab.tsx | 31 +--
.../eventtype/EventTypeSingleLayout.tsx | 112 +++++-----
.../eventtype/InstantEventController.tsx | 4 +-
.../eventtype/RecurringEventController.tsx | 10 +-
.../RequiresConfirmationController.tsx | 21 +-
.../views/event-types-single-view.tsx | 199 ++++++++++++++----
.../test/lib/handleChildrenEventTypes.test.ts | 33 +--
.../lib/handleChildrenEventTypes.ts | 25 +--
.../features/form-builder/FormBuilder.tsx | 6 +-
.../viewer/eventTypes/update.handler.ts | 8 +
15 files changed, 382 insertions(+), 196 deletions(-)
diff --git a/apps/web/components/eventtype/EventAdvancedTab.tsx b/apps/web/components/eventtype/EventAdvancedTab.tsx
index 8a023b8bbe..d45247eb24 100644
--- a/apps/web/components/eventtype/EventAdvancedTab.tsx
+++ b/apps/web/components/eventtype/EventAdvancedTab.tsx
@@ -59,7 +59,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick {
bookingFields[name] = `${name} input`;
});
@@ -101,7 +100,8 @@ export const EventAdvancedTab = ({ eventType, team }: Pick {
setUseEventTypeDestinationCalendarEmail(val);
- formMethods.setValue("useEventTypeDestinationCalendarEmail", val);
+ formMethods.setValue("useEventTypeDestinationCalendarEmail", val, { shouldDirty: true });
}}
/>
@@ -319,7 +319,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick {
- formMethods.setValue("hashedLink", e ? hashedUrl : undefined);
+ formMethods.setValue("hashedLink", e ? hashedUrl : undefined, { shouldDirty: true });
setHashedLinkVisible(e);
}}>
@@ -383,10 +383,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick
formMethods.setValue("eventName", val)}
+ setValue={(val: string) => formMethods.setValue("eventName", val, { shouldDirty: true })}
defaultValue={formMethods.getValues("eventName")}
placeHolder={eventNamePlaceholder}
event={eventNameObject}
diff --git a/apps/web/components/eventtype/EventAppsTab.tsx b/apps/web/components/eventtype/EventAppsTab.tsx
index 3246a54198..3612bc827a 100644
--- a/apps/web/components/eventtype/EventAppsTab.tsx
+++ b/apps/web/components/eventtype/EventAppsTab.tsx
@@ -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 => {
diff --git a/apps/web/components/eventtype/EventAvailabilityTab.tsx b/apps/web/components/eventtype/EventAvailabilityTab.tsx
index de6dc35fbf..a992bf4942 100644
--- a/apps/web/components/eventtype/EventAvailabilityTab.tsx
+++ b/apps/web/components/eventtype/EventAvailabilityTab.tsx
@@ -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")}
diff --git a/apps/web/components/eventtype/EventLimitsTab.tsx b/apps/web/components/eventtype/EventLimitsTab.tsx
index 1c1d35b421..12ba08738e 100644
--- a/apps/web/components/eventtype/EventLimitsTab.tsx
+++ b/apps/web/components/eventtype/EventLimitsTab.tsx
@@ -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 = () => {
@@ -424,7 +459,7 @@ function EventTypeSingleLayout({
id="hiddenSwitch"
checked={!formMethods.watch("hidden")}
onCheckedChange={(e) => {
- formMethods.setValue("hidden", !e);
+ formMethods.setValue("hidden", !e, { shouldDirty: true });
}}
/>
@@ -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({
-
-
-
+
+
);
diff --git a/apps/web/components/eventtype/InstantEventController.tsx b/apps/web/components/eventtype/InstantEventController.tsx
index d3ad1d7355..49f3c32dc2 100644
--- a/apps/web/components/eventtype/InstantEventController.tsx
+++ b/apps/web/components/eventtype/InstantEventController.tsx
@@ -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);
}
}}>
diff --git a/apps/web/components/eventtype/RecurringEventController.tsx b/apps/web/components/eventtype/RecurringEventController.tsx
index ee4c3320ff..73f3eecb0d 100644
--- a/apps/web/components/eventtype/RecurringEventController.tsx
+++ b/apps/web/components/eventtype/RecurringEventController.tsx
@@ -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);
}}
/>
diff --git a/apps/web/components/eventtype/RequiresConfirmationController.tsx b/apps/web/components/eventtype/RequiresConfirmationController.tsx
index 2d46a6fd74..ff58a7c8e9 100644
--- a/apps/web/components/eventtype/RequiresConfirmationController.tsx
+++ b/apps/web/components/eventtype/RequiresConfirmationController.tsx
@@ -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);
}}>
@@ -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}
diff --git a/apps/web/modules/event-types/views/event-types-single-view.tsx b/apps/web/modules/event-types/views/event-types-single-view.tsx
index 9945df548c..f48d716bd9 100644
--- a/apps/web/modules/event-types/views/event-types-single-view.tsx
+++ b/apps/web/modules/event-types/views/event-types-single-view.tsx
@@ -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
();
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({
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: ,
} as const;
+ const isObject = (value: T): boolean => {
+ return value !== null && typeof value === "object" && !Array.isArray(value);
+ };
+
+ const isArray = (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 => {
+ if (!isFormDirty) {
+ return {};
+ }
+ const updatedFields: Partial = {};
+ 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([]);
@@ -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 });
+ }
}}>
{tabMap[tabName]}
diff --git a/apps/web/test/lib/handleChildrenEventTypes.test.ts b/apps/web/test/lib/handleChildrenEventTypes.test.ts
index 53ad966a61..835dabdb76 100644
--- a/apps/web/test/lib/handleChildrenEventTypes.test.ts
+++ b/apps/web/test/lib/handleChildrenEventTypes.test.ts
@@ -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: {
diff --git a/packages/features/ee/managed-event-types/lib/handleChildrenEventTypes.ts b/packages/features/ee/managed-event-types/lib/handleChildrenEventTypes.ts
index bb4d138891..2a67c6b3f9 100644
--- a/packages/features/ee/managed-event-types/lib/handleChildrenEventTypes.ts
+++ b/packages/features/ee/managed-event-types/lib/handleChildrenEventTypes.ts
@@ -36,6 +36,7 @@ interface handleChildrenEventTypesProps {
}[]
| undefined;
prisma: PrismaClient | DeepMockProxy;
+ 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,
},
});
})
diff --git a/packages/features/form-builder/FormBuilder.tsx b/packages/features/form-builder/FormBuilder.tsx
index 230b56743c..1ffb1104f2 100644
--- a/packages/features/form-builder/FormBuilder.tsx
+++ b/packages/features/form-builder/FormBuilder.tsx
@@ -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" ||
diff --git a/packages/trpc/server/routers/viewer/eventTypes/update.handler.ts b/packages/trpc/server/routers/viewer/eventTypes/update.handler.ts
index 55b1a66c83..ce40d648d1 100644
--- a/packages/trpc/server/routers/viewer/eventTypes/update.handler.ts
+++ b/packages/trpc/server/routers/viewer/eventTypes/update.handler.ts
@@ -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;