Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
+1
-1
@@ -48,7 +48,7 @@ function App() {
|
||||
);
|
||||
const settingsLanguage = useAppSelector((state) => state.settings.language);
|
||||
const savedLang = localStorage.getItem("lang");
|
||||
const defaultLang = (window as any).LANG;
|
||||
const defaultLang = window.LANG;
|
||||
|
||||
const lang =
|
||||
[userLanguage, settingsLanguage, savedLang, defaultLang].find(
|
||||
|
||||
@@ -15,7 +15,7 @@ export default function UserSearch({
|
||||
placeholder,
|
||||
}: {
|
||||
attendees: userAttendee[];
|
||||
setAttendees: Function;
|
||||
setAttendees: (attendees: userAttendee[]) => void;
|
||||
disabled?: boolean;
|
||||
inputSlot?: (
|
||||
params: ExtendedAutocompleteRenderInputParams
|
||||
@@ -47,7 +47,7 @@ export default function UserSearch({
|
||||
disabled={disabled}
|
||||
inputSlot={inputSlot}
|
||||
placeholder={placeholder}
|
||||
onChange={(event: any, value: User[]) => {
|
||||
onChange={(_event, value: User[]) => {
|
||||
setAttendees(
|
||||
value.map((attendee: User) =>
|
||||
createAttendee({
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
useEffect,
|
||||
useState,
|
||||
type ReactNode,
|
||||
type SyntheticEvent,
|
||||
} from "react";
|
||||
import { useI18n } from "twake-i18n";
|
||||
|
||||
@@ -55,7 +56,7 @@ export function PeopleSearch({
|
||||
customSlotProps,
|
||||
}: {
|
||||
selectedUsers: User[];
|
||||
onChange: (event: any, users: User[]) => void;
|
||||
onChange: (event: SyntheticEvent, users: User[]) => void;
|
||||
objectTypes: string[];
|
||||
disabled?: boolean;
|
||||
freeSolo?: boolean;
|
||||
@@ -155,7 +156,7 @@ export function PeopleSearch({
|
||||
),
|
||||
};
|
||||
|
||||
const enhancedParamsWithInputProps = {
|
||||
const enhancedParams = {
|
||||
...params,
|
||||
InputProps: inputProps,
|
||||
inputProps: {
|
||||
@@ -164,8 +165,6 @@ export function PeopleSearch({
|
||||
},
|
||||
};
|
||||
|
||||
const { InputProps, ...enhancedParams } = enhancedParamsWithInputProps;
|
||||
|
||||
const handleEnterKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter" && onToggleEventPreview) {
|
||||
e.preventDefault();
|
||||
@@ -193,7 +192,7 @@ export function PeopleSearch({
|
||||
{t("peopleSearch.label")}
|
||||
</label>
|
||||
{inputSlot({
|
||||
...enhancedParamsWithInputProps,
|
||||
...enhancedParams,
|
||||
error: !!inputError,
|
||||
helperText: inputError,
|
||||
placeholder: searchPlaceholder,
|
||||
@@ -218,6 +217,7 @@ export function PeopleSearch({
|
||||
</>
|
||||
);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[inputError, t, onToggleEventPreview, loading, searchPlaceholder]
|
||||
);
|
||||
|
||||
@@ -296,7 +296,7 @@ export function PeopleSearch({
|
||||
}
|
||||
renderOption={(props, option) => {
|
||||
if (selectedUsers.find((u) => u.email === option.email)) return null;
|
||||
const { key, ...otherProps } = props as any;
|
||||
const { key, ...otherProps } = props;
|
||||
return (
|
||||
<ListItem key={key + option?.email} {...otherProps} disableGutters>
|
||||
<ListItemAvatar>
|
||||
|
||||
@@ -22,7 +22,7 @@ import { SnackbarAlert } from "../Loading/SnackBarAlert";
|
||||
|
||||
export function AccessTab({ calendar }: { calendar: Calendar }) {
|
||||
const { t } = useI18n();
|
||||
const calDAVLink = `${(window as any).CALENDAR_BASE_URL}${calendar.link.replace(".json", "")}`;
|
||||
const calDAVLink = `${window.CALENDAR_BASE_URL}${calendar.link.replace(".json", "")}`;
|
||||
|
||||
const [secretLink, setSecretLink] = useState("");
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
@@ -15,6 +15,7 @@ import { extractEventBaseUuid } from "@/utils/extractEventBaseUuid";
|
||||
import { setSelectedCalendars as setSelectedCalendarsToStorage } from "@/utils/storage/setSelectedCalendars";
|
||||
import { useSelectedCalendars } from "@/utils/storage/useSelectedCalendars";
|
||||
import { browserDefaultTimeZone } from "@/utils/timezone";
|
||||
import type { EventApi, LocaleInput } from "@fullcalendar/core";
|
||||
import { CalendarApi, DateSelectArg } from "@fullcalendar/core";
|
||||
import frLocale from "@fullcalendar/core/locales/fr";
|
||||
import ruLocale from "@fullcalendar/core/locales/ru";
|
||||
@@ -47,7 +48,7 @@ import {
|
||||
updateSlotLabelVisibility,
|
||||
} from "./utils/calendarUtils";
|
||||
|
||||
const localeMap: Record<string, any> = {
|
||||
const localeMap: Record<string, LocaleInput | undefined> = {
|
||||
fr: frLocale,
|
||||
ru: ruLocale,
|
||||
vi: viLocale,
|
||||
@@ -221,7 +222,7 @@ export default function CalendarApp({
|
||||
[rangeStart, rangeEnd]
|
||||
);
|
||||
|
||||
let filteredEvents: CalendarEvent[] = extractEvents(
|
||||
const filteredEvents: CalendarEvent[] = extractEvents(
|
||||
selectedCalendars,
|
||||
calendars || {},
|
||||
userData?.email,
|
||||
@@ -233,7 +234,7 @@ export default function CalendarApp({
|
||||
[tempcalendars]
|
||||
);
|
||||
|
||||
let filteredTempEvents: CalendarEvent[] = extractEvents(
|
||||
const filteredTempEvents: CalendarEvent[] = extractEvents(
|
||||
tempCalendarIds,
|
||||
tempcalendars || {},
|
||||
userData?.email,
|
||||
@@ -303,7 +304,7 @@ export default function CalendarApp({
|
||||
},
|
||||
})
|
||||
).unwrap();
|
||||
} catch (error) {
|
||||
} catch {
|
||||
fetchedRangesRef.current[id] = "";
|
||||
}
|
||||
});
|
||||
@@ -323,6 +324,7 @@ export default function CalendarApp({
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dispatch, rangeKey, sortedSelectedCalendars, rangeStart, rangeEnd]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -352,6 +354,7 @@ export default function CalendarApp({
|
||||
prefetchedCalendarsRef.current[id] = "";
|
||||
});
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
calendarIdsString,
|
||||
selectedCalendars,
|
||||
@@ -458,7 +461,7 @@ export default function CalendarApp({
|
||||
calType: "temp",
|
||||
})
|
||||
).unwrap();
|
||||
} catch (error) {
|
||||
} catch {
|
||||
tempFetchedRangesRef.current[id] = "";
|
||||
}
|
||||
});
|
||||
@@ -521,7 +524,7 @@ export default function CalendarApp({
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
} catch {
|
||||
// Ignore sessionStorage errors
|
||||
}
|
||||
}
|
||||
@@ -543,10 +546,10 @@ export default function CalendarApp({
|
||||
null
|
||||
);
|
||||
const [, setTypeOfAction] = useState<"solo" | "all" | undefined>(undefined);
|
||||
const [afterChoiceFunc, setAfterChoiceFunc] = useState<Function>();
|
||||
const [selectedEvent, setSelectedEvent] = useState<CalendarEvent>(
|
||||
{} as CalendarEvent
|
||||
);
|
||||
const [afterChoiceFunc, setAfterChoiceFunc] = useState<
|
||||
((type: "solo" | "all" | undefined) => void) | undefined
|
||||
>();
|
||||
const [, setSelectedEvent] = useState<CalendarEvent>({} as CalendarEvent);
|
||||
const [selectedRange, setSelectedRange] = useState<DateSelectArg | null>(
|
||||
null
|
||||
);
|
||||
@@ -587,7 +590,7 @@ export default function CalendarApp({
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV === "test") {
|
||||
(window as any).__calendarRef = calendarRef;
|
||||
window.__calendarRef = calendarRef;
|
||||
}
|
||||
|
||||
const { t, lang } = useI18n();
|
||||
@@ -635,7 +638,6 @@ export default function CalendarApp({
|
||||
<MiniCalendar
|
||||
calendarRef={calendarRef}
|
||||
selectedDate={selectedMiniDate}
|
||||
setSelectedDate={setSelectedDate}
|
||||
setSelectedMiniDate={setSelectedMiniDate}
|
||||
dottedEvents={dottedEvents}
|
||||
/>
|
||||
@@ -697,7 +699,7 @@ export default function CalendarApp({
|
||||
userData?.email,
|
||||
isPending
|
||||
)}
|
||||
eventOrder={(a: any, b: any) =>
|
||||
eventOrder={(a: EventApi, b: EventApi) =>
|
||||
a.extendedProps.priority - b.extendedProps.priority
|
||||
}
|
||||
weekNumbers={
|
||||
@@ -825,10 +827,11 @@ export default function CalendarApp({
|
||||
<EditModeDialog
|
||||
type={openEditModePopup}
|
||||
setOpen={setOpenEditModePopup}
|
||||
event={selectedEvent}
|
||||
eventAction={(type: "solo" | "all" | undefined) => {
|
||||
setTypeOfAction(type);
|
||||
afterChoiceFunc && afterChoiceFunc(type);
|
||||
if (afterChoiceFunc) {
|
||||
afterChoiceFunc(type);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{openEventDisplay && eventDisplayedId && eventDisplayedCalId && (
|
||||
|
||||
@@ -123,11 +123,11 @@ function ColorPickerBox({
|
||||
selectedColor ?? { light: "#ffffff", dark: "#808080" }
|
||||
);
|
||||
const [color, setColor] = useState(oldColor);
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const theme = useTheme();
|
||||
|
||||
const handleClick = (event: any) => {
|
||||
const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useAppDispatch, useAppSelector } from "@/app/hooks";
|
||||
import SettingsPage from "@/features/Settings/SettingsPage";
|
||||
import { getCalendarRange } from "@/utils/dateUtils";
|
||||
import type { CalendarApi } from "@fullcalendar/core";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { ErrorSnackbar } from "../Error/ErrorSnackbar";
|
||||
import { refreshCalendars } from "../Event/utils/eventUtils";
|
||||
@@ -8,7 +9,7 @@ import { Menubar, MenubarProps } from "../Menubar/Menubar";
|
||||
import CalendarApp from "./Calendar";
|
||||
|
||||
export default function CalendarLayout() {
|
||||
const calendarRef = useRef<any>(null);
|
||||
const calendarRef = useRef<CalendarApi | null>(null);
|
||||
const dispatch = useAppDispatch();
|
||||
const error = useAppSelector((state) => state.calendars.error);
|
||||
const selectedCalendars = useAppSelector((state) => state.calendars.list);
|
||||
|
||||
@@ -138,28 +138,30 @@ function CalendarPopover({
|
||||
newCalColor,
|
||||
newCalVisibility
|
||||
);
|
||||
importedContent &&
|
||||
if (importedContent) {
|
||||
dispatch(
|
||||
importEventFromFileAsync({
|
||||
calLink: `/calendar/${userData.openpaasId}/${calId}.json`,
|
||||
file: importedContent,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
importedContent &&
|
||||
if (importedContent) {
|
||||
dispatch(
|
||||
importEventFromFileAsync({
|
||||
calLink: calendars[importTarget].link,
|
||||
file: importedContent,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
handleClose({}, "backdropClick");
|
||||
};
|
||||
|
||||
const handleClose = (
|
||||
e: {},
|
||||
e: object | null,
|
||||
reason: "backdropClick" | "escapeKeyDown"
|
||||
): void => {
|
||||
onClose(e, reason);
|
||||
@@ -182,7 +184,7 @@ function CalendarPopover({
|
||||
open={open}
|
||||
onClose={() => handleClose({}, "backdropClick")}
|
||||
title={
|
||||
<Tabs value={tab} onChange={(e, v) => setTab(v)}>
|
||||
<Tabs value={tab} onChange={(_e, v) => setTab(v)}>
|
||||
<Tab
|
||||
value="settings"
|
||||
label={
|
||||
@@ -203,7 +205,7 @@ function CalendarPopover({
|
||||
<>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={(e) => handleClose({}, "backdropClick")}
|
||||
onClick={() => handleClose({}, "backdropClick")}
|
||||
>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
|
||||
@@ -21,7 +21,7 @@ import { ColorPicker } from "./CalendarColorPicker";
|
||||
import { defaultColors, getAccessiblePair } from "./utils/calendarColorsUtils";
|
||||
|
||||
interface CalendarWithOwner {
|
||||
cal: Record<string, any>;
|
||||
cal: Record<string, unknown>;
|
||||
owner: User;
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ function SelectedCalendarsList({
|
||||
>
|
||||
>((acc, cal) => {
|
||||
const exists = Object.values(calendars).some(
|
||||
(existing: any) =>
|
||||
(existing: Calendar) =>
|
||||
existing.id ===
|
||||
cal.cal?._links?.self?.href
|
||||
.replace("/calendars/", "")
|
||||
@@ -190,13 +190,14 @@ function SelectedCalendarsList({
|
||||
}
|
||||
|
||||
export default function CalendarSearch({
|
||||
anchorEl,
|
||||
open,
|
||||
onClose,
|
||||
}: {
|
||||
anchorEl: HTMLElement | null;
|
||||
open: boolean;
|
||||
onClose: Function;
|
||||
onClose: (
|
||||
result?: string[] | Record<string, never>,
|
||||
reason?: "backdropClick" | "escapeKeyDown"
|
||||
) => void;
|
||||
}) {
|
||||
const dispatch = useAppDispatch();
|
||||
const theme = useTheme();
|
||||
@@ -214,7 +215,7 @@ export default function CalendarSearch({
|
||||
selectedCal.map(async (cal) => {
|
||||
const calId = crypto.randomUUID();
|
||||
const exists = Object.values(calendars).some(
|
||||
(existing: any) =>
|
||||
(existing: Calendar) =>
|
||||
existing.id ===
|
||||
cal.cal?._links?.self?.href
|
||||
.replace("/calendars/", "")
|
||||
@@ -279,7 +280,7 @@ export default function CalendarSearch({
|
||||
objectTypes={["user"]}
|
||||
selectedUsers={selectedUsers}
|
||||
inputSlot={(params) => <TextField {...params} size="small" />}
|
||||
onChange={async (event: any, value: User[]) => {
|
||||
onChange={async (_event: React.SyntheticEvent, value: User[]) => {
|
||||
setSelectedUsers(value);
|
||||
|
||||
const cals = await Promise.all(
|
||||
@@ -288,10 +289,10 @@ export default function CalendarSearch({
|
||||
const cals = (await getCalendars(
|
||||
user.openpaasId,
|
||||
"sharedPublic=true&"
|
||||
)) as Record<string, any>;
|
||||
)) as Record<string, unknown>;
|
||||
return cals._embedded?.["dav:calendar"]
|
||||
? cals._embedded["dav:calendar"].map(
|
||||
(cal: Record<string, any>) => ({ cal, owner: user })
|
||||
(cal: Record<string, unknown>) => ({ cal, owner: user })
|
||||
)
|
||||
: { cal: undefined, owner: user };
|
||||
}
|
||||
|
||||
@@ -13,12 +13,11 @@ import {
|
||||
ListItem,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Typography,
|
||||
} from "@linagora/twake-mui";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import MoreHorizIcon from "@mui/icons-material/MoreHoriz";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { SetStateAction, useEffect, useMemo, useState } from "react";
|
||||
import { useI18n } from "twake-i18n";
|
||||
import CalendarPopover from "./CalendarModal";
|
||||
import CalendarSearch from "./CalendarSearch";
|
||||
@@ -39,9 +38,9 @@ function CalendarAccordion({
|
||||
selectedCalendars: string[];
|
||||
handleToggle: (id: string) => void;
|
||||
showAddButton?: boolean;
|
||||
onAddClick?: Function;
|
||||
onAddClick?: () => void;
|
||||
defaultExpanded?: boolean;
|
||||
setOpen: Function;
|
||||
setOpen: (id: string) => void;
|
||||
}) {
|
||||
const allCalendars = useAppSelector((state) => state.calendars.list);
|
||||
const { t } = useI18n();
|
||||
@@ -86,8 +85,12 @@ function CalendarAccordion({
|
||||
<IconButton
|
||||
component="span"
|
||||
onClick={(e) => {
|
||||
expended && e.stopPropagation();
|
||||
onAddClick && onAddClick();
|
||||
if (expended) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
if (onAddClick) {
|
||||
onAddClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AddIcon />
|
||||
@@ -116,7 +119,7 @@ export default function CalendarSelection({
|
||||
setSelectedCalendars,
|
||||
}: {
|
||||
selectedCalendars: string[];
|
||||
setSelectedCalendars: Function;
|
||||
setSelectedCalendars: (value: SetStateAction<string[]>) => void;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const userId =
|
||||
@@ -202,7 +205,6 @@ export default function CalendarSelection({
|
||||
}}
|
||||
/>
|
||||
<CalendarSearch
|
||||
anchorEl={anchorElCalOthers}
|
||||
open={Boolean(anchorElCalOthers)}
|
||||
onClose={(newCalIds?: string[]) => {
|
||||
setAnchorElCalOthers(null);
|
||||
@@ -228,7 +230,7 @@ function CalendarSelector({
|
||||
isPersonal: boolean;
|
||||
selectedCalendars: string[];
|
||||
handleCalendarToggle: (name: string) => void;
|
||||
setOpen: Function;
|
||||
setOpen: () => void;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@@ -24,21 +24,21 @@ export function ImportTab({
|
||||
}: {
|
||||
userId: string;
|
||||
importTarget: string;
|
||||
setImportTarget: Function;
|
||||
setImportedContent: Function;
|
||||
setImportTarget: (target: string) => void;
|
||||
setImportedContent: (content: File | null) => void;
|
||||
newCalParams: {
|
||||
name: string;
|
||||
setName: Function;
|
||||
setName: (name: string) => void;
|
||||
description: string;
|
||||
setDescription: (d: string) => void;
|
||||
color: Record<string, string>;
|
||||
setColor: Function;
|
||||
setColor: (color: Record<string, string>) => void;
|
||||
visibility: "public" | "private";
|
||||
setVisibility: Function;
|
||||
setVisibility: (visibility: "public" | "private") => void;
|
||||
};
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const [importMode, setImportMode] = useState<"file" | "url">("file");
|
||||
const [importMode] = useState<"file" | "url">("file");
|
||||
const [importFile, setImportFile] = useState<File | null>(null);
|
||||
const [importUrl, setImportUrl] = useState("");
|
||||
const calendars = useAppSelector((state) => state.calendars.list);
|
||||
@@ -48,7 +48,7 @@ export function ImportTab({
|
||||
|
||||
useEffect(() => {
|
||||
setImportedContent(importMode === "file" ? importFile : null);
|
||||
}, [importFile, importUrl, importMode]);
|
||||
}, [importFile, importUrl, importMode, setImportedContent]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -13,19 +13,20 @@ import moment from "moment";
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useI18n } from "twake-i18n";
|
||||
import { CalendarEvent } from "@/features/Events/EventsTypes";
|
||||
import type { CalendarApi } from "@fullcalendar/core";
|
||||
|
||||
export function MiniCalendar({
|
||||
calendarRef,
|
||||
selectedDate,
|
||||
setSelectedDate,
|
||||
|
||||
setSelectedMiniDate,
|
||||
dottedEvents,
|
||||
}: {
|
||||
calendarRef: any;
|
||||
calendarRef: React.MutableRefObject<CalendarApi | null>;
|
||||
selectedDate: Date;
|
||||
setSelectedDate: (d: Date) => void;
|
||||
setSelectedMiniDate: (d: Date) => void;
|
||||
dottedEvents: any[];
|
||||
dottedEvents: CalendarEvent[];
|
||||
}) {
|
||||
const dispatch = useAppDispatch();
|
||||
const calendars = useAppSelector((state) => state.calendars);
|
||||
|
||||
@@ -27,13 +27,13 @@ export function SettingsTab({
|
||||
calendar,
|
||||
}: {
|
||||
name: string;
|
||||
setName: Function;
|
||||
setName: (name: string) => void;
|
||||
description: string;
|
||||
setDescription: (d: string) => void;
|
||||
color: Record<string, string>;
|
||||
setColor: Function;
|
||||
setColor: (color: Record<string, string>) => void;
|
||||
visibility: "public" | "private";
|
||||
setVisibility: Function;
|
||||
setVisibility: (visibility: "public" | "private") => void;
|
||||
calendar?: Calendar;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -22,7 +22,7 @@ export function TempCalendarsInput({
|
||||
setTempUsers: (users: User[]) => void;
|
||||
handleToggleEventPreview: () => void;
|
||||
selectedCalendars: string[];
|
||||
setSelectedCalendars: Function;
|
||||
setSelectedCalendars: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
}) {
|
||||
const dispatch = useAppDispatch();
|
||||
const tempcalendars =
|
||||
@@ -34,7 +34,7 @@ export function TempCalendarsInput({
|
||||
const prevUsersRef = useRef<User[]>([]);
|
||||
const userColorsRef = useRef(new Map<string, string>());
|
||||
|
||||
const handleUserChange = async (_: any, users: User[]) => {
|
||||
const handleUserChange = async (_: React.SyntheticEvent, users: User[]) => {
|
||||
setTempUsers(users);
|
||||
|
||||
const prevUsers = prevUsersRef.current;
|
||||
|
||||
@@ -13,7 +13,12 @@ import { CalendarEvent } from "@/features/Events/EventsTypes";
|
||||
import { userAttendee } from "@/features/User/models/attendee";
|
||||
import { createAttendee } from "@/features/User/models/attendee.mapper";
|
||||
import { getDeltaInMilliseconds } from "@/utils/dateUtils";
|
||||
import { CalendarApi, DateSelectArg, EventDropArg } from "@fullcalendar/core";
|
||||
import {
|
||||
CalendarApi,
|
||||
DateSelectArg,
|
||||
EventClickArg,
|
||||
EventDropArg,
|
||||
} from "@fullcalendar/core";
|
||||
import { EventResizeDoneArg } from "@fullcalendar/interaction";
|
||||
|
||||
export interface EventHandlersProps {
|
||||
@@ -27,7 +32,9 @@ export interface EventHandlersProps {
|
||||
setEventDisplayedTemp: (temp: boolean) => void;
|
||||
calendars: Record<string, Calendar>;
|
||||
setSelectedEvent: (event: CalendarEvent) => void;
|
||||
setAfterChoiceFunc: (func: Function) => void;
|
||||
setAfterChoiceFunc: (
|
||||
func: ((type: "solo" | "all" | undefined) => void) | undefined
|
||||
) => void;
|
||||
setOpenEditModePopup: (open: string) => void;
|
||||
tempUsers: User[];
|
||||
setTempEvent: (event: CalendarEvent) => void;
|
||||
@@ -87,7 +94,7 @@ export const createEventHandlers = (props: EventHandlersProps) => {
|
||||
setOpenEventDisplay(false);
|
||||
};
|
||||
|
||||
const handleEventClick = (info: any) => {
|
||||
const handleEventClick = (info: EventClickArg) => {
|
||||
info.jsEvent.preventDefault();
|
||||
|
||||
if (info.event.url) {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
interface CalendarHTMLElement extends HTMLElement {
|
||||
__calendarMouseMoveHandler?: (e: MouseEvent) => void;
|
||||
__calendarMouseLeaveHandler?: () => void;
|
||||
}
|
||||
export interface MouseHandlersProps {
|
||||
calendarEl: HTMLElement;
|
||||
calendarEl: CalendarHTMLElement;
|
||||
}
|
||||
|
||||
export const createMouseHandlers = (props: MouseHandlersProps) => {
|
||||
@@ -75,24 +79,24 @@ export const createMouseHandlers = (props: MouseHandlersProps) => {
|
||||
calendarEl.addEventListener("mousemove", handleMouseMove);
|
||||
calendarEl.addEventListener("mouseleave", handleMouseLeave);
|
||||
|
||||
(calendarEl as any).__calendarMouseMoveHandler = handleMouseMove;
|
||||
(calendarEl as any).__calendarMouseLeaveHandler = handleMouseLeave;
|
||||
calendarEl.__calendarMouseMoveHandler = handleMouseMove;
|
||||
calendarEl.__calendarMouseLeaveHandler = handleMouseLeave;
|
||||
};
|
||||
|
||||
const removeMouseEventListeners = () => {
|
||||
if ((calendarEl as any).__calendarMouseMoveHandler) {
|
||||
if (calendarEl.__calendarMouseMoveHandler) {
|
||||
calendarEl.removeEventListener(
|
||||
"mousemove",
|
||||
(calendarEl as any).__calendarMouseMoveHandler
|
||||
calendarEl.__calendarMouseMoveHandler
|
||||
);
|
||||
delete (calendarEl as any).__calendarMouseMoveHandler;
|
||||
delete calendarEl.__calendarMouseMoveHandler;
|
||||
}
|
||||
if ((calendarEl as any).__calendarMouseLeaveHandler) {
|
||||
if (calendarEl.__calendarMouseLeaveHandler) {
|
||||
calendarEl.removeEventListener(
|
||||
"mouseleave",
|
||||
(calendarEl as any).__calendarMouseLeaveHandler
|
||||
calendarEl.__calendarMouseLeaveHandler
|
||||
);
|
||||
delete (calendarEl as any).__calendarMouseLeaveHandler;
|
||||
delete calendarEl.__calendarMouseLeaveHandler;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,14 @@ import { EventErrorHandler } from "@/components/Error/EventErrorHandler";
|
||||
import { EventChip } from "@/components/Event/EventChip/EventChip";
|
||||
import { Calendar } from "@/features/Calendars/CalendarTypes";
|
||||
import { userAttendee } from "@/features/User/models/attendee";
|
||||
import { CalendarApi, NowIndicatorContentArg } from "@fullcalendar/core";
|
||||
import {
|
||||
CalendarApi,
|
||||
DayHeaderMountArg,
|
||||
EventContentArg,
|
||||
EventMountArg,
|
||||
NowIndicatorContentArg,
|
||||
ViewMountArg,
|
||||
} from "@fullcalendar/core";
|
||||
import React from "react";
|
||||
import { createMouseHandlers } from "./mouseHandlers";
|
||||
|
||||
@@ -46,7 +53,7 @@ export const createViewHandlers = (props: ViewHandlersProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDayHeaderDidMount = (arg: any) => {
|
||||
const handleDayHeaderDidMount = (arg: DayHeaderMountArg) => {
|
||||
if (arg.view.type === "timeGridWeek") {
|
||||
const headerEl = arg.el;
|
||||
|
||||
@@ -61,22 +68,19 @@ export const createViewHandlers = (props: ViewHandlersProps) => {
|
||||
};
|
||||
|
||||
headerEl.addEventListener("click", handleDayHeaderClick);
|
||||
(headerEl as any).__dayHeaderClickHandler = handleDayHeaderClick;
|
||||
headerEl.__dayHeaderClickHandler = handleDayHeaderClick;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDayHeaderWillUnmount = (arg: any) => {
|
||||
const handleDayHeaderWillUnmount = (arg: DayHeaderMountArg) => {
|
||||
const headerEl = arg.el;
|
||||
if ((headerEl as any).__dayHeaderClickHandler) {
|
||||
headerEl.removeEventListener(
|
||||
"click",
|
||||
(headerEl as any).__dayHeaderClickHandler
|
||||
);
|
||||
delete (headerEl as any).__dayHeaderClickHandler;
|
||||
if (headerEl.__dayHeaderClickHandler) {
|
||||
headerEl.removeEventListener("click", headerEl.__dayHeaderClickHandler);
|
||||
delete headerEl.__dayHeaderClickHandler;
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewDidMount = (arg: any) => {
|
||||
const handleViewDidMount = (arg: ViewMountArg) => {
|
||||
if (arg.view.type === "timeGridWeek" || arg.view.type === "timeGridDay") {
|
||||
const calendarEl = document.querySelector(".fc") as HTMLElement;
|
||||
if (calendarEl) {
|
||||
@@ -86,15 +90,15 @@ export const createViewHandlers = (props: ViewHandlersProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewWillUnmount = (arg: any) => {
|
||||
if ((arg.el as any).__timeInterval) {
|
||||
clearInterval((arg.el as any).__timeInterval);
|
||||
delete (arg.el as any).__timeInterval;
|
||||
const handleViewWillUnmount = (arg: ViewMountArg) => {
|
||||
if (arg.el.__timeInterval) {
|
||||
clearInterval(arg.el.__timeInterval);
|
||||
delete arg.el.__timeInterval;
|
||||
}
|
||||
|
||||
if ((arg.el as any).__timeObserver) {
|
||||
(arg.el as any).__timeObserver.disconnect();
|
||||
delete (arg.el as any).__timeObserver;
|
||||
if (arg.el.__timeObserver) {
|
||||
arg.el.__timeObserver.disconnect();
|
||||
delete arg.el.__timeObserver;
|
||||
}
|
||||
|
||||
const calendarEl = document.querySelector(".fc") as HTMLElement;
|
||||
@@ -104,7 +108,7 @@ export const createViewHandlers = (props: ViewHandlersProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleEventContent = (arg: any) => {
|
||||
const handleEventContent = (arg: EventContentArg) => {
|
||||
return React.createElement(EventChip, {
|
||||
arg,
|
||||
calendars,
|
||||
@@ -113,7 +117,7 @@ export const createViewHandlers = (props: ViewHandlersProps) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleEventDidMount = (arg: any) => {
|
||||
const handleEventDidMount = (arg: EventMountArg) => {
|
||||
const attendees = arg.event._def.extendedProps.attendee || [];
|
||||
if (!calendars[arg.event._def.extendedProps.calId]) return;
|
||||
const ownerEmails = new Set(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { useCallback } from "react";
|
||||
import {
|
||||
createEventHandlers,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { useCallback } from "react";
|
||||
import {
|
||||
createViewHandlers,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { refreshSingularCalendar } from "@/components/Event/utils/eventUtils";
|
||||
import { AppDispatch } from "@/app/store";
|
||||
import { Calendar } from "@/features/Calendars/CalendarTypes";
|
||||
import { getCalendarDetailAsync } from "@/features/Calendars/services";
|
||||
import { AclEntry } from "@/features/Calendars/types/CalendarData";
|
||||
import { CalendarEvent } from "@/features/Events/EventsTypes";
|
||||
import { formatDateToYYYYMMDDTHHMMSS } from "@/utils/dateUtils";
|
||||
import { extractEventBaseUuid } from "@/utils/extractEventBaseUuid";
|
||||
import { convertEventDateTimeToISO } from "@/utils/timezone";
|
||||
import { SlotLabelContentArg } from "@fullcalendar/core";
|
||||
import { ThunkDispatch } from "@reduxjs/toolkit";
|
||||
import moment from "moment-timezone";
|
||||
import { useI18n } from "twake-i18n";
|
||||
|
||||
@@ -56,7 +56,10 @@ export const checkIfCurrentWeekOrDay = (): boolean => {
|
||||
return !!nowIndicator;
|
||||
};
|
||||
|
||||
export function formatEventChipTitle(e: CalendarEvent, t: Function) {
|
||||
export function formatEventChipTitle(
|
||||
e: CalendarEvent,
|
||||
t: (key: string) => string
|
||||
) {
|
||||
if (!e.title) {
|
||||
return t("event.untitled");
|
||||
}
|
||||
@@ -72,6 +75,7 @@ export const eventToFullCalendarFormat = (
|
||||
userAddress: string | undefined,
|
||||
pending: boolean
|
||||
) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { t } = useI18n();
|
||||
return filteredEvents
|
||||
.concat(filteredTempEvents.map((e) => ({ ...e, temp: true })))
|
||||
@@ -83,7 +87,11 @@ export const eventToFullCalendarFormat = (
|
||||
: true; // if there are no organizer in the event we assume it was organized by the owner
|
||||
const isPersonnalEvent =
|
||||
extractEventBaseUuid(e.calId) === userId && isOrganiser;
|
||||
const convertedEvent: any = {
|
||||
const convertedEvent: CalendarEvent & {
|
||||
colors: Record<string, string> | undefined;
|
||||
editable: boolean;
|
||||
priority: number;
|
||||
} = {
|
||||
...e,
|
||||
title: formatEventChipTitle(e, t),
|
||||
colors: e.color,
|
||||
@@ -147,7 +155,7 @@ export const updateCalsDetails = (
|
||||
pending: boolean,
|
||||
rangeKey: string,
|
||||
previousRangeKey: string,
|
||||
dispatch: Function,
|
||||
dispatch: AppDispatch,
|
||||
calendarRange: { start: Date; end: Date },
|
||||
calType?: "temp",
|
||||
controllers?: Map<string, AbortController>
|
||||
@@ -223,15 +231,9 @@ export const updateCalsDetails = (
|
||||
}
|
||||
};
|
||||
|
||||
interface AclEntry {
|
||||
privilege: string;
|
||||
principal: string;
|
||||
protected: boolean;
|
||||
}
|
||||
|
||||
export function getCalendarVisibility(acl: AclEntry[]): "private" | "public" {
|
||||
let hasRead = false;
|
||||
let hasFreeBusy = false;
|
||||
// const hasFreeBusy = false;
|
||||
if (acl) {
|
||||
for (const entry of acl) {
|
||||
if (entry.principal !== "{DAV:}authenticated") continue;
|
||||
|
||||
@@ -158,7 +158,7 @@ function ResponsiveDialog({
|
||||
}, [isExpanded]);
|
||||
|
||||
const handleClose = (
|
||||
event: {},
|
||||
event: unknown,
|
||||
reason: "backdropClick" | "escapeKeyDown"
|
||||
) => {
|
||||
if (isExpanded && reason === "backdropClick") {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { CalendarEvent } from "@/features/Events/EventsTypes";
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
@@ -16,12 +15,10 @@ import { useI18n } from "twake-i18n";
|
||||
export function EditModeDialog({
|
||||
type,
|
||||
setOpen,
|
||||
event,
|
||||
eventAction,
|
||||
}: {
|
||||
type: string | null;
|
||||
setOpen: (e: string | null) => void;
|
||||
event: CalendarEvent;
|
||||
eventAction: (type: "solo" | "all" | undefined) => void;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { EventErrorHandler } from "@/components/Error/EventErrorHandler";
|
||||
import { EventContentArg } from "@fullcalendar/core";
|
||||
import { Card, Typography } from "@linagora/twake-mui";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
@@ -7,18 +8,30 @@ export function ErrorEventChip({
|
||||
errorHandler,
|
||||
error,
|
||||
}: {
|
||||
event: any;
|
||||
event: EventContentArg["event"];
|
||||
errorHandler: EventErrorHandler;
|
||||
error: any;
|
||||
error: unknown;
|
||||
}) {
|
||||
const hasReported = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasReported.current) {
|
||||
const message =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: `${error.class} error during rendering ${event._def.extendedProps.uid || event.id}`;
|
||||
let message: string;
|
||||
|
||||
if (error instanceof Error) {
|
||||
message = error.message;
|
||||
} else if (
|
||||
typeof error === "object" &&
|
||||
error !== null &&
|
||||
"class" in error
|
||||
) {
|
||||
message = `${String(
|
||||
(error as { class: string }).class
|
||||
)} error during rendering ${event._def.extendedProps.uid || event.id}`;
|
||||
} else {
|
||||
message = `Unknown error during rendering ${event._def.extendedProps.uid || event.id}`;
|
||||
}
|
||||
|
||||
errorHandler.reportError(
|
||||
event._def.extendedProps.uid || event.id,
|
||||
message
|
||||
|
||||
@@ -59,7 +59,7 @@ export function EventChip({
|
||||
const ownerEmails = new Set(
|
||||
calendar.ownerEmails?.map((e) => e.toLowerCase())
|
||||
);
|
||||
const delegated = calendar.delegated;
|
||||
// const delegated = calendar.delegated;
|
||||
|
||||
// Determine owner attendee
|
||||
const ownerAttendee = getOwnerAttendee(attendees, ownerEmails);
|
||||
@@ -258,7 +258,7 @@ export function EventChip({
|
||||
!event._def.extendedProps.allday &&
|
||||
event._def.extendedProps.organizer &&
|
||||
!showCompact &&
|
||||
(window as any).displayOrgAvatar && (
|
||||
window.displayOrgAvatar && (
|
||||
<Avatar
|
||||
children={OrganizerAvatar.children}
|
||||
color={OrganizerAvatar.color}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { EventErrorHandler } from "@/components/Error/EventErrorHandler";
|
||||
import { Calendar } from "@/features/Calendars/CalendarTypes";
|
||||
import { userAttendee } from "@/features/User/models/attendee";
|
||||
import { EventContentArg } from "@fullcalendar/core";
|
||||
import { getContrastRatio } from "@linagora/twake-mui";
|
||||
import CancelIcon from "@mui/icons-material/Cancel";
|
||||
import HelpOutlineIcon from "@mui/icons-material/HelpOutline";
|
||||
@@ -12,7 +13,7 @@ import { EVENT_DURATION } from "./EventChip";
|
||||
const COMPACT_WIDTH_THRESHOLD = 100;
|
||||
|
||||
export interface EventChipProps {
|
||||
arg: any;
|
||||
arg: EventContentArg["event"];
|
||||
calendars: Record<string, Calendar>;
|
||||
tempcalendars: Record<string, Calendar>;
|
||||
errorHandler: EventErrorHandler;
|
||||
@@ -23,9 +24,9 @@ export interface IconDisplayConfig {
|
||||
needAction: boolean;
|
||||
private: boolean;
|
||||
}
|
||||
export function getEventDuration(event: any): number {
|
||||
return moment(event._instance.range.end).diff(
|
||||
moment(event._instance.range.start),
|
||||
export function getEventDuration(event: EventContentArg["event"]): number {
|
||||
return moment(event._instance?.range.end).diff(
|
||||
moment(event._instance?.range.start),
|
||||
"minutes"
|
||||
);
|
||||
}
|
||||
@@ -34,7 +35,10 @@ export function getBestColor(colors: { light: string; dark: string }): string {
|
||||
const contrastToLight = getContrastRatio(colors?.light, "#fff");
|
||||
return contrastToDark > contrastToLight ? colors?.dark : colors?.light;
|
||||
}
|
||||
export function getEventTimes(event: any, timeZone: string) {
|
||||
export function getEventTimes(
|
||||
event: EventContentArg["event"],
|
||||
timeZone: string
|
||||
) {
|
||||
return {
|
||||
startTime: moment.tz(event.start, timeZone).format("HH:mm"),
|
||||
endTime: moment.tz(event.end, timeZone).format("HH:mm"),
|
||||
@@ -52,7 +56,7 @@ export function getOwnerAttendee(
|
||||
export function getTitleStyle(
|
||||
bestColor: string,
|
||||
partstat?: string,
|
||||
calendar?: any
|
||||
calendar?: Calendar
|
||||
): React.CSSProperties {
|
||||
const baseStyle: React.CSSProperties = {
|
||||
fontFamily: "Roboto",
|
||||
@@ -122,7 +126,7 @@ export function getCardStyle(
|
||||
bestColor: string,
|
||||
eventLength: number,
|
||||
partstat?: string,
|
||||
calendar?: any
|
||||
calendar?: Calendar
|
||||
): React.CSSProperties {
|
||||
const baseStyle: React.CSSProperties = getCardVariantStyle(
|
||||
getEventVariant(eventLength),
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import { CalendarEvent } from "@/features/Events/EventsTypes";
|
||||
import { MenuItem } from "@linagora/twake-mui";
|
||||
import { useI18n } from "twake-i18n";
|
||||
|
||||
export default function EventDuplication({
|
||||
onClose,
|
||||
event,
|
||||
onOpenDuplicate,
|
||||
}: {
|
||||
onClose: Function;
|
||||
event: CalendarEvent;
|
||||
onOpenDuplicate?: () => void;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -21,17 +21,15 @@ import {
|
||||
ToggleButton,
|
||||
ToggleButtonGroup,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@linagora/twake-mui";
|
||||
import { alpha } from "@mui/material/styles";
|
||||
import {
|
||||
Close as DeleteIcon,
|
||||
ContentCopy as CopyIcon,
|
||||
Public as PublicIcon,
|
||||
LocationOn as LocationIcon,
|
||||
Public as PublicIcon,
|
||||
} from "@mui/icons-material";
|
||||
import SquareRoundedIcon from "@mui/icons-material/SquareRounded";
|
||||
import LockOutlineIcon from "@mui/icons-material/LockOutline";
|
||||
import SquareRoundedIcon from "@mui/icons-material/SquareRounded";
|
||||
import React from "react";
|
||||
import { useI18n } from "twake-i18n";
|
||||
import AttendeeSelector from "../Attendees/AttendeeSearch";
|
||||
@@ -163,7 +161,6 @@ export default function EventFormFields({
|
||||
onHasEndDateChangedChange,
|
||||
}: EventFormFieldsProps) {
|
||||
const { t } = useI18n();
|
||||
const theme = useTheme();
|
||||
|
||||
// Internal state for 4 separate fields
|
||||
const [startDate, setStartDate] = React.useState("");
|
||||
@@ -377,7 +374,6 @@ export default function EventFormFields({
|
||||
showMore,
|
||||
});
|
||||
}, [
|
||||
title,
|
||||
startDate,
|
||||
startTime,
|
||||
endDate,
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function RepeatEvent({
|
||||
}: {
|
||||
repetition: RepetitionObject;
|
||||
eventStart: Date;
|
||||
setRepetition: Function;
|
||||
setRepetition: (repetition: RepetitionObject) => void;
|
||||
isOwn?: boolean;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -83,7 +83,6 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
||||
showMore,
|
||||
hasEndDateChanged,
|
||||
showEndDate,
|
||||
onToggleEndDate,
|
||||
validation,
|
||||
onStartDateChange,
|
||||
onStartTimeChange,
|
||||
|
||||
@@ -88,7 +88,7 @@ export const DateTimeSummary: React.FC<DateTimeSummaryProps> = ({
|
||||
const offset = getTimezoneOffset(tz, dateForOffset);
|
||||
const tzName = tz.replace(/_/g, " ");
|
||||
return `(${offset}) ${tzName}`;
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return tz.replace(/_/g, " ");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -19,12 +19,12 @@ import { parseTimeInput } from "../utils/dateTimeHelpers";
|
||||
|
||||
type FieldType = "date" | "time" | "date-time";
|
||||
|
||||
type GenericPickerFieldProps = PickerFieldProps<any, any, any> & {
|
||||
type GenericPickerFieldProps = PickerFieldProps<Dayjs, false, false> & {
|
||||
fieldType: FieldType;
|
||||
validator: (
|
||||
value: any,
|
||||
value: Dayjs | null,
|
||||
context: PickerValidationScope,
|
||||
adapter: PickerFieldAdapter<any>
|
||||
adapter: PickerFieldAdapter<Dayjs>
|
||||
) => string | null;
|
||||
};
|
||||
|
||||
@@ -223,7 +223,7 @@ function EditableTimePickerField(props: GenericPickerFieldProps) {
|
||||
dispatchCloseOtherPickers();
|
||||
};
|
||||
|
||||
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||
const handleBlur = (_e: React.FocusEvent<HTMLInputElement>) => {
|
||||
// If dropdown is open, don't parse input
|
||||
// MUI will handle selection and sync value via useEffect
|
||||
if (pickerContext.open) {
|
||||
|
||||
@@ -12,15 +12,16 @@ import {
|
||||
useValidation,
|
||||
validateDate,
|
||||
} from "@mui/x-date-pickers/validation";
|
||||
import { Dayjs } from "dayjs";
|
||||
|
||||
type FieldType = "date" | "time" | "date-time";
|
||||
|
||||
type GenericPickerFieldProps = PickerFieldProps<any, any, any> & {
|
||||
type GenericPickerFieldProps = PickerFieldProps<Dayjs, false, false> & {
|
||||
fieldType: FieldType;
|
||||
validator: (
|
||||
value: any,
|
||||
value: Dayjs | null,
|
||||
context: PickerValidationScope,
|
||||
adapter: PickerFieldAdapter<any>
|
||||
adapter: PickerFieldAdapter<Dayjs>
|
||||
) => string | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -119,28 +119,18 @@ export async function handleRSVP(
|
||||
export function handleDelete(
|
||||
isRecurring: boolean,
|
||||
typeOfAction: "solo" | "all" | undefined,
|
||||
onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void,
|
||||
dispatch: Function,
|
||||
onClose: (event: unknown, reason: "backdropClick" | "escapeKeyDown") => void,
|
||||
dispatch: AppDispatch,
|
||||
calendar: Calendar,
|
||||
event: CalendarEvent,
|
||||
calId: string,
|
||||
eventId: string
|
||||
) {
|
||||
if (isRecurring) {
|
||||
onClose({}, "backdropClick");
|
||||
if (typeOfAction === "solo") {
|
||||
dispatch(deleteEventInstanceAsync({ cal: calendar, event }));
|
||||
} else {
|
||||
dispatch(
|
||||
deleteEventAsync({
|
||||
calId,
|
||||
eventId,
|
||||
eventURL: event.URL,
|
||||
})
|
||||
);
|
||||
}
|
||||
onClose({}, "backdropClick");
|
||||
|
||||
if (isRecurring && typeOfAction === "solo") {
|
||||
dispatch(deleteEventInstanceAsync({ cal: calendar, event }));
|
||||
} else {
|
||||
onClose({}, "backdropClick");
|
||||
dispatch(
|
||||
deleteEventAsync({
|
||||
calId,
|
||||
|
||||
@@ -16,7 +16,7 @@ import CheckCircleIcon from "@mui/icons-material/CheckCircle";
|
||||
export function renderAttendeeBadge(
|
||||
a: userAttendee,
|
||||
key: string,
|
||||
t: Function,
|
||||
t: (key: string) => string,
|
||||
isFull?: boolean,
|
||||
isOrganizer?: boolean
|
||||
) {
|
||||
@@ -110,7 +110,9 @@ export async function refreshCalendars(
|
||||
) {
|
||||
if (process.env.NODE_ENV === "test") return;
|
||||
|
||||
!calType && (await dispatch(getCalendarsListAsync()));
|
||||
if (!calType) {
|
||||
await dispatch(getCalendarsListAsync());
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
calendars.map((calendar) =>
|
||||
|
||||
@@ -8,7 +8,7 @@ export function SnackbarAlert({
|
||||
severity = "success",
|
||||
}: {
|
||||
open: boolean;
|
||||
setOpen: Function;
|
||||
setOpen: (o: boolean) => void;
|
||||
message: string;
|
||||
severity?: AlertColor;
|
||||
}) {
|
||||
|
||||
@@ -142,7 +142,7 @@ export default function SearchBar() {
|
||||
setFilterError(false);
|
||||
};
|
||||
|
||||
const handleContactSelect = (_event: any, contacts: User[]) => {
|
||||
const handleContactSelect = (contacts: User[]) => {
|
||||
setSelectedContacts(contacts);
|
||||
setSearch("");
|
||||
if (contacts.length > 0) {
|
||||
@@ -173,7 +173,9 @@ export default function SearchBar() {
|
||||
dispatch(setView("search"));
|
||||
setAnchorEl(null);
|
||||
} else {
|
||||
filterOpen && setFilterError(true);
|
||||
if (filterOpen) {
|
||||
setFilterError(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -222,8 +224,8 @@ export default function SearchBar() {
|
||||
{extended && (
|
||||
<PeopleSearch
|
||||
selectedUsers={selectedContacts}
|
||||
onChange={(event, users) => {
|
||||
handleContactSelect(event, users);
|
||||
onChange={(_event, users) => {
|
||||
handleContactSelect(users);
|
||||
}}
|
||||
objectTypes={["user", "contact"]}
|
||||
onToggleEventPreview={() => {}}
|
||||
|
||||
@@ -56,9 +56,9 @@ export function Menubar({
|
||||
currentView,
|
||||
onViewChange,
|
||||
}: MenubarProps) {
|
||||
const { t, lang } = useI18n(); // deliberately NOT using f()
|
||||
const { t } = useI18n(); // deliberately NOT using f()
|
||||
const user = useAppSelector((state) => state.user.userData);
|
||||
const applist: AppIconProps[] = (window as any).appList ?? [];
|
||||
const applist: AppIconProps[] = window.appList ?? [];
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [userMenuAnchorEl, setUserMenuAnchorEl] = useState<null | HTMLElement>(
|
||||
null
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { api } from "@/utils/apiUtils";
|
||||
import { CalendarInput, CalendarList } from "./types/CalendarData";
|
||||
|
||||
export async function getCalendars(
|
||||
userId: string,
|
||||
scope: string = "personal=true&sharedDelegationStatus=accepted&sharedPublicSubscription=true&",
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
): Promise<CalendarList> {
|
||||
const calendars = await api
|
||||
.get(`dav/calendars/${userId}.json?${scope}`, {
|
||||
headers: {
|
||||
@@ -13,7 +14,7 @@ export async function getCalendars(
|
||||
signal,
|
||||
})
|
||||
.json();
|
||||
return calendars;
|
||||
return calendars as CalendarList;
|
||||
}
|
||||
|
||||
export async function getCalendar(
|
||||
@@ -59,7 +60,7 @@ export async function postCalendar(
|
||||
export async function addSharedCalendar(
|
||||
userId: string,
|
||||
calId: string,
|
||||
cal: Record<string, any>
|
||||
cal: CalendarInput
|
||||
) {
|
||||
const response = await api.post(`dav/calendars/${userId}.json`, {
|
||||
headers: {
|
||||
@@ -70,7 +71,7 @@ export async function addSharedCalendar(
|
||||
...cal.cal,
|
||||
"dav:name":
|
||||
cal.cal["dav:name"] === "#default"
|
||||
? cal.owner.displayName + "'s calendar"
|
||||
? (cal.owner?.displayName ?? "Unknown") + "'s calendar"
|
||||
: cal.cal["dav:name"],
|
||||
"calendarserver:source": {
|
||||
acl: cal.cal.acl,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { formatReduxError } from "@/utils/errorUtils";
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import { getUserDetails } from "@/features/User/userAPI";
|
||||
import { toRejectedError } from "@/utils/errorUtils";
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import { addSharedCalendar } from "../CalendarApi";
|
||||
import { CalendarInput } from "../types/CalendarData";
|
||||
import { RejectedError } from "../types/RejectedError";
|
||||
|
||||
export const addSharedCalendarAsync = createAsyncThunk<
|
||||
@@ -14,14 +15,14 @@ export const addSharedCalendarAsync = createAsyncThunk<
|
||||
owner: string;
|
||||
ownerEmails: string[];
|
||||
},
|
||||
{ userId: string; calId: string; cal: Record<string, any> },
|
||||
{ userId: string; calId: string; cal: CalendarInput },
|
||||
{ rejectValue: RejectedError }
|
||||
>(
|
||||
"calendars/addSharedCalendar",
|
||||
async ({ userId, calId, cal }, { rejectWithValue }) => {
|
||||
try {
|
||||
await addSharedCalendar(userId, calId, cal);
|
||||
const ownerData: any = await getUserDetails(
|
||||
const ownerData = await getUserDetails(
|
||||
cal.cal._links.self.href
|
||||
.replace("/calendars/", "")
|
||||
.replace(".json", "")
|
||||
@@ -29,28 +30,25 @@ export const addSharedCalendarAsync = createAsyncThunk<
|
||||
);
|
||||
|
||||
return {
|
||||
calId: cal.cal._links.self.href
|
||||
.replace("/calendars/", "")
|
||||
calId: cal.cal._links.self?.href
|
||||
?.replace("/calendars/", "")
|
||||
.replace(".json", ""),
|
||||
color: cal.color,
|
||||
link: `/calendars/${userId}/${calId}.json`,
|
||||
desc: cal.cal["caldav:description"],
|
||||
desc: cal.cal["caldav:description"] ?? "",
|
||||
name:
|
||||
ownerData.id !== userId && cal.cal["dav:name"] === "#default"
|
||||
? `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${
|
||||
ownerData.lastname
|
||||
ownerData.lastname ?? ""
|
||||
}` + "'s calendar"
|
||||
: cal.cal["dav:name"],
|
||||
: (cal.cal["dav:name"] ?? ""),
|
||||
owner: `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${
|
||||
ownerData.lastname
|
||||
ownerData.lastname ?? ""
|
||||
}`,
|
||||
ownerEmails: ownerData.emails,
|
||||
};
|
||||
} catch (err: any) {
|
||||
return rejectWithValue({
|
||||
message: formatReduxError(err),
|
||||
status: err.response?.status,
|
||||
});
|
||||
} catch (err) {
|
||||
return rejectWithValue(toRejectedError(err));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { getUserDetails } from "@/features/User/userAPI";
|
||||
import { userData } from "@/features/User/userDataTypes";
|
||||
import { formatReduxError } from "@/utils/errorUtils";
|
||||
import { toRejectedError } from "@/utils/errorUtils";
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import { postCalendar } from "../CalendarApi";
|
||||
import { RejectedError } from "../types/RejectedError";
|
||||
@@ -45,11 +44,8 @@ export const createCalendarAsync = createAsyncThunk<
|
||||
owner,
|
||||
ownerEmails: userData.email ? [userData.email] : [],
|
||||
};
|
||||
} catch (err: any) {
|
||||
return rejectWithValue({
|
||||
message: formatReduxError(err),
|
||||
status: err.response?.status,
|
||||
});
|
||||
} catch (err) {
|
||||
return rejectWithValue(toRejectedError(err));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { deleteEvent } from "@/features/Events/EventApi";
|
||||
import { formatReduxError } from "@/utils/errorUtils";
|
||||
import { toRejectedError } from "@/utils/errorUtils";
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import { RejectedError } from "../types/RejectedError";
|
||||
|
||||
@@ -13,11 +13,8 @@ export const deleteEventAsync = createAsyncThunk<
|
||||
try {
|
||||
await deleteEvent(eventURL);
|
||||
return { calId, eventId };
|
||||
} catch (err: any) {
|
||||
return rejectWithValue({
|
||||
message: formatReduxError(err),
|
||||
status: err.response?.status,
|
||||
});
|
||||
} catch (err) {
|
||||
return rejectWithValue(toRejectedError(err));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -11,12 +11,13 @@ export const deleteEventInstanceAsync = createAsyncThunk<
|
||||
{ rejectValue: RejectedError }
|
||||
>("calendars/delEventInstance", async ({ cal, event }, { rejectWithValue }) => {
|
||||
try {
|
||||
await deleteEventInstance(event, cal.ownerEmails?.[0]);
|
||||
await deleteEventInstance(event);
|
||||
return { calId: cal.id, eventId: event.uid };
|
||||
} catch (err: any) {
|
||||
} catch (err) {
|
||||
const error = err as { response?: { status?: number } };
|
||||
return rejectWithValue({
|
||||
message: formatReduxError(err),
|
||||
status: err.response?.status,
|
||||
status: error.response?.status,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { CalendarEvent } from "@/features/Events/EventsTypes";
|
||||
import { formatReduxError } from "@/utils/errorUtils";
|
||||
import {
|
||||
CalendarData,
|
||||
CalendarItem,
|
||||
} from "@/features/Calendars/types/CalendarData";
|
||||
import { toRejectedError } from "@/utils/errorUtils";
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import { getCalendar } from "../CalendarApi";
|
||||
import { RejectedError } from "../types/RejectedError";
|
||||
import { extractCalendarEvents } from "../utils/extractCalendarEvents";
|
||||
import { defaultColors } from "@/components/Calendar/utils/calendarColorsUtils";
|
||||
|
||||
export const getCalendarDetailAsync = createAsyncThunk<
|
||||
{
|
||||
@@ -23,24 +28,27 @@ export const getCalendarDetailAsync = createAsyncThunk<
|
||||
"calendars/getCalendarDetails",
|
||||
async ({ calId, match, calType, signal }, { rejectWithValue }) => {
|
||||
try {
|
||||
const calendar = (await getCalendar(calId, match, signal)) as any;
|
||||
const calendar = (await getCalendar(
|
||||
calId,
|
||||
match,
|
||||
signal
|
||||
)) as CalendarData;
|
||||
|
||||
const color = calendar["apple:color"];
|
||||
const color = calendar["apple:color"]
|
||||
? { light: calendar["apple:color"], dark: calendar["apple:color"] }
|
||||
: defaultColors[0];
|
||||
const syncToken = calendar._embedded?.["sync-token"];
|
||||
|
||||
const items = calendar._embedded?.["dav:item"];
|
||||
const events: CalendarEvent[] = Array.isArray(items)
|
||||
? items.flatMap((item: any) =>
|
||||
? items.flatMap((item: CalendarItem) =>
|
||||
extractCalendarEvents(item, { calId, color })
|
||||
)
|
||||
: [];
|
||||
|
||||
return { calId, events, calType, syncToken };
|
||||
} catch (err: any) {
|
||||
return rejectWithValue({
|
||||
message: formatReduxError(err),
|
||||
status: err.response?.status,
|
||||
});
|
||||
} catch (err) {
|
||||
return rejectWithValue(toRejectedError(err));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,32 +1,33 @@
|
||||
import { RootState } from "@/app/store";
|
||||
import { OpenPaasUserData } from "@/features/User/type/OpenPaasUserData";
|
||||
import { getOpenPaasUser, getUserDetails } from "@/features/User/userAPI";
|
||||
import { formatReduxError } from "@/utils/errorUtils";
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import { getCalendars } from "../CalendarApi";
|
||||
import { RejectedError } from "../types/RejectedError";
|
||||
import { Calendar } from "../CalendarTypes";
|
||||
import { CalendarData } from "../types/CalendarData";
|
||||
import { RejectedError } from "../types/RejectedError";
|
||||
import { normalizeCalendar } from "../utils/normalizeCalendar";
|
||||
|
||||
export const getCalendarsListAsync = createAsyncThunk<
|
||||
{ importedCalendars: Record<string, Calendar>; errors: string },
|
||||
void,
|
||||
{ rejectValue: RejectedError; state: any }
|
||||
{ rejectValue: RejectedError; state: RootState }
|
||||
>("calendars/getCalendars", async (_, { rejectWithValue, getState }) => {
|
||||
const state = getState() as any;
|
||||
const state = getState();
|
||||
const existingCalendars = state.calendars.list || {};
|
||||
const existingUser = { id: state.user?.userData?.openpaasId || undefined };
|
||||
try {
|
||||
const fetchedCalendars: Record<string, Calendar> = {};
|
||||
const user = existingUser.id
|
||||
? existingUser
|
||||
: ((await getOpenPaasUser()) as Record<string, string>);
|
||||
const calendars = (await getCalendars(user.id)) as Record<string, any>;
|
||||
const rawCalendars = calendars._embedded["dav:calendar"] as Record<
|
||||
string,
|
||||
any
|
||||
>[];
|
||||
: ((await getOpenPaasUser()) as OpenPaasUserData);
|
||||
const calendars = await getCalendars(user.id);
|
||||
const rawCalendars = calendars._embedded["dav:calendar"];
|
||||
|
||||
const errors: string[] = [];
|
||||
|
||||
const normalizedCalendars = rawCalendars.map((cal) =>
|
||||
const normalizedCalendars = rawCalendars.map((cal: CalendarData) =>
|
||||
normalizeCalendar(cal)
|
||||
);
|
||||
|
||||
@@ -34,14 +35,14 @@ export const getCalendarsListAsync = createAsyncThunk<
|
||||
new Set(normalizedCalendars.map(({ ownerId }) => ownerId).filter(Boolean))
|
||||
);
|
||||
|
||||
const ownerDataMap = new Map<string, any>();
|
||||
const ownerDataMap = new Map<string, OpenPaasUserData>();
|
||||
const OWNER_BATCH_SIZE = 20;
|
||||
|
||||
const fetchOwnerData = async (ownerId: string) => {
|
||||
try {
|
||||
const data = await getUserDetails(ownerId);
|
||||
ownerDataMap.set(ownerId, data);
|
||||
} catch (error: any) {
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch user details for ${ownerId}:`, error);
|
||||
ownerDataMap.set(ownerId, {
|
||||
firstname: "",
|
||||
@@ -72,7 +73,7 @@ export const getCalendarsListAsync = createAsyncThunk<
|
||||
|
||||
const color = {
|
||||
light: cal["apple:color"] ?? "#006BD8",
|
||||
dark: cal["X-TWAKE-Dark-theme-color"] ?? "#FFF",
|
||||
dark: "#FFF",
|
||||
};
|
||||
fetchedCalendars[id] = {
|
||||
id,
|
||||
@@ -120,10 +121,11 @@ export const getCalendarsListAsync = createAsyncThunk<
|
||||
importedCalendars,
|
||||
errors: errors.join("\n"),
|
||||
};
|
||||
} catch (err: any) {
|
||||
} catch (err) {
|
||||
const error = err as { response?: { status?: number } };
|
||||
return rejectWithValue({
|
||||
message: formatReduxError(err),
|
||||
status: err.response?.status,
|
||||
status: error.response?.status,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -15,10 +15,11 @@ export const getEventAsync = createAsyncThunk<
|
||||
calId: event.calId,
|
||||
event: response,
|
||||
};
|
||||
} catch (err: any) {
|
||||
} catch (err) {
|
||||
const error = err as { response?: { status?: number } };
|
||||
return rejectWithValue({
|
||||
message: formatReduxError(err),
|
||||
status: err.response?.status,
|
||||
status: error.response?.status,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4,8 +4,8 @@ import { getUserDetails } from "@/features/User/userAPI";
|
||||
import { formatReduxError } from "@/utils/errorUtils";
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import { getCalendars } from "../CalendarApi";
|
||||
import { RejectedError } from "../types/RejectedError";
|
||||
import { Calendar } from "../CalendarTypes";
|
||||
import { RejectedError } from "../types/RejectedError";
|
||||
|
||||
export const getTempCalendarsListAsync = createAsyncThunk<
|
||||
Record<string, Calendar>,
|
||||
@@ -20,10 +20,10 @@ export const getTempCalendarsListAsync = createAsyncThunk<
|
||||
`TRANSLATION:calendar.userDoesNotHaveValidId|name=${encodeURIComponent(username)}`
|
||||
);
|
||||
}
|
||||
const calendars = (await getCalendars(
|
||||
const calendars = await getCalendars(
|
||||
tempUser.openpaasId,
|
||||
"sharedPublic=true&"
|
||||
)) as Record<string, any>;
|
||||
);
|
||||
|
||||
const rawCalendars = calendars._embedded?.["dav:calendar"];
|
||||
if (!rawCalendars || rawCalendars.length === 0) {
|
||||
@@ -36,17 +36,20 @@ export const getTempCalendarsListAsync = createAsyncThunk<
|
||||
}
|
||||
|
||||
for (const cal of rawCalendars) {
|
||||
const name = cal["dav:name"];
|
||||
const description = cal["caldav:description"];
|
||||
const name = cal["dav:name"] ?? "";
|
||||
const description = cal["caldav:description"] ?? "";
|
||||
const delegated = cal["calendarserver:delegatedsource"] ? true : false;
|
||||
const source = cal["calendarserver:source"]
|
||||
? cal["calendarserver:source"]._links.self.href
|
||||
: cal._links.self.href;
|
||||
const link = cal._links.self.href;
|
||||
? cal["calendarserver:source"]._links.self?.href
|
||||
: cal._links.self?.href;
|
||||
if (!source) {
|
||||
throw new Error("No source for calendar");
|
||||
}
|
||||
const link = cal._links.self?.href ?? "";
|
||||
|
||||
const id = source.replace("/calendars/", "").replace(".json", "");
|
||||
const visibility = getCalendarVisibility(cal["acl"]);
|
||||
const ownerData: any = await getUserDetails(id.split("/")[0]);
|
||||
const visibility = getCalendarVisibility(cal["acl"] ?? []);
|
||||
const ownerData = await getUserDetails(id.split("/")[0]);
|
||||
|
||||
importedCalendars[id] = {
|
||||
id,
|
||||
@@ -66,10 +69,11 @@ export const getTempCalendarsListAsync = createAsyncThunk<
|
||||
}
|
||||
|
||||
return importedCalendars;
|
||||
} catch (err: any) {
|
||||
} catch (err) {
|
||||
const error = err as { response?: { status?: number } };
|
||||
return rejectWithValue({
|
||||
message: formatReduxError(err),
|
||||
status: err.response?.status,
|
||||
status: error.response?.status,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -22,10 +22,11 @@ export const importEventFromFileAsync = createAsyncThunk<
|
||||
});
|
||||
}
|
||||
await importEventFromFile(id, calLink);
|
||||
} catch (err: any) {
|
||||
} catch (err) {
|
||||
const error = err as { response?: { status?: number } };
|
||||
return rejectWithValue({
|
||||
message: formatReduxError(err),
|
||||
status: err.response?.status,
|
||||
status: error.response?.status,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
import { moveEvent } 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 { toRejectedError } from "@/utils/errorUtils";
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import { getCalendar } from "../CalendarApi";
|
||||
import { RejectedError } from "../types/RejectedError";
|
||||
import { Calendar } from "../CalendarTypes";
|
||||
import { RejectedError } from "../types/RejectedError";
|
||||
|
||||
export const moveEventAsync = createAsyncThunk<
|
||||
{ calId: string },
|
||||
@@ -24,11 +18,8 @@ export const moveEventAsync = createAsyncThunk<
|
||||
return {
|
||||
calId: cal.id,
|
||||
};
|
||||
} catch (err: any) {
|
||||
return rejectWithValue({
|
||||
message: formatReduxError(err),
|
||||
status: err.response?.status,
|
||||
});
|
||||
} catch (err) {
|
||||
return rejectWithValue(toRejectedError(err));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { formatReduxError } from "@/utils/errorUtils";
|
||||
import { toRejectedError } from "@/utils/errorUtils";
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import { updateAclCalendar } from "../CalendarApi";
|
||||
import { RejectedError } from "../types/RejectedError";
|
||||
@@ -25,11 +25,8 @@ export const patchACLCalendarAsync = createAsyncThunk<
|
||||
calLink,
|
||||
request,
|
||||
};
|
||||
} catch (err: any) {
|
||||
return rejectWithValue({
|
||||
message: formatReduxError(err),
|
||||
status: err.response?.status,
|
||||
});
|
||||
} catch (err) {
|
||||
return rejectWithValue(toRejectedError(err));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { formatReduxError } from "@/utils/errorUtils";
|
||||
import { toRejectedError } from "@/utils/errorUtils";
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import { proppatchCalendar } from "../CalendarApi";
|
||||
import { RejectedError } from "../types/RejectedError";
|
||||
@@ -25,11 +25,8 @@ export const patchCalendarAsync = createAsyncThunk<
|
||||
calLink,
|
||||
patch,
|
||||
};
|
||||
} catch (err: any) {
|
||||
return rejectWithValue({
|
||||
message: formatReduxError(err),
|
||||
status: err.response?.status,
|
||||
});
|
||||
} catch (err) {
|
||||
return rejectWithValue(toRejectedError(err));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { putEvent } from "@/features/Events/EventApi";
|
||||
import { CalendarEvent } from "@/features/Events/EventsTypes";
|
||||
import { formatReduxError } from "@/utils/errorUtils";
|
||||
import { toRejectedError } from "@/utils/errorUtils";
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import { RejectedError } from "../types/RejectedError";
|
||||
import { Calendar } from "../CalendarTypes";
|
||||
import { RejectedError } from "../types/RejectedError";
|
||||
|
||||
export const putEventAsync = createAsyncThunk<
|
||||
{ calId: string; calType?: "temp" },
|
||||
@@ -22,11 +22,8 @@ export const putEventAsync = createAsyncThunk<
|
||||
calId: cal.id,
|
||||
calType,
|
||||
};
|
||||
} catch (err: any) {
|
||||
return rejectWithValue({
|
||||
message: formatReduxError(err),
|
||||
status: err.response?.status,
|
||||
});
|
||||
} catch (err) {
|
||||
return rejectWithValue(toRejectedError(err));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { CalendarEvent } from "@/features/Events/EventsTypes";
|
||||
import { formatReduxError } from "@/utils/errorUtils";
|
||||
import { toRejectedError } from "@/utils/errorUtils";
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import pMap from "p-map";
|
||||
import { fetchSyncTokenChanges } from "../api/fetchSyncTokenChanges";
|
||||
import { RejectedError } from "../types/RejectedError";
|
||||
import { Calendar } from "../CalendarTypes";
|
||||
import { RejectedError } from "../types/RejectedError";
|
||||
import { expandEventFunction } from "../utils/expandEventFunction";
|
||||
import { processSyncUpdates } from "../utils/processSyncTokenUpdates";
|
||||
|
||||
@@ -70,11 +70,8 @@ export const refreshCalendarWithSyncToken = createAsyncThunk<
|
||||
syncToken: newSyncToken,
|
||||
syncStatus: newSyncToken ? "SUCCESS" : "NO_NEW_SYNC_TOKEN",
|
||||
};
|
||||
} catch (err: any) {
|
||||
return rejectWithValue({
|
||||
message: formatReduxError(err),
|
||||
status: err.response?.status,
|
||||
});
|
||||
} catch (err) {
|
||||
return rejectWithValue(toRejectedError(err));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { formatReduxError } from "@/utils/errorUtils";
|
||||
import { toRejectedError } from "@/utils/errorUtils";
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import { removeCalendar } from "../CalendarApi";
|
||||
import { RejectedError } from "../types/RejectedError";
|
||||
@@ -20,11 +20,8 @@ export const removeCalendarAsync = createAsyncThunk<
|
||||
return {
|
||||
calId,
|
||||
};
|
||||
} catch (err: any) {
|
||||
return rejectWithValue({
|
||||
message: formatReduxError(err),
|
||||
status: err.response?.status,
|
||||
});
|
||||
} catch (err) {
|
||||
return rejectWithValue(toRejectedError(err));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { putEventWithOverrides } from "@/features/Events/EventApi";
|
||||
import { CalendarEvent } from "@/features/Events/EventsTypes";
|
||||
import { formatReduxError } from "@/utils/errorUtils";
|
||||
import { toRejectedError } from "@/utils/errorUtils";
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import { RejectedError } from "../types/RejectedError";
|
||||
import { Calendar } from "../CalendarTypes";
|
||||
@@ -15,11 +15,8 @@ export const updateEventInstanceAsync = createAsyncThunk<
|
||||
try {
|
||||
await putEventWithOverrides(event, cal.ownerEmails?.[0]);
|
||||
return { calId: cal.id, event };
|
||||
} catch (err: any) {
|
||||
return rejectWithValue({
|
||||
message: formatReduxError(err),
|
||||
status: err.response?.status,
|
||||
});
|
||||
} catch (err) {
|
||||
return rejectWithValue(toRejectedError(err));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { updateSeries } from "@/features/Events/EventApi";
|
||||
import { CalendarEvent } from "@/features/Events/EventsTypes";
|
||||
import { formatReduxError } from "@/utils/errorUtils";
|
||||
import { toRejectedError } from "@/utils/errorUtils";
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import { RejectedError } from "../types/RejectedError";
|
||||
import { Calendar } from "../CalendarTypes";
|
||||
import { RejectedError } from "../types/RejectedError";
|
||||
|
||||
export const updateSeriesAsync = createAsyncThunk<
|
||||
void,
|
||||
@@ -14,11 +14,8 @@ export const updateSeriesAsync = createAsyncThunk<
|
||||
async ({ cal, event, removeOverrides = true }, { rejectWithValue }) => {
|
||||
try {
|
||||
await updateSeries(event, cal.ownerEmails?.[0] ?? "", removeOverrides);
|
||||
} catch (err: any) {
|
||||
return rejectWithValue({
|
||||
message: formatReduxError(err),
|
||||
status: err.response?.status,
|
||||
});
|
||||
} catch (err) {
|
||||
return rejectWithValue(toRejectedError(err));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// Type for async thunk results
|
||||
export interface AsyncThunkResult {
|
||||
type: string;
|
||||
error?: { message?: string };
|
||||
payload?: { message?: string };
|
||||
unwrap?: () => Promise<unknown>;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { User } from "@/components/Attendees/PeopleSearch";
|
||||
import { CalDavLink } from "../api/types";
|
||||
|
||||
// Access control entry
|
||||
export interface AclEntry {
|
||||
privilege: string;
|
||||
principal: string;
|
||||
protected: boolean;
|
||||
}
|
||||
|
||||
// VObject property value can be various types
|
||||
export type VObjectValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Date
|
||||
| Record<string, unknown>
|
||||
| null
|
||||
| RepetitionRule
|
||||
| undefined;
|
||||
|
||||
// VObject property tuple
|
||||
export type VObjectProperty = [
|
||||
string,
|
||||
Record<string, unknown>,
|
||||
string | Array<unknown>,
|
||||
VObjectValue,
|
||||
];
|
||||
|
||||
export type VCalComponent = [
|
||||
string,
|
||||
VObjectProperty[],
|
||||
VCalComponent[],
|
||||
...unknown[],
|
||||
];
|
||||
|
||||
export interface Organizer {
|
||||
cn?: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
// The `dav:item` object in _embedded
|
||||
export interface CalendarItem {
|
||||
_links: CalDavLink;
|
||||
etag: string;
|
||||
status: number;
|
||||
data: [
|
||||
"vcalendar",
|
||||
Array<
|
||||
VObjectProperty | [string, VObjectProperty[], unknown[]] // vevent array
|
||||
>,
|
||||
];
|
||||
}
|
||||
|
||||
// Main calendar data
|
||||
export interface CalendarData {
|
||||
_links: CalDavLink;
|
||||
"caldav:description"?: string;
|
||||
"dav:name"?: string;
|
||||
"apple:color"?: string;
|
||||
id?: string;
|
||||
acl?: AclEntry[];
|
||||
invite?: unknown;
|
||||
_embedded: {
|
||||
"sync-token": string;
|
||||
"dav:item": CalendarItem[];
|
||||
};
|
||||
"calendarserver:source"?: { _links: CalDavLink };
|
||||
"calendarserver:delegatedsource"?: string;
|
||||
}
|
||||
|
||||
export interface CalendarList {
|
||||
_embedded: { "dav:calendar": CalendarData[] };
|
||||
}
|
||||
|
||||
// Calendar input for forms or UI
|
||||
export interface CalendarInput {
|
||||
cal: CalendarData;
|
||||
color: Record<string, string>;
|
||||
owner?: User;
|
||||
}
|
||||
|
||||
// Vevent repetition rule
|
||||
export interface RepetitionRule {
|
||||
freq: string;
|
||||
interval?: number;
|
||||
count?: number;
|
||||
until?: string;
|
||||
byday?: string | string[];
|
||||
}
|
||||
@@ -20,7 +20,7 @@ export function expandEventFunction(
|
||||
});
|
||||
return events;
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch event", eventUrl);
|
||||
console.error("Failed to fetch event", eventUrl, err);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { defaultColors } from "@/components/Calendar/utils/calendarColorsUtils";
|
||||
import { CalendarEvent } from "@/features/Events/EventsTypes";
|
||||
import { parseCalendarEvent } from "@/features/Events/eventUtils";
|
||||
import { CalDavItem } from "../api/types";
|
||||
import { VCalComponent } from "../types/CalendarData";
|
||||
|
||||
export function extractCalendarEvents(
|
||||
item: CalDavItem,
|
||||
@@ -50,7 +51,7 @@ export function extractCalendarEvents(
|
||||
.filter(Boolean) as CalendarEvent[];
|
||||
}
|
||||
|
||||
function extractValarm(vevent: any[]) {
|
||||
function extractValarm(vevent: VCalComponent[]) {
|
||||
const subComponents = vevent[2];
|
||||
if (!Array.isArray(subComponents)) {
|
||||
return undefined;
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import { getCalendarVisibility } from "@/components/Calendar/utils/calendarUtils";
|
||||
import { CalendarData } from "../types/CalendarData";
|
||||
|
||||
export function normalizeCalendar(rawCalendar: Record<string, any>) {
|
||||
export function normalizeCalendar(rawCalendar: CalendarData) {
|
||||
const description = rawCalendar["caldav:description"];
|
||||
let delegated = false;
|
||||
let source = rawCalendar["calendarserver:source"]
|
||||
? rawCalendar["calendarserver:source"]._links.self.href
|
||||
: rawCalendar._links.self.href;
|
||||
const link = rawCalendar._links.self.href;
|
||||
? rawCalendar["calendarserver:source"]._links.self?.href
|
||||
: rawCalendar._links.self?.href;
|
||||
const link = rawCalendar._links.self?.href;
|
||||
if (rawCalendar["calendarserver:delegatedsource"]) {
|
||||
source = rawCalendar["calendarserver:delegatedsource"];
|
||||
delegated = true;
|
||||
}
|
||||
if (!source) {
|
||||
throw new Error("No source for calendar");
|
||||
}
|
||||
const id = source.replace("/calendars/", "").replace(".json", "");
|
||||
const ownerId = id.split("/")[0];
|
||||
const visibility = getCalendarVisibility(rawCalendar["acl"]);
|
||||
const visibility = getCalendarVisibility(rawCalendar["acl"] ?? []);
|
||||
return {
|
||||
cal: rawCalendar,
|
||||
description,
|
||||
|
||||
@@ -30,6 +30,6 @@ export function processSyncUpdates(
|
||||
}
|
||||
|
||||
function extractFileNameFromHref(href: string): string {
|
||||
const fileNameMatch = href.match(/\/([^\/]+)\.ics$/); // CalDAV href are like /calendars/userID/CalendarID/EventId.ics
|
||||
const fileNameMatch = href.match(/\/([^/]+)\.ics$/); // CalDAV href are like /calendars/userID/CalendarID/EventId.ics
|
||||
return fileNameMatch ? fileNameMatch[1] : href;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import EventPreviewModal from "../Events/EventDisplayPreview";
|
||||
import { CalendarEvent } from "../Events/EventsTypes";
|
||||
import { setView } from "../Settings/SettingsSlice";
|
||||
import "./searchResult.styl";
|
||||
import { SearchEventResult } from "./types/SearchEventResult";
|
||||
|
||||
const styles = {
|
||||
M3BodyLarge: {
|
||||
@@ -110,7 +111,7 @@ export default function SearchResultsPage() {
|
||||
layout = (
|
||||
<Box className="search-result-content-body">
|
||||
<Stack sx={{ mt: 2 }}>
|
||||
{results?.map((r: any, idx: number) => (
|
||||
{results.map((r: SearchEventResult, idx: number) => (
|
||||
<ResultItem
|
||||
key={`row-${idx}-event-${r.data.uid}`}
|
||||
eventData={r}
|
||||
@@ -144,7 +145,7 @@ function ResultItem({
|
||||
eventData,
|
||||
dispatch,
|
||||
}: {
|
||||
eventData: Record<string, any>;
|
||||
eventData: SearchEventResult;
|
||||
dispatch: AppDispatch;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
@@ -163,7 +164,7 @@ function ResultItem({
|
||||
|
||||
const [openPreview, setOpenPreview] = useState(false);
|
||||
|
||||
const handleOpenResult = async (eventData: Record<string, any>) => {
|
||||
const handleOpenResult = async (eventData: SearchEventResult) => {
|
||||
if (calendar) {
|
||||
const event = {
|
||||
URL: eventData._links.self.href,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { searchEvent } from "../Events/EventApi";
|
||||
|
||||
export interface SearchResultsState {
|
||||
hits: number;
|
||||
results: Record<string, any>[];
|
||||
results: Record<string, unknown>[];
|
||||
error: string | null;
|
||||
loading: boolean;
|
||||
}
|
||||
@@ -17,24 +17,30 @@ const initialState: SearchResultsState = {
|
||||
};
|
||||
|
||||
export const searchEventsAsync = createAsyncThunk<
|
||||
{ hits: number; events: Record<string, any>[] },
|
||||
{ search: string; filters: any },
|
||||
{ hits: number; events: Record<string, unknown>[] },
|
||||
{
|
||||
search: string;
|
||||
filters: {
|
||||
searchIn: string[];
|
||||
keywords: string;
|
||||
organizers: string[];
|
||||
attendees: string[];
|
||||
};
|
||||
},
|
||||
{ rejectValue: { message: string; status?: number } }
|
||||
>("events/searchEvents", async ({ search, filters }, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = (await searchEvent(search, filters)) as Record<
|
||||
string,
|
||||
any
|
||||
>;
|
||||
const response = await searchEvent(search, filters);
|
||||
|
||||
return {
|
||||
hits: Number(response._total_hits),
|
||||
events: response._embedded?.events ?? [],
|
||||
};
|
||||
} catch (err: any) {
|
||||
} catch (err) {
|
||||
const error = err as { response?: { status?: number } };
|
||||
return rejectWithValue({
|
||||
message: formatReduxError(err),
|
||||
status: err.response?.status,
|
||||
status: error.response?.status,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
export type SearchEventData = {
|
||||
uid: string;
|
||||
userId: string;
|
||||
calendarId: string;
|
||||
start: string;
|
||||
end?: string;
|
||||
allDay?: boolean;
|
||||
summary?: string;
|
||||
description?: string;
|
||||
location?: string;
|
||||
class?: string;
|
||||
dtstamp?: string;
|
||||
isRecurrentMaster?: boolean;
|
||||
attendees?: unknown[];
|
||||
organizer?: {
|
||||
cn?: string;
|
||||
email?: string;
|
||||
};
|
||||
["x-openpaas-videoconference"]?: string;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { SearchEventData } from "./SearchEventData";
|
||||
|
||||
export type SearchEventResult = {
|
||||
data: SearchEventData;
|
||||
_links: {
|
||||
self: {
|
||||
href: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
export type SearchEventsResponse = {
|
||||
_total_hits?: number | string;
|
||||
_embedded?: {
|
||||
events?: Record<string, unknown>[];
|
||||
};
|
||||
};
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useAppDispatch, useAppSelector } from "@/app/hooks";
|
||||
import { useTimeZoneList } from "@/components/Calendar/TimezoneSelector";
|
||||
import { getTimezoneOffset } from "@/utils/timezone";
|
||||
import { TimezoneAutocomplete } from "@/components/Timezone/TimezoneAutocomplete";
|
||||
import { browserDefaultTimeZone } from "@/utils/timezone";
|
||||
import { browserDefaultTimeZone, getTimezoneOffset } from "@/utils/timezone";
|
||||
import {
|
||||
Box,
|
||||
FormControl,
|
||||
@@ -14,6 +13,7 @@ import {
|
||||
ListItemText,
|
||||
MenuItem,
|
||||
Select,
|
||||
SelectChangeEvent,
|
||||
Snackbar,
|
||||
Switch,
|
||||
Tab,
|
||||
@@ -105,7 +105,7 @@ export default function SettingsPage() {
|
||||
setActiveSettingsSubTab(newValue);
|
||||
};
|
||||
|
||||
const handleLanguageChange = (event: any) => {
|
||||
const handleLanguageChange = (event: SelectChangeEvent<string>) => {
|
||||
const newLanguage = event.target.value;
|
||||
const previousLanguage = currentLanguage;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { browserDefaultTimeZone } from "@/utils/timezone";
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { ConfigurationItem, ModuleConfiguration } from "../User/userDataTypes";
|
||||
import { getOpenPaasUserDataAsync } from "../User/userSlice";
|
||||
|
||||
export interface SettingsState {
|
||||
@@ -12,7 +13,7 @@ export interface SettingsState {
|
||||
}
|
||||
|
||||
const savedLang = localStorage.getItem("lang");
|
||||
const defaultLang = savedLang ?? (window as any).LANG ?? "en";
|
||||
const defaultLang = savedLang ?? window.LANG ?? "en";
|
||||
|
||||
const savedTimeZone = localStorage.getItem("timeZone");
|
||||
// If savedTimeZone is the string "null" or doesn't exist, use null
|
||||
@@ -59,12 +60,15 @@ export const settingsSlice = createSlice({
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(getOpenPaasUserDataAsync.fulfilled, (state, action) => {
|
||||
const coreModule = action.payload.configurations?.modules?.find(
|
||||
(module: any) => module.name === "core"
|
||||
(module: ModuleConfiguration) => module.name === "core"
|
||||
);
|
||||
const datetimeConfig = coreModule?.configurations?.find(
|
||||
(config: any) => config.name === "datetime"
|
||||
(config: ConfigurationItem) => config.name === "datetime"
|
||||
);
|
||||
const timeZone = datetimeConfig?.value?.timeZone;
|
||||
const datetimeValue = datetimeConfig?.value as
|
||||
| { timeZone?: string }
|
||||
| undefined;
|
||||
const timeZone = datetimeValue?.timeZone;
|
||||
|
||||
if (timeZone) {
|
||||
state.timeZone = timeZone;
|
||||
@@ -76,25 +80,25 @@ export const settingsSlice = createSlice({
|
||||
localStorage.setItem("timeZone", browserDefaultTimeZone);
|
||||
}
|
||||
const esnCalendarModule = action.payload.configurations?.modules?.find(
|
||||
(module: any) => module.name === "linagora.esn.calendar"
|
||||
(module: ModuleConfiguration) => module.name === "linagora.esn.calendar"
|
||||
);
|
||||
const hideDeclinedEventsConfig = esnCalendarModule?.configurations?.find(
|
||||
(config: any) => config.name === "hideDeclinedEvents"
|
||||
(config: ConfigurationItem) => config.name === "hideDeclinedEvents"
|
||||
);
|
||||
state.hideDeclinedEvents =
|
||||
typeof hideDeclinedEventsConfig?.value === "boolean"
|
||||
? hideDeclinedEventsConfig.value
|
||||
: null;
|
||||
|
||||
const calendarModule = action.payload.configurations.modules.find(
|
||||
(module: any) => module.name === "calendar"
|
||||
const calendarModule = action.payload.configurations?.modules?.find(
|
||||
(module: ModuleConfiguration) => module.name === "calendar"
|
||||
);
|
||||
if (calendarModule?.configurations) {
|
||||
const alarmEmailsConfig = calendarModule.configurations.find(
|
||||
(config: any) => config.name === "displayWeekNumbers"
|
||||
const displayWeekNumbersConfig = calendarModule.configurations.find(
|
||||
(config: ConfigurationItem) => config.name === "displayWeekNumbers"
|
||||
);
|
||||
if (alarmEmailsConfig) {
|
||||
state.displayWeekNumbers = alarmEmailsConfig.value === true;
|
||||
if (displayWeekNumbersConfig) {
|
||||
state.displayWeekNumbers = displayWeekNumbersConfig.value === true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -52,6 +52,7 @@ export function HandleLogin() {
|
||||
};
|
||||
|
||||
initiateLogin();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [userData.userData, calendars.list, dispatch]);
|
||||
|
||||
// Navigate to /calendar only when all data is ready
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import * as client from "openid-client";
|
||||
import { getLocation } from "@/utils/apiUtils";
|
||||
import * as client from "openid-client";
|
||||
|
||||
export const clientConfig = {
|
||||
url: (window as any).SSO_BASE_URL ?? "",
|
||||
client_id: (window as any).SSO_CLIENT_ID ?? "",
|
||||
scope: (window as any).SSO_SCOPE ?? "",
|
||||
redirect_uri: (window as any).SSO_REDIRECT_URI ?? "",
|
||||
response_type: (window as any).SSO_RESPONSE_TYPE ?? "",
|
||||
code_challenge_method: (window as any).SSO_CODE_CHALLENGE_METHOD ?? "",
|
||||
post_logout_redirect_uri: (window as any).SSO_POST_LOGOUT_REDIRECT ?? "",
|
||||
url: window.SSO_BASE_URL ?? "",
|
||||
client_id: window.SSO_CLIENT_ID ?? "",
|
||||
scope: window.SSO_SCOPE ?? "",
|
||||
redirect_uri: window.SSO_REDIRECT_URI ?? "",
|
||||
response_type: window.SSO_RESPONSE_TYPE ?? "",
|
||||
code_challenge_method: window.SSO_CODE_CHALLENGE_METHOD ?? "",
|
||||
post_logout_redirect_uri: window.SSO_POST_LOGOUT_REDIRECT ?? "",
|
||||
};
|
||||
|
||||
export async function getClientConfig() {
|
||||
@@ -19,18 +19,21 @@ export async function getClientConfig() {
|
||||
}
|
||||
|
||||
export async function Auth() {
|
||||
let code_verifier = client.randomPKCECodeVerifier();
|
||||
let code_challenge = await client.calculatePKCECodeChallenge(code_verifier);
|
||||
const code_verifier = client.randomPKCECodeVerifier();
|
||||
const code_challenge = await client.calculatePKCECodeChallenge(code_verifier);
|
||||
const openIdClientConfig = await getClientConfig();
|
||||
let state = client.randomState();
|
||||
let parameters: Record<string, string> = {
|
||||
const state = client.randomState();
|
||||
const parameters: Record<string, string> = {
|
||||
redirect_uri: clientConfig.redirect_uri,
|
||||
scope: clientConfig.scope!,
|
||||
code_challenge,
|
||||
code_challenge_method: clientConfig.code_challenge_method,
|
||||
state,
|
||||
};
|
||||
let redirectTo = client.buildAuthorizationUrl(openIdClientConfig, parameters);
|
||||
const redirectTo = client.buildAuthorizationUrl(
|
||||
openIdClientConfig,
|
||||
parameters
|
||||
);
|
||||
|
||||
return { redirectTo, code_verifier, state };
|
||||
}
|
||||
@@ -44,13 +47,15 @@ export async function Logout() {
|
||||
return endSessionUrl;
|
||||
}
|
||||
|
||||
export async function Callback(code_verifier: string, state: any) {
|
||||
export async function Callback(
|
||||
code_verifier: string,
|
||||
state: string | undefined
|
||||
) {
|
||||
try {
|
||||
const openIdClientConfig = await getClientConfig();
|
||||
const currentLocation = getLocation();
|
||||
|
||||
console.log("Callback URL:", currentLocation);
|
||||
console.log("Code verifier:", code_verifier);
|
||||
console.info("Callback URL:", currentLocation);
|
||||
|
||||
const tokenSet = await client.authorizationCodeGrant(
|
||||
openIdClientConfig,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ModuleConfiguration } from "../userDataTypes";
|
||||
|
||||
export interface OpenPaasUserData {
|
||||
firstname?: string;
|
||||
lastname?: string;
|
||||
id?: string;
|
||||
preferredEmail?: string;
|
||||
configurations?: {
|
||||
modules?: ModuleConfiguration[];
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
import { User } from "@/components/Attendees/PeopleSearch";
|
||||
import { api } from "@/utils/apiUtils";
|
||||
import { OpenPaasUserData } from "./type/OpenPaasUserData";
|
||||
import {
|
||||
ConfigurationItem,
|
||||
ModuleConfiguration,
|
||||
SearchResponseItem,
|
||||
} from "./userDataTypes";
|
||||
|
||||
export async function getOpenPaasUser() {
|
||||
const user = await api.get(`api/user`);
|
||||
@@ -10,7 +16,7 @@ export async function searchUsers(
|
||||
query: string,
|
||||
objectTypes: string[] = ["user", "contact"]
|
||||
): Promise<User[]> {
|
||||
const response: any[] = await api
|
||||
const response: SearchResponseItem[] = await api
|
||||
.post(`api/people/search`, {
|
||||
body: JSON.stringify({
|
||||
limit: 10,
|
||||
@@ -29,9 +35,9 @@ export async function searchUsers(
|
||||
}));
|
||||
}
|
||||
|
||||
export async function getUserDetails(id: string) {
|
||||
export async function getUserDetails(id: string): Promise<OpenPaasUserData> {
|
||||
const user = await api.get(`api/users/${id}`).json();
|
||||
return user;
|
||||
return user as OpenPaasUserData;
|
||||
}
|
||||
|
||||
export interface UserConfigurationUpdates {
|
||||
@@ -39,7 +45,7 @@ export interface UserConfigurationUpdates {
|
||||
notifications?: Record<string, unknown>;
|
||||
timezone?: string | null;
|
||||
displayWeekNumbers?: boolean;
|
||||
previousConfig?: Record<string, any>;
|
||||
previousConfig?: Record<string, unknown>;
|
||||
alarmEmails?: boolean;
|
||||
hideDeclinedEvents?: boolean;
|
||||
}
|
||||
@@ -47,9 +53,9 @@ export interface UserConfigurationUpdates {
|
||||
export async function updateUserConfigurations(
|
||||
updates: UserConfigurationUpdates
|
||||
): Promise<Response | { status: number }> {
|
||||
const coreConfigs: Array<{ name: string; value: any }> = [];
|
||||
const calendarConfigs: Array<{ name: string; value: any }> = [];
|
||||
const esnCalendarConfigs: Array<{ name: string; value: any }> = [];
|
||||
const coreConfigs: ConfigurationItem[] = [];
|
||||
const calendarConfigs: ConfigurationItem[] = [];
|
||||
const esnCalendarConfigs: ConfigurationItem[] = [];
|
||||
|
||||
if (updates.language !== undefined) {
|
||||
coreConfigs.push({ name: "language", value: updates.language });
|
||||
@@ -58,10 +64,13 @@ export async function updateUserConfigurations(
|
||||
coreConfigs.push({ name: "notifications", value: updates.notifications });
|
||||
}
|
||||
if (updates.timezone !== undefined) {
|
||||
const previousDatetime = updates.previousConfig?.datetime as
|
||||
| { timeZone?: string }
|
||||
| undefined;
|
||||
coreConfigs.push({
|
||||
name: "datetime",
|
||||
value: {
|
||||
...updates.previousConfig?.datetime,
|
||||
...previousDatetime,
|
||||
timeZone: updates.timezone,
|
||||
},
|
||||
});
|
||||
@@ -85,10 +94,7 @@ export async function updateUserConfigurations(
|
||||
});
|
||||
}
|
||||
|
||||
const modules: Array<{
|
||||
name: string;
|
||||
configurations: Array<{ name: string; value: any }>;
|
||||
}> = [];
|
||||
const modules: ModuleConfiguration[] = [];
|
||||
|
||||
if (coreConfigs.length > 0) {
|
||||
modules.push({
|
||||
|
||||
@@ -11,13 +11,7 @@ export interface userData {
|
||||
}
|
||||
|
||||
export interface UserConfigurations {
|
||||
modules?: Array<{
|
||||
name: string;
|
||||
configurations?: Array<{
|
||||
name: string;
|
||||
value: any;
|
||||
}>;
|
||||
}>;
|
||||
modules?: ModuleConfiguration[];
|
||||
}
|
||||
|
||||
export interface NotificationSettings {
|
||||
@@ -33,3 +27,22 @@ export interface userOrganiser {
|
||||
cn: string;
|
||||
cal_address: string;
|
||||
}
|
||||
// Type for search response from the API
|
||||
export interface SearchResponseItem {
|
||||
id?: string;
|
||||
emailAddresses?: Array<{ value?: string }>;
|
||||
names?: Array<{ displayName?: string }>;
|
||||
photos?: Array<{ url?: string }>;
|
||||
}
|
||||
|
||||
// Type for configuration item
|
||||
export interface ConfigurationItem {
|
||||
name: string;
|
||||
value: unknown;
|
||||
}
|
||||
|
||||
// Type for module configuration
|
||||
export interface ModuleConfiguration {
|
||||
name: string;
|
||||
configurations: ConfigurationItem[];
|
||||
}
|
||||
|
||||
@@ -1,24 +1,44 @@
|
||||
import { formatReduxError } from "@/utils/errorUtils";
|
||||
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
|
||||
import { OpenPaasUserData } from "./type/OpenPaasUserData";
|
||||
import {
|
||||
getOpenPaasUser,
|
||||
updateUserConfigurations,
|
||||
UserConfigurationUpdates,
|
||||
} from "./userAPI";
|
||||
import { userData, userOrganiser } from "./userDataTypes";
|
||||
import {
|
||||
ConfigurationItem,
|
||||
ModuleConfiguration,
|
||||
userData,
|
||||
userOrganiser,
|
||||
} from "./userDataTypes";
|
||||
|
||||
// Type for core config datetime
|
||||
interface DatetimeConfig {
|
||||
timeZone: string | null;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// Type for core config
|
||||
interface CoreConfig {
|
||||
language: string | null;
|
||||
datetime: DatetimeConfig;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export const getOpenPaasUserDataAsync = createAsyncThunk<
|
||||
Record<string, any>,
|
||||
OpenPaasUserData,
|
||||
void,
|
||||
{ rejectValue: { message: string; status?: number } }
|
||||
>("user/getOpenPaasUserData", async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
const user = (await getOpenPaasUser()) as Record<string, any>;
|
||||
return user;
|
||||
} catch (err: any) {
|
||||
const user = await getOpenPaasUser();
|
||||
return user as OpenPaasUserData;
|
||||
} catch (err) {
|
||||
const error = err as { response?: { status?: number } };
|
||||
return rejectWithValue({
|
||||
message: formatReduxError(err),
|
||||
status: err.response?.status,
|
||||
status: error.response?.status,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -31,10 +51,11 @@ export const updateUserConfigurationsAsync = createAsyncThunk<
|
||||
try {
|
||||
await updateUserConfigurations(updates);
|
||||
return updates;
|
||||
} catch (err: any) {
|
||||
} catch (err) {
|
||||
const error = err as { response?: { status?: number } };
|
||||
return rejectWithValue({
|
||||
message: formatReduxError(err),
|
||||
status: err.response?.status,
|
||||
status: error.response?.status,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -46,11 +67,11 @@ export const userSlice = createSlice({
|
||||
organiserData: null as unknown as userOrganiser,
|
||||
tokens: null as unknown as Record<string, string>,
|
||||
coreConfig: {
|
||||
language: null as string | null,
|
||||
language: null,
|
||||
datetime: {
|
||||
timeZone: null as string | null,
|
||||
timeZone: null,
|
||||
},
|
||||
} as Record<string, any>,
|
||||
} as CoreConfig,
|
||||
alarmEmailsEnabled: null as boolean | null,
|
||||
loading: true,
|
||||
error: null as unknown as string | null,
|
||||
@@ -76,7 +97,7 @@ export const userSlice = createSlice({
|
||||
},
|
||||
setTimezone: (state, action) => {
|
||||
if (!state.coreConfig.datetime) {
|
||||
state.coreConfig.datetime = {};
|
||||
state.coreConfig.datetime = { timeZone: null };
|
||||
}
|
||||
state.coreConfig.datetime.timeZone = action.payload;
|
||||
if (state.userData) {
|
||||
@@ -110,40 +131,44 @@ export const userSlice = createSlice({
|
||||
state.organiserData.cal_address = action.payload.preferredEmail;
|
||||
state.userData.email = action.payload.preferredEmail;
|
||||
}
|
||||
|
||||
// Extract data from configurations.modules
|
||||
if (action.payload.configurations?.modules) {
|
||||
const coreModule = action.payload.configurations.modules.find(
|
||||
(module: any) => module.name === "core"
|
||||
(module: ModuleConfiguration) => module.name === "core"
|
||||
);
|
||||
if (coreModule?.configurations) {
|
||||
const newCoreConfig = Object.fromEntries(
|
||||
coreModule.configurations.map(
|
||||
(e: { name: string; value: any }) => [e.name, e.value]
|
||||
)
|
||||
coreModule.configurations.map((e: ConfigurationItem) => [
|
||||
e.name,
|
||||
e.value,
|
||||
])
|
||||
);
|
||||
|
||||
state.coreConfig = {
|
||||
...state.coreConfig,
|
||||
...newCoreConfig,
|
||||
};
|
||||
} as CoreConfig;
|
||||
const languageConfig = coreModule.configurations.find(
|
||||
(config: any) => config.name === "language"
|
||||
(config: ConfigurationItem) => config.name === "language"
|
||||
);
|
||||
if (languageConfig?.value) {
|
||||
state.coreConfig.language = languageConfig.value;
|
||||
state.coreConfig.language = languageConfig.value as string;
|
||||
if (state.userData)
|
||||
state.userData.language = languageConfig.value;
|
||||
state.userData.language = languageConfig.value as string;
|
||||
}
|
||||
|
||||
const datetimeConfig = coreModule.configurations.find(
|
||||
(config: any) => config.name === "datetime"
|
||||
(config: ConfigurationItem) => config.name === "datetime"
|
||||
);
|
||||
if (datetimeConfig?.value) {
|
||||
const serverTimeZone = datetimeConfig.value.timeZone;
|
||||
const datetimeValue = datetimeConfig.value as {
|
||||
timeZone?: string;
|
||||
};
|
||||
const serverTimeZone = datetimeValue.timeZone;
|
||||
state.coreConfig.datetime = {
|
||||
...state.coreConfig.datetime,
|
||||
...datetimeConfig.value,
|
||||
...(typeof datetimeConfig.value === "object" &&
|
||||
datetimeConfig.value !== null
|
||||
? datetimeConfig.value
|
||||
: {}),
|
||||
timeZone: serverTimeZone !== undefined ? serverTimeZone : null,
|
||||
};
|
||||
if (state.userData) {
|
||||
@@ -160,14 +185,13 @@ export const userSlice = createSlice({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract alarmEmails from configurations.modules
|
||||
const calendarModule = action.payload.configurations.modules.find(
|
||||
(module: any) => module.name === "calendar"
|
||||
(module: ModuleConfiguration) => module.name === "calendar"
|
||||
);
|
||||
if (calendarModule?.configurations) {
|
||||
const alarmEmailsConfig = calendarModule.configurations.find(
|
||||
(config: any) => config.name === "alarmEmails"
|
||||
(config: ConfigurationItem) => config.name === "alarmEmails"
|
||||
);
|
||||
if (alarmEmailsConfig) {
|
||||
state.alarmEmailsEnabled = alarmEmailsConfig.value === true;
|
||||
@@ -194,7 +218,7 @@ export const userSlice = createSlice({
|
||||
}
|
||||
if (action.payload.timezone !== undefined) {
|
||||
if (!state.coreConfig.datetime) {
|
||||
state.coreConfig.datetime = {};
|
||||
state.coreConfig.datetime = { timeZone: null };
|
||||
}
|
||||
state.coreConfig.datetime.timeZone = action.payload.timezone;
|
||||
if (state.userData) {
|
||||
|
||||
+4
-3
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import "@testing-library/jest-dom";
|
||||
|
||||
import { TextEncoder } from "util";
|
||||
@@ -49,7 +50,7 @@ class ResizeObserverMock {
|
||||
disconnect() {}
|
||||
}
|
||||
|
||||
(global as any).ResizeObserver = ResizeObserverMock;
|
||||
global.ResizeObserver = ResizeObserverMock;
|
||||
|
||||
class IntersectionObserverMock {
|
||||
observe() {}
|
||||
@@ -59,8 +60,8 @@ class IntersectionObserverMock {
|
||||
|
||||
(global as any).IntersectionObserver = IntersectionObserverMock;
|
||||
if (typeof window !== "undefined") {
|
||||
(window as any).WS_PING_PERIOD_MS = 5000;
|
||||
(window as any).WS_PING_TIMEOUT_PERIOD_MS = 5000;
|
||||
window.WS_PING_PERIOD_MS = 5000;
|
||||
window.WS_PING_TIMEOUT_PERIOD_MS = 5000;
|
||||
}
|
||||
// Suppress jsdom CSS selector parsing errors for Emotion/MUI
|
||||
if (typeof window !== "undefined" && window.getComputedStyle) {
|
||||
|
||||
@@ -11,7 +11,7 @@ const mockWindow = {
|
||||
VIDEO_CONFERENCE_BASE_URL: "https://meet.linagora.com",
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
// @ts-expect-error : Mock window object for Node.js environment
|
||||
global.window = mockWindow;
|
||||
|
||||
describe("videoConferenceUtils", () => {
|
||||
|
||||
@@ -8,7 +8,7 @@ const RETRY_CONFIG = {
|
||||
maxDelay: 120000,
|
||||
};
|
||||
export const api = ky.extend({
|
||||
prefixUrl: (window as any).CALENDAR_BASE_URL,
|
||||
prefixUrl: window.CALENDAR_BASE_URL,
|
||||
retry: {
|
||||
limit: RETRY_CONFIG.maxRetries,
|
||||
backoffLimit: RETRY_CONFIG.maxDelay,
|
||||
@@ -90,7 +90,7 @@ export function isValidUrl(string?: string) {
|
||||
|
||||
try {
|
||||
url = new URL(string ?? "");
|
||||
} catch (_) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return url;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { AsyncThunkResult } from "@/features/Calendars/types/AsyncThunkResult";
|
||||
|
||||
export async function assertThunkSuccess(result: unknown): Promise<void> {
|
||||
const typed = result as AsyncThunkResult;
|
||||
if (typed?.type?.endsWith("/rejected")) {
|
||||
throw new Error(
|
||||
typed.error?.message || typed.payload?.message || "API call failed"
|
||||
);
|
||||
}
|
||||
if (typeof typed.unwrap === "function") {
|
||||
await typed.unwrap();
|
||||
}
|
||||
}
|
||||
+20
-3
@@ -1,9 +1,26 @@
|
||||
import { RejectedError } from "@/features/Calendars/types/RejectedError";
|
||||
|
||||
export function formatReduxError(error: unknown): string {
|
||||
if (!error) return "Unknown error";
|
||||
|
||||
if (typeof error === "string") return error;
|
||||
if (typeof error === "object") {
|
||||
const err = error as any;
|
||||
if (err?.message) return err.message;
|
||||
|
||||
if (typeof error === "object" && error !== null) {
|
||||
if (
|
||||
"message" in error &&
|
||||
typeof (error as { message: unknown }).message === "string"
|
||||
) {
|
||||
return (error as { message: string }).message;
|
||||
}
|
||||
}
|
||||
|
||||
return "Unexpected error occurred";
|
||||
}
|
||||
|
||||
export function toRejectedError(err: unknown): RejectedError {
|
||||
const status =
|
||||
typeof err === "object" && err !== null && "response" in err
|
||||
? (err as { response?: { status?: number } }).response?.status
|
||||
: undefined;
|
||||
return { message: formatReduxError(err), status };
|
||||
}
|
||||
|
||||
@@ -22,14 +22,14 @@ export function registerTimezones() {
|
||||
}
|
||||
}
|
||||
|
||||
function buildTimezone(tzid: string, ics: string): any {
|
||||
function buildTimezone(tzid: string, ics: string): ICAL.Timezone {
|
||||
return (
|
||||
ICAL.TimezoneService.get(tzid) ||
|
||||
new ICAL.Timezone(new ICAL.Component(ICAL.parse(ics)))
|
||||
);
|
||||
}
|
||||
|
||||
function findTimezone(tzid: string): any {
|
||||
function findTimezone(tzid: string): ICAL.Timezone {
|
||||
if (TIMEZONES.zones[tzid]) {
|
||||
return buildTimezone(tzid, TIMEZONES.zones[tzid].ics);
|
||||
}
|
||||
|
||||
@@ -25,9 +25,7 @@ export function generateMeetingId(): string {
|
||||
*/
|
||||
export function generateMeetingLink(baseUrl?: string): string {
|
||||
const base =
|
||||
baseUrl ||
|
||||
(window as any).VIDEO_CONFERENCE_BASE_URL ||
|
||||
"https://meet.linagora.com";
|
||||
baseUrl || window.VIDEO_CONFERENCE_BASE_URL || "https://meet.linagora.com";
|
||||
const meetingId = generateMeetingId();
|
||||
return `${base}/${meetingId}`;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useAppDispatch, useAppSelector } from "@/app/hooks";
|
||||
import { AppDispatch } from "@/app/store";
|
||||
import { Calendar } from "@/features/Calendars/CalendarTypes";
|
||||
import { useSelectedCalendars } from "@/utils/storage/useSelectedCalendars";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useI18n } from "twake-i18n";
|
||||
@@ -51,7 +52,9 @@ export function WebSocketGate() {
|
||||
useAppSelector((state) => state?.calendars?.templist) ?? {}
|
||||
);
|
||||
|
||||
const calendarsToRefreshRef = useRef<Map<string, any>>(new Map());
|
||||
const calendarsToRefreshRef = useRef<
|
||||
Map<string, { calendar: Calendar; type?: "temp" }>
|
||||
>(new Map());
|
||||
const calendarsToHideRef = useRef<Set<string>>(new Set());
|
||||
const debouncedUpdateFnRef = useRef<
|
||||
((dispatch: AppDispatch) => void) | undefined
|
||||
@@ -102,6 +105,7 @@ export function WebSocketGate() {
|
||||
clearReconnectTimeout();
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[scheduleReconnect, clearReconnectTimeout]
|
||||
);
|
||||
|
||||
@@ -111,6 +115,7 @@ export function WebSocketGate() {
|
||||
(error as ErrorEvent)?.message ?? error.type ?? "unknown";
|
||||
setWebSocketStatus(t("websocket.error", { error: errorMessage }));
|
||||
setWebSocketStatusSerity("error");
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const callBacks = useMemo(
|
||||
@@ -145,6 +150,7 @@ export function WebSocketGate() {
|
||||
|
||||
clearReconnectTimeout();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isSocketOpen, clearReconnectTimeout]);
|
||||
|
||||
// Manage WebSocket connection
|
||||
@@ -206,6 +212,7 @@ export function WebSocketGate() {
|
||||
abortController.abort();
|
||||
cleanup();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
isAuthenticated,
|
||||
callBacks,
|
||||
@@ -220,7 +227,7 @@ export function WebSocketGate() {
|
||||
|
||||
// If we just reconnected, force a re-sync
|
||||
if (justReconnectedRef.current && isSocketOpen) {
|
||||
console.log("Re-syncing calendars after reconnection");
|
||||
console.info("Re-syncing calendars after reconnection");
|
||||
previousCalendarListRef.current = [];
|
||||
previousTempCalendarListRef.current = [];
|
||||
justReconnectedRef.current = false;
|
||||
@@ -277,6 +284,7 @@ export function WebSocketGate() {
|
||||
window.removeEventListener("online", handleOnline);
|
||||
window.removeEventListener("offline", handleOffline);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isSocketOpen, isAuthenticated, clearReconnectTimeout]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -315,6 +323,7 @@ export function WebSocketGate() {
|
||||
pingCleanupRef.current = null;
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isSocketOpen]);
|
||||
|
||||
return websocketStatus ? (
|
||||
|
||||
@@ -6,8 +6,8 @@ export async function createWebSocketConnection(
|
||||
callbacks: WebSocketCallbacks
|
||||
): Promise<WebSocketWithCleanup> {
|
||||
const wsBaseUrl =
|
||||
(window as any).WEBSOCKET_URL ??
|
||||
(window as any).CALENDAR_BASE_URL?.replace(
|
||||
window.WEBSOCKET_URL ??
|
||||
window.CALENDAR_BASE_URL?.replace(
|
||||
/^http(s)?:/,
|
||||
(_: string, s: string | undefined) => (s ? "wss:" : "ws:")
|
||||
) ??
|
||||
@@ -37,7 +37,7 @@ export async function createWebSocketConnection(
|
||||
}, CONNECTION_TIMEOUT_MS);
|
||||
|
||||
const openHandler = () => {
|
||||
console.log("WebSocket connection opened");
|
||||
console.info("WebSocket connection opened");
|
||||
clearTimeout(timeoutId);
|
||||
socket.removeEventListener(
|
||||
WS_INBOUND_EVENTS.CONNECTION_OPENED,
|
||||
@@ -73,7 +73,7 @@ export async function createWebSocketConnection(
|
||||
};
|
||||
|
||||
const closeHandler = (event: CloseEvent) => {
|
||||
console.log("WebSocket closed:", event.code, event.reason);
|
||||
console.info("WebSocket closed:", event.code, event.reason);
|
||||
cleanup();
|
||||
callbacks.onClose?.(event);
|
||||
};
|
||||
|
||||
@@ -19,8 +19,8 @@ export interface PingCleanup {
|
||||
sendPing: () => void;
|
||||
}
|
||||
|
||||
const DEFAULT_PING_INTERVAL = (window as any).WS_PING_PERIOD_MS ?? 30000;
|
||||
const DEFAULT_PONG_TIMEOUT = (window as any).WS_PING_TIMEOUT_PERIOD_MS ?? 35000;
|
||||
const DEFAULT_PING_INTERVAL = window.WS_PING_PERIOD_MS ?? 30000;
|
||||
const DEFAULT_PONG_TIMEOUT = window.WS_PING_TIMEOUT_PERIOD_MS ?? 35000;
|
||||
|
||||
/**
|
||||
* Sets up a ping/pong mechanism to monitor WebSocket connection health
|
||||
|
||||
@@ -18,6 +18,7 @@ export function useWebSocketReconnect(
|
||||
clearTimeout(reconnectTimeoutRef.current);
|
||||
reconnectTimeoutRef.current = null;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const scheduleReconnect = useCallback(() => {
|
||||
@@ -35,7 +36,7 @@ export function useWebSocketReconnect(
|
||||
const delay = getRetryDelay(reconnectAttemptsRef.current, RECONNECT_CONFIG);
|
||||
reconnectAttemptsRef.current += 1;
|
||||
|
||||
console.log(
|
||||
console.info(
|
||||
`Scheduling WebSocket reconnection in ${Math.round(delay)}ms ` +
|
||||
`(attempt ${reconnectAttemptsRef.current}/${MAX_RECONNECT_ATTEMPTS})`
|
||||
);
|
||||
@@ -45,12 +46,13 @@ export function useWebSocketReconnect(
|
||||
reconnectTimeoutRef.current = null;
|
||||
return;
|
||||
}
|
||||
console.log(
|
||||
console.info(
|
||||
`Attempting WebSocket reconnection (attempt ${reconnectAttemptsRef.current}/${MAX_RECONNECT_ATTEMPTS})`
|
||||
);
|
||||
setShouldConnect((prev) => !prev);
|
||||
clearReconnectTimeout();
|
||||
}, delay);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [clearReconnectTimeout]);
|
||||
return { scheduleReconnect, clearReconnectTimeout };
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { AppDispatch } from "@/app/store";
|
||||
import { Calendar } from "@/features/Calendars/CalendarTypes";
|
||||
|
||||
export interface UpdateCalendarsAccumulators {
|
||||
calendarsToRefresh: Map<string, any>;
|
||||
calendarsToRefresh: Map<string, { calendar: Calendar; type?: "temp" }>;
|
||||
calendarsToHide: Set<string>;
|
||||
debouncedUpdateFn?: (dispatch: AppDispatch) => void;
|
||||
currentDebouncePeriod?: number;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { AppDispatch } from "@/app/store";
|
||||
import { store } from "@/app/store";
|
||||
import { Calendar } from "@/features/Calendars/CalendarTypes";
|
||||
import { refreshCalendarWithSyncToken } from "@/features/Calendars/services";
|
||||
import { findCalendarById, getDisplayedCalendarRange } from "@/utils";
|
||||
import { setSelectedCalendars } from "@/utils/storage/setSelectedCalendars";
|
||||
@@ -12,7 +13,10 @@ const DEFAULT_DEBOUNCE_MS = 0;
|
||||
|
||||
function createDebouncedUpdate(
|
||||
debouncePeriodMs: number,
|
||||
getCalendarsToRefresh: () => Map<string, any>,
|
||||
getCalendarsToRefresh: () => Map<
|
||||
string,
|
||||
{ calendar: Calendar; type?: "temp" }
|
||||
>,
|
||||
getCalendarsToHide: () => Set<string>
|
||||
) {
|
||||
return debounce(
|
||||
@@ -55,8 +59,7 @@ export function updateCalendars(
|
||||
);
|
||||
accumulateCalendarsToHide(calendarsToHide, accumulators.calendarsToHide);
|
||||
|
||||
const debouncePeriod =
|
||||
(window as any).WS_DEBOUNCE_PERIOD_MS ?? DEFAULT_DEBOUNCE_MS;
|
||||
const debouncePeriod = window.WS_DEBOUNCE_PERIOD_MS ?? DEFAULT_DEBOUNCE_MS;
|
||||
|
||||
if (debouncePeriod > 0) {
|
||||
if (
|
||||
@@ -94,7 +97,7 @@ export function updateCalendars(
|
||||
function accumulateCalendarsToRefresh(
|
||||
state: ReturnType<typeof store.getState>,
|
||||
calendarPaths: Set<string>,
|
||||
calendarsToRefreshMap: Map<string, any>
|
||||
calendarsToRefreshMap: Map<string, { calendar: Calendar; type?: "temp" }>
|
||||
) {
|
||||
calendarPaths.forEach((calendarPath) => {
|
||||
const calendarId = parseCalendarPath(calendarPath);
|
||||
@@ -126,7 +129,7 @@ function accumulateCalendarsToHide(
|
||||
function processCalendarsToRefresh(
|
||||
dispatch: AppDispatch,
|
||||
currentRange: { start: Date; end: Date },
|
||||
calendarsMap: Map<string, any>
|
||||
calendarsMap: Map<string, { calendar: Calendar; type?: "temp" }>
|
||||
) {
|
||||
calendarsMap.forEach((calendar) => {
|
||||
dispatch(
|
||||
|
||||
@@ -12,5 +12,5 @@ export function registerToCalendars(
|
||||
})
|
||||
);
|
||||
|
||||
console.log("Registered to calendars", calendarURIList);
|
||||
console.info("Registered to calendars", calendarURIList);
|
||||
}
|
||||
|
||||
@@ -12,5 +12,5 @@ export function unregisterToCalendars(
|
||||
})
|
||||
);
|
||||
|
||||
console.log("Unregistered to calendars", calendarURIList);
|
||||
console.info("Unregistered to calendars", calendarURIList);
|
||||
}
|
||||
|
||||
Vendored
+33
@@ -0,0 +1,33 @@
|
||||
import type { AppIconProps } from "@/components/Menubar/Menubar";
|
||||
import type { MutableRefObject } from "react";
|
||||
import type { CalendarApi } from "@fullcalendar/core";
|
||||
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
SSO_BASE_URL: string;
|
||||
SSO_CLIENT_ID: string;
|
||||
SSO_SCOPE: string;
|
||||
SSO_REDIRECT_URI: string;
|
||||
SSO_RESPONSE_TYPE: "code";
|
||||
SSO_CODE_CHALLENGE_METHOD: "S256";
|
||||
SSO_POST_LOGOUT_REDIRECT: string;
|
||||
|
||||
CALENDAR_BASE_URL: string;
|
||||
MAIL_SPA_URL: string;
|
||||
VIDEO_CONFERENCE_BASE_URL: string;
|
||||
|
||||
DEBUG: boolean;
|
||||
LANG: string;
|
||||
|
||||
WEBSOCKET_URL: string;
|
||||
WS_DEBOUNCE_PERIOD_MS: number;
|
||||
WS_PING_PERIOD_MS: number;
|
||||
WS_PING_TIMEOUT_PERIOD_MS: number;
|
||||
|
||||
appList: AppIconProps[];
|
||||
|
||||
__calendarRef: MutableRefObject<CalendarApi | null>;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user