Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user