From 4ef3a887eed8a1e697a9874a1624dcab474a561c Mon Sep 17 00:00:00 2001 From: lenhanphung Date: Fri, 3 Oct 2025 17:22:19 +0700 Subject: [PATCH] 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 --- src/features/Calendars/CalendarSlice.ts | 21 ++-- src/features/Events/EventModal.tsx | 156 ++++++++++++++++-------- src/utils/dateUtils.ts | 7 ++ 3 files changed, 122 insertions(+), 62 deletions(-) diff --git a/src/features/Calendars/CalendarSlice.ts b/src/features/Calendars/CalendarSlice.ts index 3db77ed..145787a 100644 --- a/src/features/Calendars/CalendarSlice.ts +++ b/src/features/Calendars/CalendarSlice.ts @@ -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; const events: CalendarEvent[] = calEvents._embedded["dav:item"].flatMap( (eventdata: any) => { diff --git a/src/features/Events/EventModal.tsx b/src/features/Events/EventModal.tsx index cc27a23..6c075a1 100644 --- a/src/features/Events/EventModal.tsx +++ b/src/features/Events/EventModal.tsx @@ -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( diff --git a/src/utils/dateUtils.ts b/src/utils/dateUtils.ts index 3a89796..2c6846d 100644 --- a/src/utils/dateUtils.ts +++ b/src/utils/dateUtils.ts @@ -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 }; +};