[#446] removed direct state update (#468)

This commit is contained in:
Camille Moussu
2026-01-21 15:17:49 +01:00
committed by GitHub
parent 23d50f250d
commit 00742119be
38 changed files with 119 additions and 1366 deletions
+9 -85
View File
@@ -23,12 +23,6 @@ import {
updateSeriesAsync,
} from "./services";
// Define error type for rejected actions
export interface RejectedError {
message: string;
status?: number;
}
const CalendarSlice = createSlice({
name: "calendars",
initialState: {
@@ -182,42 +176,10 @@ const CalendarSlice = createSlice({
);
}
)
.addCase(
putEventAsync.fulfilled,
(
state,
action: PayloadAction<{
calId: string;
events: CalendarEvent[];
calType?: "temp";
}>
) => {
state.pending = false;
const type = action.payload.calType === "temp" ? "templist" : "list";
if (!state[type][action.payload.calId]) {
state[type][action.payload.calId] = {
id: action.payload.calId,
events: {},
} as Calendar;
}
action.payload.events.forEach((event) => {
state[type][action.payload.calId].events[event.uid] = event;
});
Object.keys(state[type][action.payload.calId].events).forEach(
(id) => {
state[type][action.payload.calId].events[id].color =
state[type][action.payload.calId].color;
state[type][action.payload.calId].events[id].calId =
action.payload.calId;
if (!state[type][action.payload.calId].events[id].timezone) {
state[type][action.payload.calId].events[id].timezone =
browserDefaultTimeZone;
}
}
);
}
)
.addCase(putEventAsync.fulfilled, (state) => {
state.pending = false;
state.error = null;
})
.addCase(
getEventAsync.fulfilled,
(
@@ -236,50 +198,12 @@ const CalendarSlice = createSlice({
action.payload.event;
}
)
.addCase(
moveEventAsync.fulfilled,
(
state,
action: PayloadAction<{ calId: string; events: CalendarEvent[] }>
) => {
state.pending = false;
if (!state.list[action.payload.calId]) {
state.list[action.payload.calId] = {
id: action.payload.calId,
events: {},
} as Calendar;
}
action.payload.events.forEach((event) => {
state.list[action.payload.calId].events[event.uid] = event;
});
Object.keys(state.list[action.payload.calId].events).forEach((id) => {
state.list[action.payload.calId].events[id].color =
state.list[action.payload.calId].color;
state.list[action.payload.calId].events[id].calId =
action.payload.calId;
if (!state.list[action.payload.calId].events[id].timezone) {
state.list[action.payload.calId].events[id].timezone =
browserDefaultTimeZone;
}
});
}
)
.addCase(deleteEventAsync.fulfilled, (state, action) => {
.addCase(moveEventAsync.fulfilled, (state) => {
state.pending = false;
state.error = null;
})
.addCase(deleteEventAsync.fulfilled, (state) => {
state.pending = false;
const [baseId, recurrenceId] = action.payload.eventId.split("/");
if (recurrenceId) {
Object.keys(state.list[action.payload.calId].events).forEach(
(element) => {
if (extractEventBaseUuid(element) === baseId) {
delete state.list[action.payload.calId].events[element];
}
}
);
} else {
delete state.list[action.payload.calId].events[
action.payload.eventId
];
}
state.error = null;
})
.addCase(deleteEventInstanceAsync.fulfilled, (state, action) => {
@@ -2,7 +2,7 @@ import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit";
import { getUserDetails } from "@/features/User/userAPI";
import { addSharedCalendar } from "../CalendarApi";
import { RejectedError } from "../CalendarSlice";
import { RejectedError } from "../types/RejectedError";
export const addSharedCalendarAsync = createAsyncThunk<
{
@@ -1,8 +1,9 @@
import { getUserDetails } from "@/features/User/userAPI";
import { userData } from "@/features/User/userDataTypes";
import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit";
import { postCalendar } from "../CalendarApi";
import { RejectedError } from "../CalendarSlice";
import { RejectedError } from "../types/RejectedError";
export const createCalendarAsync = createAsyncThunk<
{
@@ -15,7 +16,7 @@ export const createCalendarAsync = createAsyncThunk<
ownerEmails: string[];
},
{
userId: string;
userData: userData;
calId: string;
color: Record<string, string>;
name: string;
@@ -24,21 +25,25 @@ export const createCalendarAsync = createAsyncThunk<
{ rejectValue: RejectedError }
>(
"calendars/createCalendar",
async ({ userId, calId, color, name, desc }, { rejectWithValue }) => {
async ({ userData, calId, color, name, desc }, { rejectWithValue }) => {
try {
await postCalendar(userId, calId, color, name, desc);
const ownerData: any = await getUserDetails(userId.split("/")[0]);
if (!userData.openpaasId) {
throw new Error("No openpaasId");
}
await postCalendar(userData.openpaasId, calId, color, name, desc);
const owner = [userData.given_name, userData.family_name]
.filter(Boolean)
.join(" ");
return {
userId,
userId: userData.openpaasId,
calId,
color,
name,
desc,
owner: [ownerData.firstname, ownerData.lastname]
.filter(Boolean)
.join(" "),
ownerEmails: ownerData.emails ?? [],
owner,
ownerEmails: userData.email ? [userData.email] : [],
};
} catch (err: any) {
return rejectWithValue({
@@ -1,7 +1,7 @@
import { deleteEvent } from "@/features/Events/EventApi";
import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit";
import { RejectedError } from "../CalendarSlice";
import { RejectedError } from "../types/RejectedError";
export const deleteEventAsync = createAsyncThunk<
{ calId: string; eventId: string },
@@ -2,7 +2,7 @@ import { deleteEventInstance } from "@/features/Events/EventApi";
import { CalendarEvent } from "@/features/Events/EventsTypes";
import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit";
import { RejectedError } from "../CalendarSlice";
import { RejectedError } from "../types/RejectedError";
import { Calendar } from "../CalendarTypes";
export const deleteEventInstanceAsync = createAsyncThunk<
@@ -2,7 +2,7 @@ import { CalendarEvent } from "@/features/Events/EventsTypes";
import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit";
import { getCalendar } from "../CalendarApi";
import { RejectedError } from "../CalendarSlice";
import { RejectedError } from "../types/RejectedError";
import { extractCalendarEvents } from "../utils/extractCalendarEvents";
export const getCalendarDetailAsync = createAsyncThunk<
@@ -2,7 +2,7 @@ import { getOpenPaasUser, getUserDetails } from "@/features/User/userAPI";
import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit";
import { getCalendars } from "../CalendarApi";
import { RejectedError } from "../CalendarSlice";
import { RejectedError } from "../types/RejectedError";
import { Calendar } from "../CalendarTypes";
import { normalizeCalendar } from "../utils/normalizeCalendar";
@@ -2,7 +2,7 @@ import { getEvent } from "@/features/Events/EventApi";
import { CalendarEvent } from "@/features/Events/EventsTypes";
import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit";
import { RejectedError } from "../CalendarSlice";
import { RejectedError } from "../types/RejectedError";
export const getEventAsync = createAsyncThunk<
{ calId: string; event: CalendarEvent },
@@ -4,7 +4,7 @@ import { getUserDetails } from "@/features/User/userAPI";
import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit";
import { getCalendars } from "../CalendarApi";
import { RejectedError } from "../CalendarSlice";
import { RejectedError } from "../types/RejectedError";
import { Calendar } from "../CalendarTypes";
export const getTempCalendarsListAsync = createAsyncThunk<
@@ -14,9 +14,14 @@ export const getTempCalendarsListAsync = createAsyncThunk<
>("calendars/getTempCalendars", async (tempUser, { rejectWithValue }) => {
try {
const importedCalendars: Record<string, Calendar> = {};
if (!tempUser.openpaasId) {
const username = tempUser.displayName || tempUser.email || "User";
throw new Error(
`TRANSLATION:calendar.userDoesNotHaveValidId|name=${encodeURIComponent(username)}`
);
}
const calendars = (await getCalendars(
tempUser.openpaasId ?? "",
tempUser.openpaasId,
"sharedPublic=true&"
)) as Record<string, any>;
@@ -2,7 +2,7 @@ import { importEventFromFile } from "@/features/Events/EventApi";
import { importFile } from "@/utils/apiUtils";
import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit";
import { RejectedError } from "../CalendarSlice";
import { RejectedError } from "../types/RejectedError";
export const importEventFromFileAsync = createAsyncThunk<
void,
@@ -8,11 +8,11 @@ import {
import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit";
import { getCalendar } from "../CalendarApi";
import { RejectedError } from "../CalendarSlice";
import { RejectedError } from "../types/RejectedError";
import { Calendar } from "../CalendarTypes";
export const moveEventAsync = createAsyncThunk<
{ calId: string; events: CalendarEvent[] },
{ calId: string },
{ cal: Calendar; newEvent: CalendarEvent; newURL: string },
{ rejectValue: RejectedError }
>(
@@ -21,31 +21,8 @@ export const moveEventAsync = createAsyncThunk<
try {
await moveEvent(newEvent, newURL);
const eventDate = new Date(newEvent.start);
const { start: weekStart, end: weekEnd } = computeWeekRange(eventDate);
const calEvents = (await getCalendar(cal.id, {
start: formatDateToYYYYMMDDTHHMMSS(weekStart),
end: formatDateToYYYYMMDDTHHMMSS(weekEnd),
})) as Record<string, any>;
const events: CalendarEvent[] = calEvents._embedded["dav:item"].flatMap(
(eventdata: any) => {
const vevents = eventdata.data[2] as any[][];
const eventURL = eventdata._links.self.href;
return vevents.map((vevent: any[]) => {
return parseCalendarEvent(
vevent[1],
cal.color ?? {},
cal.id,
eventURL
);
});
}
);
return {
calId: cal.id,
events,
};
} catch (err: any) {
return rejectWithValue({
@@ -1,7 +1,7 @@
import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit";
import { updateAclCalendar } from "../CalendarApi";
import { RejectedError } from "../CalendarSlice";
import { RejectedError } from "../types/RejectedError";
export const patchACLCalendarAsync = createAsyncThunk<
{
@@ -1,7 +1,7 @@
import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit";
import { proppatchCalendar } from "../CalendarApi";
import { RejectedError } from "../CalendarSlice";
import { RejectedError } from "../types/RejectedError";
export const patchCalendarAsync = createAsyncThunk<
{
@@ -1,18 +1,12 @@
import { putEvent } from "@/features/Events/EventApi";
import { CalendarEvent } from "@/features/Events/EventsTypes";
import { parseCalendarEvent } from "@/features/Events/eventUtils";
import {
computeWeekRange,
formatDateToYYYYMMDDTHHMMSS,
} from "@/utils/dateUtils";
import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit";
import { getCalendar } from "../CalendarApi";
import { RejectedError } from "../CalendarSlice";
import { RejectedError } from "../types/RejectedError";
import { Calendar } from "../CalendarTypes";
export const putEventAsync = createAsyncThunk<
{ calId: string; events: CalendarEvent[]; calType?: "temp" },
{ calId: string; calType?: "temp" },
{ cal: Calendar; newEvent: CalendarEvent; calType?: "temp" },
{ rejectValue: RejectedError }
>(
@@ -23,34 +17,9 @@ export const putEventAsync = createAsyncThunk<
newEvent,
cal.ownerEmails ? cal.ownerEmails[0] : undefined
);
const eventDate = new Date(newEvent.start);
const { start: weekStart, end: weekEnd } = computeWeekRange(eventDate);
const calEvents = (await getCalendar(cal.id, {
start: formatDateToYYYYMMDDTHHMMSS(weekStart),
end: formatDateToYYYYMMDDTHHMMSS(weekEnd),
})) as Record<string, any>;
const events: CalendarEvent[] = calEvents._embedded["dav:item"].flatMap(
(eventdata: any) => {
const vevents = eventdata.data[2] as any[][];
const eventURL = eventdata._links.self.href;
const valarm = eventdata.data[2][0][2][0];
return vevents.map((vevent: any[]) => {
return parseCalendarEvent(
vevent[1],
cal.color ?? {},
cal.id,
eventURL,
valarm
);
});
}
);
return {
calId: cal.id,
events,
calType,
};
} catch (err: any) {
@@ -3,7 +3,7 @@ import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit";
import pMap from "p-map";
import { fetchSyncTokenChanges } from "../api/fetchSyncTokenChanges";
import { RejectedError } from "../CalendarSlice";
import { RejectedError } from "../types/RejectedError";
import { Calendar } from "../CalendarTypes";
import { expandEventFunction } from "../utils/expandEventFunction";
import { processSyncUpdates } from "../utils/processSyncTokenUpdates";
@@ -1,7 +1,7 @@
import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit";
import { removeCalendar } from "../CalendarApi";
import { RejectedError } from "../CalendarSlice";
import { RejectedError } from "../types/RejectedError";
export const removeCalendarAsync = createAsyncThunk<
{
@@ -2,7 +2,7 @@ import { putEventWithOverrides } from "@/features/Events/EventApi";
import { CalendarEvent } from "@/features/Events/EventsTypes";
import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit";
import { RejectedError } from "../CalendarSlice";
import { RejectedError } from "../types/RejectedError";
import { Calendar } from "../CalendarTypes";
export const updateEventInstanceAsync = createAsyncThunk<
@@ -2,7 +2,7 @@ import { updateSeries } from "@/features/Events/EventApi";
import { CalendarEvent } from "@/features/Events/EventsTypes";
import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit";
import { RejectedError } from "../CalendarSlice";
import { RejectedError } from "../types/RejectedError";
import { Calendar } from "../CalendarTypes";
export const updateSeriesAsync = createAsyncThunk<
@@ -0,0 +1,6 @@
// Define error type for rejected actions
export interface RejectedError {
message: string;
status?: number;
}
@@ -1,4 +1,3 @@
import { Calendar } from "@/features/Calendars/CalendarTypes";
import { userData } from "@/features/User/userDataTypes";
import { Box, Typography } from "@linagora/twake-mui";
import { Dispatch, SetStateAction } from "react";
@@ -8,7 +7,6 @@ import { RSVPButton } from "./RSVPButton";
interface AttendanceValidationProps {
contextualizedEvent: ContextualizedEvent;
calendarList: Calendar[];
user: userData | undefined;
setAfterChoiceFunc: Dispatch<SetStateAction<Function | undefined>>;
setOpenEditModePopup: Dispatch<SetStateAction<string | null>>;
@@ -16,7 +14,6 @@ interface AttendanceValidationProps {
export function AttendanceValidation({
contextualizedEvent,
calendarList,
user,
setAfterChoiceFunc,
setOpenEditModePopup,
@@ -36,7 +33,6 @@ export function AttendanceValidation({
const commonButtonProps = {
contextualizedEvent,
user,
calendarList,
setAfterChoiceFunc,
setOpenEditModePopup,
};
@@ -1,5 +1,4 @@
import { useAppDispatch } from "@/app/hooks";
import { Calendar } from "@/features/Calendars/CalendarTypes";
import { PartStat } from "@/features/User/models/attendee";
import { userData } from "@/features/User/userDataTypes";
import { Button } from "@linagora/twake-mui";
@@ -20,7 +19,6 @@ interface RSVPButtonProps {
rsvpValue: PartStat;
contextualizedEvent: ContextualizedEvent;
user: userData | undefined;
calendarList: Calendar[];
setAfterChoiceFunc: Dispatch<SetStateAction<Function | undefined>>;
setOpenEditModePopup: Dispatch<SetStateAction<string | null>>;
}
@@ -29,7 +27,6 @@ export function RSVPButton({
rsvpValue,
contextualizedEvent,
user,
calendarList,
setAfterChoiceFunc,
setOpenEditModePopup,
}: RSVPButtonProps) {
@@ -54,7 +51,6 @@ export function RSVPButton({
rsvpValue,
contextualizedEvent,
user,
calendarList,
setAfterChoiceFunc,
setOpenEditModePopup,
dispatch
@@ -1,6 +1,5 @@
import { AppDispatch } from "@/app/store";
import { handleRSVP } from "@/components/Event/eventHandlers/eventHandlers";
import { Calendar } from "@/features/Calendars/CalendarTypes";
import { PartStat } from "@/features/User/models/attendee";
import { userData } from "@/features/User/userDataTypes";
import { Dispatch, SetStateAction } from "react";
@@ -10,7 +9,6 @@ export async function handleRSVPClick(
rsvp: PartStat,
contextualizedEvent: ContextualizedEvent,
user: userData | undefined,
calendarList: Calendar[],
setAfterChoiceFunc: Dispatch<SetStateAction<Function | undefined>>,
setOpenEditModePopup: Dispatch<SetStateAction<string | null>>,
dispatch: AppDispatch
@@ -19,15 +17,7 @@ export async function handleRSVPClick(
if (isRecurring) {
setAfterChoiceFunc(() => async (type: string) => {
try {
await handleRSVP(
dispatch,
calendar,
user,
event,
rsvp,
type,
calendarList
);
await handleRSVP(dispatch, calendar, user, event, rsvp, type);
} catch (error) {
console.error("Error handling RSVP:", error);
}
+1 -19
View File
@@ -1,17 +1,13 @@
import { useAppDispatch, useAppSelector } from "@/app/hooks";
import { CalendarName } from "@/components/Calendar/CalendarName";
import { getTimezoneOffset } from "@/components/Calendar/TimezoneSelector";
import {
formatEventChipTitle,
updateTempCalendar,
} from "@/components/Calendar/utils/calendarUtils";
import { formatEventChipTitle } from "@/components/Calendar/utils/calendarUtils";
import ResponsiveDialog from "@/components/Dialog/ResponsiveDialog";
import { EditModeDialog } from "@/components/Event/EditModeDialog";
import EventDuplication from "@/components/Event/EventDuplicate";
import { handleDelete } from "@/components/Event/eventHandlers/eventHandlers";
import { InfoRow } from "@/components/Event/InfoRow";
import { renderAttendeeBadge } from "@/components/Event/utils/eventUtils";
import { getCalendarRange } from "@/utils/dateUtils";
import { browserDefaultTimeZone } from "@/utils/timezone";
import { DateSelectArg } from "@fullcalendar/core";
import {
@@ -282,18 +278,6 @@ export default function EventPreviewModal({
(a) => a.cal_address === event.organizer?.cal_address
);
const updateTempList = async () => {
if (calendars.templist) {
const calendarRange = getCalendarRange(new Date(event.start));
await updateTempCalendar(
calendars.templist,
event,
dispatch,
calendarRange
);
}
};
return (
<>
<ResponsiveDialog
@@ -429,8 +413,6 @@ export default function EventPreviewModal({
if (result && typeof result.unwrap === "function") {
await result.unwrap();
}
await updateTempList();
} catch (error) {
console.error("Failed to delete event:", error);
}
+1 -7
View File
@@ -60,12 +60,11 @@ function EventPopover({
event?: CalendarEvent;
}) {
const dispatch = useAppDispatch();
const { t, lang } = useI18n();
const { t } = useI18n();
const organizer = useAppSelector((state) => state.user.organiserData);
const userId =
useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
const tempList = useAppSelector((state) => state.calendars.templist);
const calList = useAppSelector((state) => state.calendars.list);
const selectPersonalCalendars = createSelector(
(state: any) => state.calendars,
@@ -838,11 +837,6 @@ function EventPopover({
}
}
if (tempList) {
const calendarRange = getCalendarRange(new Date(start));
await updateTempCalendar(tempList, newEvent, dispatch, calendarRange);
}
// Clear temp data on successful save
clearEventFormTempData("create");
+5 -29
View File
@@ -65,7 +65,6 @@ function EventUpdateModal({
}) {
const { t } = useI18n();
const dispatch = useAppDispatch();
const tempList = useAppSelector((state) => state.calendars.templist);
const calList = useAppSelector((state) => state.calendars.list);
// Get event from Redux store (cached data) as fallback
const cachedEvent = useAppSelector(
@@ -78,6 +77,10 @@ function EventUpdateModal({
// Use fresh data if available, otherwise use eventData from props, otherwise use cached data
const event = freshEvent || eventData || cachedEvent;
useEffect(() => {
setFreshEvent(null);
}, [eventId, calId]);
// Fetch fresh event data when modal opens
useEffect(() => {
if (open && cachedEvent && !eventData) {
@@ -714,11 +717,6 @@ function EventUpdateModal({
// Clear cache to ensure navigation to other weeks works
dispatch(clearFetchCache(calId));
if (tempList) {
const calendarRange = getCalendarRange(new Date(start));
await updateTempCalendar(tempList, event, dispatch, calendarRange);
}
// STEP 5: Remove old recurring instances only after the rest succeeds
removeSeriesInstancesFromUI();
@@ -834,7 +832,7 @@ function EventUpdateModal({
const repetitionRulesChanged = changes.repetitionRulesChanged;
if (repetitionRulesChanged) {
// Date/time or repetition rules changed - remove all overrides and refresh
// Date/time or repetition rules changed - remove all overrides
const seriesInstancesSnapshot = getSeriesInstances();
@@ -892,23 +890,6 @@ function EventUpdateModal({
}
}
// STEP 3: Fetch to get new instances with correct timing
// If refreshCalendars fails, we need to throw error to reopen modal
try {
const calendarRange = getCalendarRange(new Date(start));
await refreshCalendars(
dispatch,
Object.values(calendarsList),
calendarRange
);
} catch (refreshError: any) {
// If refreshCalendars fails, throw error to reopen modal
throw new Error(
refreshError?.message ||
"Failed to refresh calendar events. Please try again."
);
}
// Clear cache after reload
dispatch(clearFetchCache(calId));
@@ -1085,15 +1066,10 @@ function EventUpdateModal({
if (moveResult && typeof moveResult.unwrap === "function") {
await moveResult.unwrap();
}
dispatch(removeEvent({ calendarUid: calId, eventUid: event.uid }));
// Clear temp data on successful move
clearEventFormTempData("update");
}
if (tempList) {
const calendarRange = getCalendarRange(new Date(start));
await updateTempCalendar(tempList, event, dispatch, calendarRange);
}
// Reset all state to default values only on successful save (after all branches)
clearEventFormTempData("update");