fix: resolve infinite loop and week calculation issues in EventModal

- Fix infinite loop in EventModal useEffect by using useRef for userPersonnalCalendars
- Fix weekStart calculation in putEventAsync to use Monday as first day (consistent with FullCalendar firstDay=1)
- Add computeWeekRange utility function for consistent week calculations
- Improve state reset logic when closing EventModal popup
- Ensure repeat checkbox state is properly reset to prevent stale repetition data
This commit is contained in:
lenhanphung
2025-10-03 17:22:19 +07:00
parent f245991816
commit 4ef3a887ee
3 changed files with 122 additions and 62 deletions
+10 -11
View File
@@ -12,7 +12,7 @@ import {
import { getOpenPaasUser, getUserDetails } from "../User/userAPI";
import { parseCalendarEvent } from "../Events/eventUtils";
import { deleteEvent, getEvent, moveEvent, putEvent } from "../Events/EventApi";
import { formatDateToYYYYMMDDTHHMMSS } from "../../utils/dateUtils";
import { formatDateToYYYYMMDDTHHMMSS, computeWeekRange } from "../../utils/dateUtils";
import { User } from "../../components/Attendees/PeopleSearch";
export const getCalendarsListAsync = createAsyncThunk<
@@ -124,12 +124,8 @@ export const putEventAsync = createAsyncThunk<
await putEvent(newEvent, cal.ownerEmails ? cal.ownerEmails[0] : undefined);
const eventDate = new Date(newEvent.start);
const weekStart = new Date(eventDate);
weekStart.setHours(0, 0, 0, 0);
weekStart.setDate(eventDate.getDate() - eventDate.getDay());
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekStart.getDate() + 7);
// Calculate week range based on Monday as first day (consistent with FullCalendar firstDay={1})
const { start: weekStart, end: weekEnd } = computeWeekRange(eventDate);
const calEvents = (await getCalendar(cal.id, {
start: formatDateToYYYYMMDDTHHMMSS(weekStart),
@@ -210,11 +206,14 @@ export const moveEventAsync = createAsyncThunk<
{ cal: Calendars; newEvent: CalendarEvent; newURL: string } // Arg type
>("calendars/moveEvent", async ({ cal, newEvent, newURL }) => {
await moveEvent(newEvent, newURL);
// Calculate week range based on Monday as first day (consistent with FullCalendar firstDay={1})
const eventDate = new Date(newEvent.start);
const { start: weekStart, end: weekEnd } = computeWeekRange(eventDate);
const calEvents = (await getCalendar(cal.id, {
start: formatDateToYYYYMMDDTHHMMSS(new Date(newEvent.start)),
end: formatDateToYYYYMMDDTHHMMSS(
new Date(new Date(newEvent.start).getTime() + 86400000)
),
start: formatDateToYYYYMMDDTHHMMSS(weekStart),
end: formatDateToYYYYMMDDTHHMMSS(weekEnd),
})) as Record<string, any>;
const events: CalendarEvent[] = calEvents._embedded["dav:item"].flatMap(
(eventdata: any) => {
+105 -51
View File
@@ -23,7 +23,7 @@ import {
ContentCopy as CopyIcon,
Close as DeleteIcon,
} from "@mui/icons-material";
import React, { useEffect, useState, useMemo } from "react";
import React, { useEffect, useState, useMemo, useCallback, useRef } from "react";
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import AttendeeSelector from "../../components/Attendees/AttendeeSearch";
import { ResponsiveDialog } from "../../components/Dialog";
@@ -129,6 +129,7 @@ function EventPopover({
selectPersonnalCalendars
);
// Helper function to resolve timezone aliases
const resolveTimezone = (tzName: string): string => {
if (TIMEZONES.zones[tzName]) {
@@ -209,6 +210,37 @@ function EventPopover({
event?.x_openpass_videoconference || null
);
// Use ref to track if we've already initialized to avoid infinite loop
const isInitializedRef = useRef(false);
const userPersonnalCalendarsRef = useRef(userPersonnalCalendars);
// Update ref when userPersonnalCalendars changes
useEffect(() => {
userPersonnalCalendarsRef.current = userPersonnalCalendars;
}, [userPersonnalCalendars]);
const resetAllStateToDefault = useCallback(() => {
setShowMore(false);
setShowDescription(false);
setShowRepeat(false);
setTitle("");
setDescription("");
setAttendees([]);
setLocation("");
setStart("");
setEnd("");
setCalendarid(0);
setAllDay(false);
setRepetition({} as RepetitionObject);
setAlarm("");
setEventClass("PUBLIC");
setBusy("OPAQUE");
setImportant(false);
setTimezone(timezoneList.browserTz);
setHasVideoConference(false);
setMeetingLink(null);
}, [timezoneList.browserTz]);
useEffect(() => {
if (selectedRange) {
setStart(selectedRange ? formatLocalDateTime(selectedRange.start) : "");
@@ -216,33 +248,78 @@ function EventPopover({
}
}, [selectedRange]);
// Initialize state when event prop changes
useEffect(() => {
setTitle(event?.title ?? "");
setAttendees(
event?.attendee
? event.attendee.filter((a) => a.cal_address !== organizer?.cal_address)
: []
);
// Update video conference state when editing different events
setHasVideoConference(event?.x_openpass_videoconference ? true : false);
setMeetingLink(event?.x_openpass_videoconference || null);
// Update description to include video conference footer if exists
if (event?.x_openpass_videoconference && event?.description) {
const hasVideoFooter = event.description.includes("Visio:");
if (!hasVideoFooter) {
setDescription(
addVideoConferenceToDescription(
event.description,
event.x_openpass_videoconference
)
);
} else {
setDescription(event.description);
if (event) {
// Editing existing event - populate fields with event data
setTitle(event.title ?? "");
setDescription(event.description ?? "");
setLocation(event.location ?? "");
setStart(event.start ? event.start : "");
setEnd(event.end ? event.end : "");
setCalendarid(
event.calId
? userPersonnalCalendarsRef.current.findIndex((e) => e.id === event.calId)
: 0
);
setAllDay(event.allday ?? false);
setRepetition(event.repetition ?? ({} as RepetitionObject));
setShowRepeat(event.repetition?.freq ? true : false);
setAttendees(
event.attendee
? event.attendee.filter((a) => a.cal_address !== organizer?.cal_address)
: []
);
setAlarm(event.alarm?.trigger ?? "");
setEventClass(event.class ?? "PUBLIC");
setBusy(event.transp ?? "OPAQUE");
setTimezone(event.timezone ? resolveTimezone(event.timezone) : timezoneList.browserTz);
setHasVideoConference(event.x_openpass_videoconference ? true : false);
setMeetingLink(event.x_openpass_videoconference || null);
// Update description to include video conference footer if exists
if (event.x_openpass_videoconference && event.description) {
const hasVideoFooter = event.description.includes("Visio:");
if (!hasVideoFooter) {
setDescription(
addVideoConferenceToDescription(
event.description,
event.x_openpass_videoconference
)
);
} else {
setDescription(event.description);
}
}
} else {
setDescription(event?.description ?? "");
}
}, [event, organizer?.cal_address]);
}, [event, organizer?.cal_address, timezoneList.browserTz]);
// Reset state when creating new event (event is undefined)
useEffect(() => {
if (!event && isInitializedRef.current) {
// Creating new event - reset all fields to default
setShowMore(false);
setShowDescription(false);
setShowRepeat(false);
setTitle("");
setDescription("");
setAttendees([]);
setLocation("");
setStart("");
setEnd("");
setCalendarid(0);
setAllDay(false);
setRepetition({} as RepetitionObject);
setAlarm("");
setEventClass("PUBLIC");
setBusy("OPAQUE");
setImportant(false);
setTimezone(timezoneList.browserTz);
setHasVideoConference(false);
setMeetingLink(null);
}
isInitializedRef.current = true;
}, [event, timezoneList.browserTz]);
const handleAddVideoConference = () => {
const newMeetingLink = generateMeetingLink();
@@ -280,19 +357,7 @@ function EventPopover({
const handleClose = () => {
onClose({}, "backdropClick");
// Reset state
setShowMore(false);
setShowDescription(false);
setShowRepeat(false);
setTitle("");
setDescription("");
setAttendees([]);
setLocation("");
setCalendarid(0);
setImportant(false);
setTimezone(timezoneList.browserTz);
setHasVideoConference(false);
setMeetingLink(null);
resetAllStateToDefault();
};
const handleSave = async () => {
@@ -337,19 +402,8 @@ function EventPopover({
// Close popup immediately
onClose({}, "backdropClick");
// Reset state
setShowMore(false);
setShowDescription(false);
setShowRepeat(false);
setTitle("");
setDescription("");
setAttendees([]);
setLocation("");
setCalendarid(0);
setImportant(false);
setTimezone(timezoneList.browserTz);
setHasVideoConference(false);
setMeetingLink(null);
// Reset all state to default values
resetAllStateToDefault();
// Save to API in background
dispatch(
+7
View File
@@ -52,3 +52,10 @@ export const computeStartOfTheWeek = (date: Date): Date => {
startOfWeek.setHours(0, 0, 0, 0);
return startOfWeek;
};
export const computeWeekRange = (date: Date): { start: Date; end: Date } => {
const weekStart = computeStartOfTheWeek(date);
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekStart.getDate() + 7);
return { start: weekStart, end: weekEnd };
};