[#421] added eslint check to CI (#534)

Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
Camille Moussu
2026-02-10 10:41:37 +01:00
committed by GitHub
parent 8a6ec8fc39
commit d6e464afad
116 changed files with 1232 additions and 801 deletions
@@ -9,7 +9,9 @@ import { PartStat } from "@/features/User/models/attendee";
interface AttendanceValidationProps {
contextualizedEvent: ContextualizedEvent;
user: userData | undefined;
setAfterChoiceFunc: Dispatch<SetStateAction<Function | undefined>>;
setAfterChoiceFunc: (
func: ((type: "solo" | "all" | undefined) => void) | undefined
) => void;
setOpenEditModePopup: Dispatch<SetStateAction<string | null>>;
}
@@ -1,8 +1,8 @@
import { useAppDispatch } from "@/app/hooks";
import { PartStat } from "@/features/User/models/attendee";
import { userData } from "@/features/User/userDataTypes";
import { Button, CircularProgress, Box, Theme } from "@linagora/twake-mui";
import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react";
import { Box, Button, CircularProgress, Theme } from "@linagora/twake-mui";
import { Dispatch, SetStateAction, useEffect, useRef } from "react";
import { useI18n } from "twake-i18n";
import { ContextualizedEvent } from "../EventsTypes";
import { handleRSVPClick } from "./handleRSVPClick";
@@ -19,7 +19,9 @@ interface RSVPButtonProps {
rsvpValue: PartStat;
contextualizedEvent: ContextualizedEvent;
user: userData | undefined;
setAfterChoiceFunc: Dispatch<SetStateAction<Function | undefined>>;
setAfterChoiceFunc: (
func: ((type: "solo" | "all" | undefined) => void) | undefined
) => void;
setOpenEditModePopup: Dispatch<SetStateAction<string | null>>;
isLoading: boolean;
onLoadingChange: (loading: boolean, value?: PartStat) => void;
@@ -9,7 +9,9 @@ export async function handleRSVPClick(
rsvp: PartStat,
contextualizedEvent: ContextualizedEvent,
user: userData | undefined,
setAfterChoiceFunc: Dispatch<SetStateAction<Function | undefined>>,
setAfterChoiceFunc: (
func: ((type: "solo" | "all" | undefined) => void) | undefined
) => void,
setOpenEditModePopup: Dispatch<SetStateAction<string | null>>,
dispatch: AppDispatch,
onLoadingChange?: (loading: boolean, value?: PartStat) => void
+35 -19
View File
@@ -3,6 +3,7 @@ import { resolveTimezoneId, convertEventDateTimeToISO } from "@/utils/timezone";
import { TIMEZONES } from "@/utils/timezone-data";
import ICAL from "ical.js";
import { CalDavItem } from "../Calendars/api/types";
import { SearchEventsResponse } from "../Search/types/SearchEventsResponse";
import { CalendarEvent } from "./EventsTypes";
import {
calendarEventToJCal,
@@ -11,6 +12,23 @@ import {
parseCalendarEvent,
} from "./eventUtils";
type JCalValue = string | number | boolean | null;
type JCalParams = Record<string, JCalValue | JCalValue[]>;
type JCalProperty = [
name: string,
params: JCalParams,
type: string,
value: JCalValue,
];
type JCalComponent = [
name: string,
properties: JCalProperty[],
components?: JCalComponent[],
];
export async function reportEvent(
event: CalendarEvent,
match: { start: string; end: string }
@@ -43,8 +61,8 @@ export async function getEvent(event: CalendarEvent, isMaster?: boolean) {
let targetVevent;
if (isMaster) {
targetVevent = vevents.find(
([, props]: [string, any[]]) =>
!props.find(([k]: string[]) => k.toLowerCase() === "recurrence-id")
([, props]: JCalComponent) =>
!props.find(([k]) => k.toLowerCase() === "recurrence-id")
);
if (!targetVevent) {
targetVevent = vevents[0];
@@ -152,7 +170,7 @@ export async function putEvent(event: CalendarEvent, calOwnerEmail?: string) {
});
if (response.status === 201) {
console.log("Event created successfully:", response.url || event.URL);
console.info("Event created successfully:", response.url || event.URL);
}
return response;
@@ -198,17 +216,14 @@ export async function putEventWithOverrides(
});
}
export const deleteEventInstance = async (
event: CalendarEvent,
calOwnerEmail?: string
) => {
export const deleteEventInstance = async (event: CalendarEvent) => {
// Get all VEVENTs (master + overrides) from the series
const vevents = await getAllRecurrentEvent(event);
// Find the master VEVENT
const masterIndex = vevents.findIndex(
([, props]: [string, any[]]) =>
!props.find(([k]: string[]) => k.toLowerCase() === "recurrence-id")
([, props]: JCalComponent) =>
!props.find(([k]) => k.toLowerCase() === "recurrence-id")
);
if (masterIndex === -1) {
@@ -220,8 +235,9 @@ export const deleteEventInstance = async (
const masterProps = vevents[masterIndex][1];
// Check if this date is already in EXDATE (avoid duplicates)
const normalizeRecurrenceId = (id: string) => (id ?? "").replace(/Z$/, "");
const isDuplicate = masterProps.some((prop: any[]) => {
const normalizeRecurrenceId = (id: JCalValue) =>
String(id ?? "").replace(/Z$/, "");
const isDuplicate = masterProps.some((prop: JCalProperty) => {
if (prop[0].toLowerCase() === "exdate" && prop[3]) {
return (
normalizeRecurrenceId(prop[3]) === normalizeRecurrenceId(exdateValue)
@@ -240,9 +256,9 @@ export const deleteEventInstance = async (
vevents[masterIndex][1] = masterProps;
// Remove the override instance if it exists (in case it was an override being deleted)
const filteredVevents = vevents.filter(([, props]: [string, any[]]) => {
const filteredVevents = vevents.filter(([, props]: JCalComponent) => {
const recurrenceIdProp = props.find(
([k]: string[]) => k.toLowerCase() === "recurrence-id"
([k]) => k.toLowerCase() === "recurrence-id"
);
if (!recurrenceIdProp) return true; // Keep master
return (
@@ -278,9 +294,9 @@ export const updateSeriesPartstat = async (
const vevents = await getAllRecurrentEvent(event);
// Update PARTSTAT in ALL VEVENTs (master + exceptions)
const updatedVevents = vevents.map((vevent: any[]) => {
const updatedVevents = vevents.map((vevent: JCalComponent) => {
const properties = vevent[1];
const updatedProperties = properties.map((prop: any[]) => {
const updatedProperties = properties.map((prop: JCalProperty) => {
// Find ATTENDEE properties
if (prop[0] === "attendee") {
const calAddress = prop[3];
@@ -321,7 +337,7 @@ export const updateSeries = async (
) => {
const vevents = await getAllRecurrentEvent(event);
const masterIndex = vevents.findIndex(
([, props]: [string, string[]]) =>
([, props]: JCalComponent) =>
!props.find(([k]) => k.toLowerCase() === "recurrence-id")
);
if (masterIndex === -1) {
@@ -404,7 +420,7 @@ export async function searchEvent(
organizers: string[];
attendees: string[];
}
) {
): Promise<SearchEventsResponse> {
const { keywords, searchIn, organizers, attendees } = filters;
const reqParam: {
@@ -413,7 +429,7 @@ export async function searchEvent(
organizers?: string[];
attendees?: string[];
} = {
query: !!keywords ? keywords : query,
query: keywords ? keywords : query,
calendars: searchIn.map((calId) => {
const [userId, calendarId] = calId.split("/");
return { calendarId, userId };
@@ -431,5 +447,5 @@ export async function searchEvent(
})
.json();
return response;
return response as SearchEventsResponse;
}
+22 -19
View File
@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { useAppDispatch, useAppSelector } from "@/app/hooks";
import { CalendarName } from "@/components/Calendar/CalendarName";
import { getTimezoneOffset } from "@/utils/timezone";
@@ -59,7 +60,7 @@ export default function EventPreviewModal({
calId: string;
tempEvent?: boolean;
open: boolean;
onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void;
onClose: (event: unknown, reason: "backdropClick" | "escapeKeyDown") => void;
}) {
const { t } = useI18n();
const dispatch = useAppDispatch();
@@ -67,9 +68,7 @@ export default function EventPreviewModal({
const timezone =
useAppSelector((state) => state.settings.timeZone) ??
browserDefaultTimeZone;
const calendarList = Object.values(
useAppSelector((state) => state.calendars.list)
);
const calendar = tempEvent
? calendars.templist[calId]
: calendars.list[calId];
@@ -95,14 +94,16 @@ export default function EventPreviewModal({
const [typeOfAction, setTypeOfAction] = useState<"solo" | "all" | undefined>(
undefined
);
const [afterChoiceFunc, setAfterChoiceFunc] = useState<Function>();
const [afterChoiceFunc, setAfterChoiceFunc] = useState<
((type: "solo" | "all" | undefined) => void) | undefined
>();
const attendeePreview = makeAttendeePreview(event.attendee, t);
const hasCheckedSessionStorageRef = useRef(false);
const [toggleActionMenu, setToggleActionMenu] = useState<Element | null>(
null
);
const mailSpaUrl = (window as any).MAIL_SPA_URL ?? null;
const mailSpaUrl = window.MAIL_SPA_URL ?? null;
useEffect(() => {
if (!event || !calendar) {
@@ -160,7 +161,7 @@ export default function EventPreviewModal({
}
}
}
} catch (err) {
} catch {
// Ignore sessionStorage errors
}
};
@@ -201,7 +202,7 @@ export default function EventPreviewModal({
// Clear sessionStorage after reopening
try {
sessionStorage.removeItem("eventUpdateModalReopen");
} catch (err) {
} catch {
// Ignore sessionStorage errors
}
}
@@ -241,7 +242,7 @@ export default function EventPreviewModal({
}
}
}
} catch (err) {
} catch {
// Ignore sessionStorage errors
}
};
@@ -296,7 +297,7 @@ export default function EventPreviewModal({
gap={0.5}
width="100%"
>
{(window as any).DEBUG && (
{window.DEBUG && (
<IconButton
size="small"
onClick={async () => {
@@ -373,8 +374,6 @@ export default function EventPreviewModal({
</MenuItem>
)}
<EventDuplication
event={event}
onClose={() => setToggleActionMenu(null)}
onOpenDuplicate={() => {
setToggleActionMenu(null);
setHidePreview(true);
@@ -723,10 +722,11 @@ export default function EventPreviewModal({
<EditModeDialog
type={openEditModePopup}
setOpen={setOpenEditModePopup}
event={event}
eventAction={(type: "solo" | "all" | undefined) => {
setTypeOfAction(type);
afterChoiceFunc && afterChoiceFunc(type);
if (afterChoiceFunc) {
afterChoiceFunc(type);
}
}}
/>
<EventUpdateModal
@@ -759,7 +759,7 @@ export default function EventPreviewModal({
return data.typeOfAction;
}
}
} catch (err) {
} catch {
// Ignore
}
return undefined;
@@ -791,7 +791,7 @@ export default function EventPreviewModal({
function makeRecurrenceString(
event: CalendarEvent,
t: Function
t: (k: string, p?: string | object) => string
): string | undefined {
if (!event.repetition) return;
@@ -851,7 +851,7 @@ function makeRecurrenceString(
function formatDate(
date: Date | string,
t: Function,
t: (k: string, p?: string | object) => string,
timeZone: string,
allday?: boolean
) {
@@ -880,7 +880,7 @@ function formatDate(
function formatEnd(
start: Date | string,
end: Date | string,
t: Function,
t: (k: string, p?: string | object) => string,
timeZone: string,
allday?: boolean
) {
@@ -924,7 +924,10 @@ function formatEnd(
}
}
export function makeAttendeePreview(attendees: userAttendee[], t: Function) {
export function makeAttendeePreview(
attendees: userAttendee[],
t: (k: string, p?: string | object) => string
) {
const attendeePreview = [];
const yesCount = attendees?.filter((a) => a.partstat === "ACCEPTED").length;
const noCount = attendees?.filter((a) => a.partstat === "DECLINED").length;
+22 -18
View File
@@ -1,6 +1,5 @@
import { useAppDispatch, useAppSelector } from "@/app/hooks";
import { getTimezoneOffset, resolveTimezone } from "@/utils/timezone";
import { updateTempCalendar } from "@/components/Calendar/utils/calendarUtils";
import { RootState } from "@/app/store";
import { ResponsiveDialog } from "@/components/Dialog";
import EventFormFields from "@/components/Event/EventFormFields";
import { addDays } from "@/components/Event/utils/dateRules";
@@ -9,7 +8,6 @@ import {
formatLocalDateTime,
} from "@/components/Event/utils/dateTimeFormatters";
import { convertFormDateTimeToISO } from "@/components/Event/utils/dateTimeHelpers";
import { getCalendarRange } from "@/utils/dateUtils";
import {
buildEventFormTempData,
clearEventFormTempData,
@@ -19,7 +17,11 @@ import {
saveEventFormDataToTemp,
showErrorNotification,
} from "@/utils/eventFormTempStorage";
import { browserDefaultTimeZone } from "@/utils/timezone";
import {
browserDefaultTimeZone,
getTimezoneOffset,
resolveTimezone,
} from "@/utils/timezone";
import { TIMEZONES } from "@/utils/timezone-data";
import { addVideoConferenceToDescription } from "@/utils/videoConferenceUtils";
import { CalendarApi, DateSelectArg } from "@fullcalendar/core";
@@ -37,6 +39,7 @@ import React, {
import { useI18n } from "twake-i18n";
import { Calendar } from "../Calendars/CalendarTypes";
import { putEventAsync } from "../Calendars/services";
import { AsyncThunkResult } from "../Calendars/types/AsyncThunkResult";
import { userAttendee } from "../User/models/attendee";
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
@@ -52,7 +55,7 @@ function EventPopover({
open: boolean;
onClose: (refresh?: boolean) => void;
selectedRange: DateSelectArg | null;
setSelectedRange: Function;
setSelectedRange: React.Dispatch<React.SetStateAction<DateSelectArg | null>>;
calendarRef: React.RefObject<CalendarApi | null>;
event?: CalendarEvent;
}) {
@@ -64,8 +67,8 @@ function EventPopover({
useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
const calList = useAppSelector((state) => state.calendars.list);
const selectPersonalCalendars = createSelector(
(state: any) => state.calendars,
(calendars: any) =>
(state: RootState) => state.calendars,
(calendars: RootState["calendars"]) =>
Object.keys(calendars.list || {})
.map((id) => {
if (id.split("/")[0] === userId) {
@@ -704,7 +707,6 @@ function EventPopover({
}, 0);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, event?.uid]);
const handleSave = async () => {
@@ -818,20 +820,20 @@ function EventPopover({
);
// Handle result of putEventAsync - check if rejected first
const typedResult = result as AsyncThunkResult;
// Check if result is a rejected action
if (result.type && result.type.endsWith("/rejected")) {
if (typedResult.type && typedResult.type.endsWith("/rejected")) {
throw new Error(
result.error?.message || result.payload?.message || "API call failed"
typedResult.error?.message ||
typedResult.payload?.message ||
"API call failed"
);
}
// If result has unwrap, call it (it will throw if rejected)
if (result && typeof result.unwrap === "function") {
try {
await result.unwrap();
} catch (unwrapError: any) {
throw unwrapError;
}
if (typedResult && typeof typedResult.unwrap === "function") {
await typedResult.unwrap();
}
// Clear temp data on successful save
@@ -839,7 +841,9 @@ function EventPopover({
// Reset all state to default values only on successful save
resetAllStateToDefault();
} catch (error: any) {
} catch (error) {
const errorObj = error as { message?: string };
// API failed - restore form data and mark as error
const errorFormData = {
...formDataToSave,
@@ -849,7 +853,7 @@ function EventPopover({
// Show error notification
showErrorNotification(
error?.message || "Failed to create event. Please try again."
errorObj.message || "Failed to create event. Please try again."
);
// Try to reopen modal by dispatching custom event
+70 -105
View File
@@ -11,6 +11,7 @@ import {
updateEventInstanceAsync,
updateSeriesAsync,
} from "@/features/Calendars/services";
import { assertThunkSuccess } from "@/utils/assertThunkSuccess";
import {
buildEventFormTempData,
clearEventFormTempData,
@@ -24,8 +25,8 @@ import {
import { extractEventBaseUuid } from "@/utils/extractEventBaseUuid";
import {
browserDefaultTimeZone,
resolveTimezone,
getTimezoneOffset,
resolveTimezone,
} from "@/utils/timezone";
import { TIMEZONES } from "@/utils/timezone-data";
import { addVideoConferenceToDescription } from "@/utils/videoConferenceUtils";
@@ -39,6 +40,7 @@ import {
updateEventLocal,
} from "../Calendars/CalendarSlice";
import { Calendar } from "../Calendars/CalendarTypes";
import { AsyncThunkResult } from "../Calendars/types/AsyncThunkResult";
import { userAttendee } from "../User/models/attendee";
import { deleteEvent, getEvent, putEvent } from "./EventApi";
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
@@ -56,7 +58,7 @@ function EventUpdateModal({
eventId: string;
calId: string;
open: boolean;
onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void;
onClose: (event: unknown, reason: "backdropClick" | "escapeKeyDown") => void;
onCloseAll?: () => void;
eventData?: CalendarEvent | null;
typeOfAction?: "solo" | "all";
@@ -192,7 +194,7 @@ function EventUpdateModal({
};
const fetchedMasterEvent = await getEvent(masterEventToFetch, true);
setMasterEvent(fetchedMasterEvent);
} catch (err: any) {
} catch (err) {
console.error("Failed to fetch master event:", err);
// Fallback to using the clicked instance
setMasterEvent(event);
@@ -335,6 +337,7 @@ function EventUpdateModal({
}
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
open,
event,
@@ -675,16 +678,22 @@ function EventUpdateModal({
const deletePromises = Array.from(uniqueURLs).map(async (url) => {
try {
await deleteEvent(url);
} catch (deleteError: any) {
} catch (deleteError) {
// Check if error is an object with response or message properties
const errorObj = deleteError as {
response?: { status?: number };
message?: string;
};
// Silently ignore 404 - file might already be deleted
const is404 =
deleteError.response?.status === 404 ||
deleteError.message?.includes("404") ||
deleteError.message?.includes("Not Found");
errorObj.response?.status === 404 ||
errorObj.message?.includes("404") ||
errorObj.message?.includes("Not Found");
if (!is404) {
console.error(
`Failed to delete event file: ${deleteError.message}`
`Failed to delete event file: ${errorObj.message || "Unknown error"}`
);
}
}
@@ -724,7 +733,10 @@ function EventUpdateModal({
// Reset all state to default values only on successful save
resetAllStateToDefault();
initializedKeyRef.current = null;
} catch (err: any) {
} catch (err) {
// Check if error is an object with a message property
const errorObj = err as { message?: string };
// API failed - restore form data and mark as error
const errorFormData = {
...formDataToSave,
@@ -734,7 +746,8 @@ function EventUpdateModal({
// Show error notification
showErrorNotification(
err?.message || "Failed to convert recurring event. Please try again."
errorObj.message ||
"Failed to convert recurring event. Please try again."
);
if (createdSingleEventUid) {
@@ -769,7 +782,6 @@ function EventUpdateModal({
if (recurrenceId) {
if (typeOfAction === "solo") {
// Update single instance with optimistic update + rollback
const oldEvent = { ...event };
dispatch(
updateEventLocal({
@@ -778,43 +790,30 @@ function EventUpdateModal({
})
);
try {
const result = await dispatch(
updateEventInstanceAsync({
cal: targetCalendar,
event: { ...newEvent, recurrenceId },
})
);
const result = await dispatch(
updateEventInstanceAsync({
cal: targetCalendar,
event: { ...newEvent, recurrenceId },
})
);
// Handle result of updateEventInstanceAsync
if (result && typeof (result as any).unwrap === "function") {
try {
await (result as any).unwrap();
} catch (unwrapError: any) {
throw unwrapError;
}
} else {
// Check if result is rejected
if (
result.type &&
(result.type as string).endsWith("/rejected")
) {
const rejectedResult = result as any;
throw new Error(
rejectedResult.error?.message ||
rejectedResult.payload?.message ||
"API call failed"
);
}
// Handle result of updateEventInstanceAsync
const typedResult = result as AsyncThunkResult;
if (typedResult && typeof typedResult.unwrap === "function") {
await typedResult.unwrap();
} else {
// Check if result is rejected
if (typedResult.type && typedResult.type.endsWith("/rejected")) {
throw new Error(
typedResult.error?.message ||
typedResult.payload?.message ||
"API call failed"
);
}
// Clear temp data on successful save
clearEventFormTempData("update");
} catch (error: any) {
// Rollback optimistic update
dispatch(updateEventLocal({ calId, event: oldEvent }));
throw error; // Re-throw to be caught by outer catch
}
// Clear temp data on successful save
clearEventFormTempData("update");
} else if (typeOfAction === "all") {
// Update all instances - check if repetition rules changed
const baseUID = extractEventBaseUuid(event.uid);
@@ -872,22 +871,18 @@ function EventUpdateModal({
);
// Handle result of updateSeriesAsync
if (result && typeof (result as any).unwrap === "function") {
try {
await (result as any).unwrap();
} catch (unwrapError: any) {
throw unwrapError;
}
const typedResult = result as AsyncThunkResult;
if (typedResult && typeof typedResult.unwrap === "function") {
await typedResult.unwrap();
} else {
// Check if result is rejected
if (
result.type &&
(result.type as string).endsWith("/rejected")
typedResult.type &&
typedResult.type.endsWith("/rejected")
) {
const rejectedResult = result as any;
throw new Error(
rejectedResult.error?.message ||
rejectedResult.payload?.message ||
typedResult.error?.message ||
typedResult.payload?.message ||
"API call failed"
);
}
@@ -949,26 +944,7 @@ function EventUpdateModal({
);
// Handle result of updateSeriesAsync
if (result && typeof (result as any).unwrap === "function") {
try {
await (result as any).unwrap();
} catch (unwrapError: any) {
throw unwrapError;
}
} else {
// Check if result is rejected
if (
result.type &&
(result.type as string).endsWith("/rejected")
) {
const rejectedResult = result as any;
throw new Error(
rejectedResult.error?.message ||
rejectedResult.payload?.message ||
"API call failed"
);
}
}
assertThunkSuccess(result);
// Clear cache to ensure navigation shows updated data
dispatch(clearFetchCache(calId));
@@ -990,20 +966,7 @@ function EventUpdateModal({
);
// Handle result of putEventAsync - check if rejected first
if (result.type && result.type.endsWith("/rejected")) {
throw new Error(
result.error?.message ||
result.payload?.message ||
"API call failed"
);
}
if (result && typeof result.unwrap === "function") {
try {
await result.unwrap();
} catch (unwrapError: any) {
throw unwrapError;
}
}
assertThunkSuccess(result);
// Remove old single event AFTER new recurring instances are added to store
// This prevents empty grid during the transition
@@ -1024,19 +987,16 @@ function EventUpdateModal({
);
// Handle result of putEventAsync - check if rejected first
if (result.type && result.type.endsWith("/rejected")) {
const typedResult = result as AsyncThunkResult;
if (typedResult.type && typedResult.type.endsWith("/rejected")) {
throw new Error(
result.error?.message ||
result.payload?.message ||
typedResult.error?.message ||
typedResult.payload?.message ||
"API call failed"
);
}
if (result && typeof result.unwrap === "function") {
try {
await result.unwrap();
} catch (unwrapError: any) {
throw unwrapError;
}
if (typedResult && typeof typedResult.unwrap === "function") {
await typedResult.unwrap();
}
// Clear temp data on successful save
@@ -1060,8 +1020,9 @@ function EventUpdateModal({
);
// Handle result of putEventAsync
if (putResult && typeof putResult.unwrap === "function") {
await putResult.unwrap();
const typedPutResult = putResult as AsyncThunkResult;
if (typedPutResult && typeof typedPutResult.unwrap === "function") {
await typedPutResult.unwrap();
}
// Then move it to the new calendar
@@ -1074,8 +1035,9 @@ function EventUpdateModal({
);
// Handle result of moveEventAsync
if (moveResult && typeof moveResult.unwrap === "function") {
await moveResult.unwrap();
const typedMoveResult = moveResult as AsyncThunkResult;
if (typedMoveResult && typeof typedMoveResult.unwrap === "function") {
await typedMoveResult.unwrap();
}
// Clear temp data on successful move
@@ -1086,7 +1048,10 @@ function EventUpdateModal({
clearEventFormTempData("update");
resetAllStateToDefault();
initializedKeyRef.current = null;
} catch (error: any) {
} catch (error) {
// Check if error is an object with a message property
const errorObj = error as { message?: string };
// Handle errors for all branches
// Rollback optimistic updates if any
// API failed - restore form data and mark as error
@@ -1098,7 +1063,7 @@ function EventUpdateModal({
// Show error notification
showErrorNotification(
error?.message || "Failed to update event. Please try again."
errorObj.message || "Failed to update event. Please try again."
);
// Try to reopen modal
+3 -3
View File
@@ -31,10 +31,10 @@ export interface CalendarEvent {
export interface RepetitionObject {
freq: string;
interval?: number;
interval?: number | null;
byday?: string[] | null;
occurrences?: number;
endDate?: string;
occurrences?: number | null;
endDate?: string | null;
}
export interface AlarmObject {
+68 -61
View File
@@ -1,17 +1,19 @@
import { convertFormDateTimeToISO } from "@/components/Event/utils/dateTimeHelpers";
import { extractEventBaseUuid } from "@/utils/extractEventBaseUuid";
import { resolveTimezoneId, convertEventDateTimeToISO } from "@/utils/timezone";
import { convertEventDateTimeToISO, resolveTimezoneId } from "@/utils/timezone";
import { TIMEZONES } from "@/utils/timezone-data";
import ICAL from "ical.js";
import moment from "moment-timezone";
import {
RepetitionRule,
VObjectProperty,
} from "../Calendars/types/CalendarData";
import { userAttendee } from "../User/models/attendee";
import { createAttendee } from "../User/models/attendee.mapper";
import { AlarmObject, CalendarEvent, RepetitionObject } from "./EventsTypes";
type RawEntry = [string, Record<string, string>, string, any];
function inferTimezoneFromValue(
params: Record<string, string> | undefined,
value: string
params: Record<string, string> | undefined
): string | undefined {
if (!params) {
return undefined;
@@ -30,11 +32,11 @@ function inferTimezoneFromValue(
}
export function parseCalendarEvent(
data: RawEntry[],
data: VObjectProperty[],
color: Record<string, string>,
calendarid: string,
eventURL: string,
valarm?: RawEntry[]
valarm?: VObjectProperty[]
): CalendarEvent {
const event: Partial<CalendarEvent> = { color, attendee: [] };
let recurrenceId;
@@ -44,18 +46,20 @@ export function parseCalendarEvent(
for (const [key, params, , value] of data) {
switch (key.toLowerCase()) {
case "uid":
event.uid = value;
event.uid = String(value);
break;
case "transp":
event.transp = value;
event.transp = String(value);
break;
case "dtstart": {
event.start = value;
const detectedTz = inferTimezoneFromValue(params, value);
event.start = String(value);
const detectedTz = inferTimezoneFromValue(
params as Record<string, string>
);
if (detectedTz) {
event.timezone = detectedTz;
}
if (dateRegex.test(value)) {
if (dateRegex.test(String(value))) {
event.allday = true;
} else {
event.allday = false;
@@ -63,14 +67,16 @@ export function parseCalendarEvent(
break;
}
case "dtend": {
event.end = value;
event.end = String(value);
if (!event.timezone) {
const detectedTz = inferTimezoneFromValue(params, value);
const detectedTz = inferTimezoneFromValue(
params as Record<string, string>
);
if (detectedTz) {
event.timezone = detectedTz;
}
}
if (dateRegex.test(value)) {
if (dateRegex.test(String(value))) {
event.allday = true;
} else {
event.allday = false;
@@ -78,50 +84,54 @@ export function parseCalendarEvent(
break;
}
case "class":
event.class = value;
event.class = String(value);
break;
case "x-openpaas-videoconference":
event.x_openpass_videoconference = value;
event.x_openpass_videoconference = String(value);
break;
case "summary":
event.title = value;
event.title = String(value);
break;
case "description":
event.description = value;
event.description = String(value);
break;
case "location":
event.location = value;
event.location = String(value);
break;
case "organizer":
case "organizer": {
const paramsObj = params as Record<string, string>;
event.organizer = {
cn: params?.cn ?? "",
cal_address: value?.replace(/^mailto:/i, ""),
cn: paramsObj?.cn ?? "",
cal_address: String(value).replace(/^mailto:/i, ""),
};
break;
case "attendee":
}
case "attendee": {
const paramsObj = params as Record<string, string>;
(event.attendee as userAttendee[]).push(
createAttendee({
cn: params?.cn,
cal_address: value.replace(/^mailto:/i, ""),
partstat: params?.partstat as userAttendee["partstat"],
rsvp: params?.rsvp as userAttendee["rsvp"],
role: params?.role as userAttendee["role"],
cutype: params?.cutype as userAttendee["cutype"],
cn: paramsObj?.cn,
cal_address: String(value).replace(/^mailto:/i, ""),
partstat: paramsObj?.partstat as userAttendee["partstat"],
rsvp: paramsObj?.rsvp as userAttendee["rsvp"],
role: paramsObj?.role as userAttendee["role"],
cutype: paramsObj?.cutype as userAttendee["cutype"],
})
);
break;
}
case "dtstamp":
event.stamp = value;
event.stamp = String(value);
break;
case "sequence":
event.sequence = Number(value);
break;
case "recurrence-id":
recurrenceId = value;
recurrenceId = String(value);
break;
case "exdate":
if (!event.exdates) event.exdates = [];
event.exdates.push(value);
event.exdates.push(String(value));
break;
case "status":
event.status = String(value);
@@ -129,25 +139,27 @@ export function parseCalendarEvent(
case "duration":
duration = String(value);
break;
case "rrule":
event.repetition = { freq: value.freq.toLowerCase() };
if (value.byday) {
if (typeof value.byday === "string") {
event.repetition.byday = [value.byday];
case "rrule": {
const ruleValue = value as RepetitionRule;
event.repetition = { freq: ruleValue.freq.toLowerCase() };
if (ruleValue.byday) {
if (typeof ruleValue.byday === "string") {
event.repetition.byday = [ruleValue.byday];
} else {
event.repetition.byday = value.byday;
event.repetition.byday = ruleValue.byday;
}
}
if (value.until) {
event.repetition.endDate = value.until;
if (ruleValue.until) {
event.repetition.endDate = ruleValue.until;
}
if (value.count) {
event.repetition.occurrences = value.count;
if (ruleValue.count) {
event.repetition.occurrences = ruleValue.count;
}
if (value.interval) {
event.repetition.interval = value.interval;
if (ruleValue.interval) {
event.repetition.interval = ruleValue.interval;
}
break;
}
}
}
if (recurrenceId && event.uid) {
@@ -160,10 +172,10 @@ export function parseCalendarEvent(
for (const [key, , , value] of valarm[1]) {
switch (key.toLowerCase()) {
case "action":
event.alarm.action = value;
event.alarm.action = String(value);
break;
case "trigger":
event.alarm.trigger = value;
event.alarm.trigger = String(value);
break;
}
}
@@ -209,10 +221,10 @@ export function parseCalendarEvent(
export function calendarEventToJCal(
event: CalendarEvent,
calOwnerEmail?: string
): any[] {
) {
const tzid = event.timezone; // Fallback to UTC if no timezone provided
const vevent: any[] = makeVevent(event, tzid, calOwnerEmail);
const vevent = makeVevent(event, tzid, calOwnerEmail);
const timezoneData = TIMEZONES.zones[event.timezone];
const vtimezone = makeTimezone(timezoneData, event);
@@ -242,7 +254,7 @@ export function makeVevent(
calOwnerEmail: string | undefined,
isMasterEvent?: boolean
) {
const vevent: any[] = [
const vevent: [string, unknown[]] = [
"vevent",
[
["uid", {}, "text", extractEventBaseUuid(event.uid)],
@@ -279,18 +291,13 @@ export function makeVevent(
];
vevent.push([["valarm", valarm]]);
}
vevent.push([]);
if (event.end) {
const startDate = new Date(event.start);
const endDate = new Date(event.end);
let finalEndDate = endDate;
vevent[1].push([
"dtend",
{ tzid },
event.allday ? "date" : "date-time",
formatDateToICal(finalEndDate, event.allday ?? false, tzid),
formatDateToICal(new Date(event.end), event.allday ?? false, tzid),
]);
}
if (event.organizer) {
@@ -311,21 +318,21 @@ export function makeVevent(
vevent[1].push(["description", {}, "text", event.description]);
}
if (event.repetition?.freq) {
const repetitionRule: Record<string, any> = { freq: event.repetition.freq };
const repetitionRule: RepetitionRule = { freq: event.repetition.freq };
if (event.repetition.interval) {
repetitionRule["interval"] = event.repetition.interval;
repetitionRule.interval = event.repetition.interval;
}
if (event.repetition.occurrences) {
repetitionRule["count"] = event.repetition.occurrences;
repetitionRule.count = event.repetition.occurrences;
}
if (event.repetition.endDate) {
repetitionRule["until"] = event.repetition.endDate;
repetitionRule.until = event.repetition.endDate;
}
if (
event.repetition.byday !== null &&
event.repetition.byday !== undefined
) {
repetitionRule["byday"] = event.repetition.byday;
repetitionRule.byday = event.repetition.byday;
}
vevent[1].push(["rrule", {}, "recur", repetitionRule]);
}
@@ -362,7 +369,7 @@ export function makeVevent(
return vevent;
}
function formatDateToICal(date: Date, allday: Boolean, timezone?: string) {
function formatDateToICal(date: Date, allday: boolean, timezone?: string) {
const pad = (n: number) => n.toString().padStart(2, "0");
if (allday) {
+2 -1
View File
@@ -6,6 +6,7 @@ import { TIMEZONES } from "@/utils/timezone-data";
import { addVideoConferenceToDescription } from "@/utils/videoConferenceUtils";
import { userAttendee } from "../User/models/attendee";
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
import { Calendar } from "../Calendars/CalendarTypes";
export interface TimezoneListResult {
zones: string[];
@@ -58,7 +59,7 @@ export interface PopulateFormFromEventParams {
setHasVideoConference: (value: boolean) => void;
setMeetingLink: (value: string | null) => void;
setCalendarid?: (value: string) => void;
calendarsList?: Record<string, any>;
calendarsList?: Record<string, Calendar>;
calId?: string;
}