325d33caef
* [#361] added back button * [#361] greatly improved looks of results * [#361] added popup on click * [#361] removed classnames with \!important replaced with style in component --------- Co-authored-by: Camille Moussu <cmoussu@linagora.com>
1181 lines
35 KiB
TypeScript
1181 lines
35 KiB
TypeScript
import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit";
|
|
import { Calendars } from "./CalendarTypes";
|
|
import { CalendarEvent } from "../Events/EventsTypes";
|
|
import {
|
|
addSharedCalendar,
|
|
getCalendar,
|
|
getCalendars,
|
|
postCalendar,
|
|
proppatchCalendar,
|
|
removeCalendar,
|
|
updateAclCalendar,
|
|
} from "./CalendarApi";
|
|
import { getOpenPaasUser, getUserDetails } from "../User/userAPI";
|
|
import { parseCalendarEvent } from "../Events/eventUtils";
|
|
import {
|
|
deleteEvent,
|
|
deleteEventInstance,
|
|
getEvent,
|
|
importEventFromFile,
|
|
moveEvent,
|
|
putEvent,
|
|
putEventWithOverrides,
|
|
updateSeries,
|
|
} from "../Events/EventApi";
|
|
import {
|
|
computeWeekRange,
|
|
formatDateToYYYYMMDDTHHMMSS,
|
|
} from "../../utils/dateUtils";
|
|
import { User } from "../../components/Attendees/PeopleSearch";
|
|
import { getCalendarVisibility } from "../../components/Calendar/utils/calendarUtils";
|
|
import { importFile } from "../../utils/apiUtils";
|
|
import { formatReduxError } from "../../utils/errorUtils";
|
|
|
|
// Define error type for rejected actions
|
|
interface RejectedError {
|
|
message: string;
|
|
status?: number;
|
|
}
|
|
|
|
export const getCalendarsListAsync = createAsyncThunk<
|
|
{ importedCalendars: Record<string, Calendars>; errors: string }, // Return type
|
|
void, // Arg type
|
|
{ rejectValue: RejectedError; state: any } // ThunkAPI config
|
|
>("calendars/getCalendars", async (_, { rejectWithValue, getState }) => {
|
|
const state = getState() as any;
|
|
if (Object.keys(state.calendars.list).length > 0) {
|
|
return {
|
|
importedCalendars: state.calendars.list,
|
|
errors: "",
|
|
};
|
|
}
|
|
|
|
try {
|
|
const importedCalendars: Record<string, Calendars> = {};
|
|
const user = (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
|
|
>[];
|
|
const errors: string[] = [];
|
|
|
|
const normalizedCalendars = rawCalendars.map((cal) => {
|
|
const description = cal["caldav:description"];
|
|
let delegated = false;
|
|
let source = cal["calendarserver:source"]
|
|
? cal["calendarserver:source"]._links.self.href
|
|
: cal._links.self.href;
|
|
const link = cal._links.self.href;
|
|
if (cal["calendarserver:delegatedsource"]) {
|
|
source = cal["calendarserver:delegatedsource"];
|
|
delegated = true;
|
|
}
|
|
const id = source.replace("/calendars/", "").replace(".json", "");
|
|
const ownerId = id.split("/")[0];
|
|
const visibility = getCalendarVisibility(cal["acl"]);
|
|
return {
|
|
cal,
|
|
description,
|
|
delegated,
|
|
source,
|
|
link,
|
|
id,
|
|
ownerId,
|
|
visibility,
|
|
};
|
|
});
|
|
|
|
const uniqueOwnerIds = Array.from(
|
|
new Set(normalizedCalendars.map(({ ownerId }) => ownerId).filter(Boolean))
|
|
);
|
|
|
|
const ownerDataMap = new Map<string, any>();
|
|
const OWNER_BATCH_SIZE = 20;
|
|
|
|
const fetchOwnerData = async (ownerId: string) => {
|
|
try {
|
|
const data = await getUserDetails(ownerId);
|
|
ownerDataMap.set(ownerId, data);
|
|
} catch (error: any) {
|
|
console.error(`Failed to fetch user details for ${ownerId}:`, error);
|
|
ownerDataMap.set(ownerId, {
|
|
firstname: "",
|
|
lastname: "Unknown User",
|
|
emails: [],
|
|
});
|
|
errors.push(formatReduxError(error));
|
|
}
|
|
};
|
|
|
|
for (let i = 0; i < uniqueOwnerIds.length; i += OWNER_BATCH_SIZE) {
|
|
const chunk = uniqueOwnerIds.slice(i, i + OWNER_BATCH_SIZE);
|
|
await Promise.all(chunk.map((ownerId) => fetchOwnerData(ownerId)));
|
|
}
|
|
|
|
normalizedCalendars.forEach(
|
|
({ cal, description, delegated, link, id, ownerId, visibility }) => {
|
|
const ownerData = ownerDataMap.get(ownerId) || {
|
|
firstname: "",
|
|
lastname: "Unknown User",
|
|
emails: [],
|
|
};
|
|
const name =
|
|
ownerId !== user.id && cal["dav:name"] === "#default"
|
|
? `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${
|
|
ownerData.lastname
|
|
}` + "'s calendar"
|
|
: cal["dav:name"];
|
|
|
|
const color = {
|
|
light: cal["apple:color"] ?? "#006BD8",
|
|
dark: cal["X-TWAKE-Dark-theme-color"] ?? "#FFF",
|
|
};
|
|
importedCalendars[id] = {
|
|
id,
|
|
name,
|
|
link,
|
|
owner: `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${
|
|
ownerData.lastname
|
|
}`,
|
|
ownerEmails: ownerData.emails,
|
|
description,
|
|
delegated,
|
|
color,
|
|
visibility,
|
|
events: {},
|
|
};
|
|
}
|
|
);
|
|
|
|
return { importedCalendars, errors: errors.join("\n") };
|
|
} catch (err: any) {
|
|
return rejectWithValue({
|
|
message: formatReduxError(err),
|
|
status: err.response?.status,
|
|
});
|
|
}
|
|
});
|
|
|
|
export const getTempCalendarsListAsync = createAsyncThunk<
|
|
Record<string, Calendars>,
|
|
User,
|
|
{ rejectValue: RejectedError }
|
|
>("calendars/getTempCalendars", async (tempUser, { rejectWithValue }) => {
|
|
try {
|
|
const importedCalendars: Record<string, Calendars> = {};
|
|
|
|
const calendars = (await getCalendars(
|
|
tempUser.openpaasId ?? "",
|
|
"sharedPublic=true&"
|
|
)) as Record<string, any>;
|
|
const rawCalendars = calendars._embedded["dav:calendar"];
|
|
|
|
for (const cal of rawCalendars) {
|
|
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;
|
|
|
|
const id = source.replace("/calendars/", "").replace(".json", "");
|
|
const visibility = getCalendarVisibility(cal["acl"]);
|
|
const ownerData: any = await getUserDetails(id.split("/")[0]);
|
|
|
|
importedCalendars[id] = {
|
|
id,
|
|
name,
|
|
link,
|
|
owner: `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${ownerData.lastname}`,
|
|
ownerEmails: ownerData.emails,
|
|
description,
|
|
delegated,
|
|
color: {
|
|
light: tempUser.color?.light ?? "#a8a8a8ff",
|
|
dark: tempUser.color?.dark ?? "#a8a8a8ff",
|
|
},
|
|
visibility,
|
|
events: {},
|
|
};
|
|
}
|
|
|
|
return importedCalendars;
|
|
} catch (err: any) {
|
|
return rejectWithValue({
|
|
message: formatReduxError(err),
|
|
status: err.response?.status,
|
|
});
|
|
}
|
|
});
|
|
|
|
export const getCalendarDetailAsync = createAsyncThunk<
|
|
{ calId: string; events: CalendarEvent[]; calType?: string },
|
|
{
|
|
calId: string;
|
|
match: { start: string; end: string };
|
|
calType?: string;
|
|
signal?: AbortSignal;
|
|
},
|
|
{ rejectValue: RejectedError }
|
|
>(
|
|
"calendars/getCalendarDetails",
|
|
async ({ calId, match, calType, signal }, { rejectWithValue }) => {
|
|
try {
|
|
const calendar = (await getCalendar(calId, match, signal)) as Record<
|
|
string,
|
|
any
|
|
>;
|
|
const color = calendar["apple:color"];
|
|
const events: CalendarEvent[] = calendar._embedded["dav:item"].flatMap(
|
|
(eventdata: any) => {
|
|
const vevents = eventdata.data[2] as any[][];
|
|
const valarm = eventdata.data[2][0][2][0];
|
|
const eventURL = eventdata._links.self.href;
|
|
return vevents.map((vevent: any[]) => {
|
|
return parseCalendarEvent(
|
|
vevent[1],
|
|
color,
|
|
calId,
|
|
eventURL,
|
|
valarm
|
|
);
|
|
});
|
|
}
|
|
);
|
|
|
|
return { calId, events, calType };
|
|
} catch (err: any) {
|
|
return rejectWithValue({
|
|
message: formatReduxError(err),
|
|
status: err.response?.status,
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
export const putEventAsync = createAsyncThunk<
|
|
{ calId: string; events: CalendarEvent[]; calType?: "temp" },
|
|
{ cal: Calendars; newEvent: CalendarEvent; calType?: "temp" },
|
|
{ rejectValue: RejectedError }
|
|
>(
|
|
"calendars/putEvent",
|
|
async ({ cal, newEvent, calType }, { rejectWithValue }) => {
|
|
try {
|
|
await putEvent(
|
|
newEvent,
|
|
cal.ownerEmails ? cal.ownerEmails[0] : undefined
|
|
);
|
|
const eventDate = new Date(newEvent.start);
|
|
|
|
const { start: weekStart, end: weekEnd } = computeWeekRange(eventDate);
|
|
|
|
const calEvents = (await getCalendar(cal.id, {
|
|
start: formatDateToYYYYMMDDTHHMMSS(weekStart),
|
|
end: formatDateToYYYYMMDDTHHMMSS(weekEnd),
|
|
})) as Record<string, any>;
|
|
const events: CalendarEvent[] = calEvents._embedded["dav:item"].flatMap(
|
|
(eventdata: any) => {
|
|
const vevents = eventdata.data[2] as any[][];
|
|
const eventURL = eventdata._links.self.href;
|
|
const valarm = eventdata.data[2][0][2][0];
|
|
return vevents.map((vevent: any[]) => {
|
|
return parseCalendarEvent(
|
|
vevent[1],
|
|
cal.color ?? {},
|
|
cal.id,
|
|
eventURL,
|
|
valarm
|
|
);
|
|
});
|
|
}
|
|
);
|
|
|
|
return {
|
|
calId: cal.id,
|
|
events,
|
|
calType,
|
|
};
|
|
} catch (err: any) {
|
|
return rejectWithValue({
|
|
message: formatReduxError(err),
|
|
status: err.response?.status,
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
export const getEventAsync = createAsyncThunk<
|
|
{ calId: string; event: CalendarEvent },
|
|
CalendarEvent,
|
|
{ rejectValue: RejectedError }
|
|
>("calendars/getEvent", async (event, { rejectWithValue }) => {
|
|
try {
|
|
const response: CalendarEvent = await getEvent(event);
|
|
return {
|
|
calId: event.calId,
|
|
event: response,
|
|
};
|
|
} catch (err: any) {
|
|
return rejectWithValue({
|
|
message: formatReduxError(err),
|
|
status: err.response?.status,
|
|
});
|
|
}
|
|
});
|
|
|
|
export const patchCalendarAsync = createAsyncThunk<
|
|
{
|
|
calId: string;
|
|
calLink: string;
|
|
patch: { name: string; desc: string; color: Record<string, string> };
|
|
},
|
|
{
|
|
calId: string;
|
|
calLink: string;
|
|
patch: { name: string; desc: string; color: Record<string, string> };
|
|
},
|
|
{ rejectValue: RejectedError }
|
|
>(
|
|
"calendars/patchCalendar",
|
|
async ({ calId, calLink, patch }, { rejectWithValue }) => {
|
|
try {
|
|
await proppatchCalendar(calLink, patch);
|
|
return {
|
|
calId,
|
|
calLink,
|
|
patch,
|
|
};
|
|
} catch (err: any) {
|
|
return rejectWithValue({
|
|
message: formatReduxError(err),
|
|
status: err.response?.status,
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
export const removeCalendarAsync = createAsyncThunk<
|
|
{
|
|
calId: string;
|
|
},
|
|
{
|
|
calId: string;
|
|
calLink: string;
|
|
},
|
|
{ rejectValue: RejectedError }
|
|
>(
|
|
"calendars/removeCalendar",
|
|
async ({ calId, calLink }, { rejectWithValue }) => {
|
|
try {
|
|
await removeCalendar(calLink);
|
|
return {
|
|
calId,
|
|
calLink,
|
|
};
|
|
} catch (err: any) {
|
|
return rejectWithValue({
|
|
message: formatReduxError(err),
|
|
status: err.response?.status,
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
export const moveEventAsync = createAsyncThunk<
|
|
{ calId: string; events: CalendarEvent[] },
|
|
{ cal: Calendars; newEvent: CalendarEvent; newURL: string },
|
|
{ rejectValue: RejectedError }
|
|
>(
|
|
"calendars/moveEvent",
|
|
async ({ cal, newEvent, newURL }, { rejectWithValue }) => {
|
|
try {
|
|
await moveEvent(newEvent, newURL);
|
|
|
|
const eventDate = new Date(newEvent.start);
|
|
const { start: weekStart, end: weekEnd } = computeWeekRange(eventDate);
|
|
|
|
const calEvents = (await getCalendar(cal.id, {
|
|
start: formatDateToYYYYMMDDTHHMMSS(weekStart),
|
|
end: formatDateToYYYYMMDDTHHMMSS(weekEnd),
|
|
})) as Record<string, any>;
|
|
const events: CalendarEvent[] = calEvents._embedded["dav:item"].flatMap(
|
|
(eventdata: any) => {
|
|
const vevents = eventdata.data[2] as any[][];
|
|
const eventURL = eventdata._links.self.href;
|
|
return vevents.map((vevent: any[]) => {
|
|
return parseCalendarEvent(
|
|
vevent[1],
|
|
cal.color ?? {},
|
|
cal.id,
|
|
eventURL
|
|
);
|
|
});
|
|
}
|
|
);
|
|
|
|
return {
|
|
calId: cal.id,
|
|
events,
|
|
};
|
|
} catch (err: any) {
|
|
return rejectWithValue({
|
|
message: formatReduxError(err),
|
|
status: err.response?.status,
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
export const patchACLCalendarAsync = createAsyncThunk<
|
|
{
|
|
calId: string;
|
|
calLink: string;
|
|
request: string;
|
|
},
|
|
{
|
|
calId: string;
|
|
calLink: string;
|
|
request: string;
|
|
},
|
|
{ rejectValue: RejectedError }
|
|
>(
|
|
"calendars/requestACLCalendar",
|
|
async ({ calId, calLink, request }, { rejectWithValue }) => {
|
|
try {
|
|
const response = await updateAclCalendar(calLink, request);
|
|
return {
|
|
calId,
|
|
calLink,
|
|
request,
|
|
};
|
|
} catch (err: any) {
|
|
return rejectWithValue({
|
|
message: formatReduxError(err),
|
|
status: err.response?.status,
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
export const deleteEventAsync = createAsyncThunk<
|
|
{ calId: string; eventId: string },
|
|
{ calId: string; eventId: string; eventURL: string },
|
|
{ rejectValue: RejectedError }
|
|
>(
|
|
"calendars/delEvent",
|
|
async ({ calId, eventId, eventURL }, { rejectWithValue }) => {
|
|
try {
|
|
await deleteEvent(eventURL);
|
|
return { calId, eventId };
|
|
} catch (err: any) {
|
|
return rejectWithValue({
|
|
message: formatReduxError(err),
|
|
status: err.response?.status,
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
export const deleteEventInstanceAsync = createAsyncThunk<
|
|
{ calId: string; eventId: string },
|
|
{ cal: Calendars; event: CalendarEvent },
|
|
{ rejectValue: RejectedError }
|
|
>("calendars/delEventInstance", async ({ cal, event }, { rejectWithValue }) => {
|
|
try {
|
|
await deleteEventInstance(event, cal.ownerEmails?.[0]);
|
|
return { calId: cal.id, eventId: event.uid };
|
|
} catch (err: any) {
|
|
return rejectWithValue({
|
|
message: formatReduxError(err),
|
|
status: err.response?.status,
|
|
});
|
|
}
|
|
});
|
|
|
|
export const updateEventInstanceAsync = createAsyncThunk<
|
|
{ calId: string; event: CalendarEvent },
|
|
{ cal: Calendars; event: CalendarEvent },
|
|
{ rejectValue: RejectedError }
|
|
>(
|
|
"calendars/updateEventInstance",
|
|
async ({ cal, event }, { rejectWithValue }) => {
|
|
try {
|
|
await putEventWithOverrides(event, cal.ownerEmails?.[0]);
|
|
return { calId: cal.id, event };
|
|
} catch (err: any) {
|
|
return rejectWithValue({
|
|
message: formatReduxError(err),
|
|
status: err.response?.status,
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
export const updateSeriesAsync = createAsyncThunk<
|
|
void,
|
|
{ cal: Calendars; event: CalendarEvent; removeOverrides?: boolean },
|
|
{ rejectValue: RejectedError }
|
|
>(
|
|
"calendars/updateSeries",
|
|
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,
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
export const createCalendarAsync = createAsyncThunk<
|
|
{
|
|
userId: string;
|
|
calId: string;
|
|
color: Record<string, string>;
|
|
name: string;
|
|
desc: string;
|
|
owner: string;
|
|
ownerEmails: string[];
|
|
},
|
|
{
|
|
userId: string;
|
|
calId: string;
|
|
color: Record<string, string>;
|
|
name: string;
|
|
desc: string;
|
|
},
|
|
{ rejectValue: RejectedError }
|
|
>(
|
|
"calendars/createCalendar",
|
|
async ({ userId, calId, color, name, desc }, { rejectWithValue }) => {
|
|
try {
|
|
await postCalendar(userId, calId, color, name, desc);
|
|
const ownerData: any = await getUserDetails(userId.split("/")[0]);
|
|
|
|
return {
|
|
userId,
|
|
calId,
|
|
color,
|
|
name,
|
|
desc,
|
|
owner: `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${
|
|
ownerData.lastname
|
|
}`,
|
|
ownerEmails: ownerData.emails,
|
|
};
|
|
} catch (err: any) {
|
|
return rejectWithValue({
|
|
message: formatReduxError(err),
|
|
status: err.response?.status,
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
export const addSharedCalendarAsync = createAsyncThunk<
|
|
{
|
|
calId: string;
|
|
color: Record<string, string>;
|
|
link: string;
|
|
name: string;
|
|
desc: string;
|
|
owner: string;
|
|
ownerEmails: string[];
|
|
},
|
|
{ userId: string; calId: string; cal: Record<string, any> },
|
|
{ rejectValue: RejectedError }
|
|
>(
|
|
"calendars/addSharedCalendar",
|
|
async ({ userId, calId, cal }, { rejectWithValue }) => {
|
|
try {
|
|
await addSharedCalendar(userId, calId, cal);
|
|
const ownerData: any = await getUserDetails(
|
|
cal.cal._links.self.href
|
|
.replace("/calendars/", "")
|
|
.replace(".json", "")
|
|
.split("/")[0]
|
|
);
|
|
|
|
return {
|
|
calId: cal.cal._links.self.href
|
|
.replace("/calendars/", "")
|
|
.replace(".json", ""),
|
|
color: cal.color,
|
|
link: `/calendars/${userId}/${calId}.json`,
|
|
desc: cal.cal["caldav:description"],
|
|
name:
|
|
ownerData.id !== userId && cal.cal["dav:name"] === "#default"
|
|
? `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${
|
|
ownerData.lastname
|
|
}` + "'s calendar"
|
|
: cal.cal["dav:name"],
|
|
owner: `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${
|
|
ownerData.lastname
|
|
}`,
|
|
ownerEmails: ownerData.emails,
|
|
};
|
|
} catch (err: any) {
|
|
return rejectWithValue({
|
|
message: formatReduxError(err),
|
|
status: err.response?.status,
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
export const importEventFromFileAsync = createAsyncThunk<
|
|
void,
|
|
{
|
|
calLink: string;
|
|
file: File;
|
|
},
|
|
{ rejectValue: RejectedError }
|
|
>("calendars/importEvent", async ({ calLink, file }, { rejectWithValue }) => {
|
|
try {
|
|
const id = ((await importFile(file)) as Record<string, string>)._id;
|
|
const response = await importEventFromFile(id, calLink);
|
|
} catch (err: any) {
|
|
return rejectWithValue({
|
|
message: formatReduxError(err),
|
|
status: err.response?.status,
|
|
});
|
|
}
|
|
});
|
|
|
|
const CalendarSlice = createSlice({
|
|
name: "calendars",
|
|
initialState: {
|
|
list: {} as Record<string, Calendars>,
|
|
templist: {} as Record<string, Calendars>,
|
|
pending: false,
|
|
error: null as string | null,
|
|
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
} as {
|
|
list: Record<string, Calendars>;
|
|
templist: Record<string, Calendars>;
|
|
pending: boolean;
|
|
error: string | null;
|
|
timeZone: string;
|
|
},
|
|
reducers: {
|
|
createCalendar: (
|
|
state,
|
|
action: PayloadAction<Record<string, string | Record<string, string>>>
|
|
) => {
|
|
const id = Date.now().toString(36);
|
|
state.list[id] = {} as Calendars;
|
|
state.list[id].name = action.payload.name as string;
|
|
state.list[id].color = action.payload.color as Record<string, string>;
|
|
state.list[id].description = action.payload.description as string;
|
|
state.list[id].events = {};
|
|
},
|
|
addEvent: (
|
|
state,
|
|
action: PayloadAction<{ calendarUid: string; event: CalendarEvent }>
|
|
) => {
|
|
if (!state.list[action.payload.calendarUid].events) {
|
|
state.list[action.payload.calendarUid].events = {};
|
|
}
|
|
state.list[action.payload.calendarUid].events[action.payload.event.uid] =
|
|
action.payload.event;
|
|
state.list[action.payload.calendarUid].events[
|
|
action.payload.event.uid
|
|
].URL = `/calendars/${action.payload.calendarUid}/${
|
|
action.payload.event.uid.split("/")[0]
|
|
}.isc`;
|
|
},
|
|
removeEvent: (
|
|
state,
|
|
action: PayloadAction<{ calendarUid: string; eventUid: string }>
|
|
) => {
|
|
delete state.list[action.payload.calendarUid].events[
|
|
action.payload.eventUid
|
|
];
|
|
},
|
|
removeTempCal: (state, action: PayloadAction<string>) => {
|
|
delete state.templist[action.payload];
|
|
},
|
|
emptyEventsCal: (
|
|
state,
|
|
action: PayloadAction<{ calId?: string; calType?: "temp" }>
|
|
) => {
|
|
const cals =
|
|
action.payload.calType === "temp" ? state.templist : state.list;
|
|
if (action.payload.calId) {
|
|
cals[action.payload.calId].events = {};
|
|
} else {
|
|
Object.keys(state.templist).forEach(
|
|
(calId) => (cals[calId].events = {})
|
|
);
|
|
}
|
|
},
|
|
updateEventLocal: (
|
|
state,
|
|
action: PayloadAction<{ calId: string; event: CalendarEvent }>
|
|
) => {
|
|
const { calId, event } = action.payload;
|
|
state.list[calId].events[event.uid] = event;
|
|
},
|
|
setTimeZone: (state, action: PayloadAction<string>) => {
|
|
state.timeZone = action.payload;
|
|
},
|
|
clearFetchCache: (state, action: PayloadAction<string>) => {
|
|
if (!state.list[action.payload]) return;
|
|
state.list[action.payload].lastCacheCleared = Date.now();
|
|
},
|
|
clearError: (state) => {
|
|
state.error = null;
|
|
},
|
|
updateCalColor: (
|
|
state,
|
|
action: PayloadAction<{
|
|
id: string;
|
|
color: { light: string; dark: string };
|
|
}>
|
|
) => {
|
|
state.list[action.payload.id].color = action.payload.color;
|
|
},
|
|
},
|
|
extraReducers: (builder) => {
|
|
builder
|
|
// Fulfilled cases
|
|
.addCase(
|
|
getCalendarsListAsync.fulfilled,
|
|
(
|
|
state,
|
|
action: PayloadAction<{
|
|
importedCalendars: Record<string, Calendars>;
|
|
errors: string;
|
|
}>
|
|
) => {
|
|
state.pending = false;
|
|
state.list = action.payload.importedCalendars;
|
|
state.error = action.payload.errors.length
|
|
? action.payload.errors
|
|
: null;
|
|
}
|
|
)
|
|
.addCase(
|
|
getTempCalendarsListAsync.fulfilled,
|
|
(state, action: PayloadAction<Record<string, Calendars>>) => {
|
|
state.pending = false;
|
|
Object.keys(action.payload).forEach(
|
|
(id) => (state.templist[id] = action.payload[id])
|
|
);
|
|
}
|
|
)
|
|
.addCase(
|
|
getCalendarDetailAsync.fulfilled,
|
|
(
|
|
state,
|
|
action: PayloadAction<{
|
|
calId: string;
|
|
events: CalendarEvent[];
|
|
calType?: string;
|
|
}>
|
|
) => {
|
|
state.pending = false;
|
|
const type = action.payload.calType === "temp" ? "templist" : "list";
|
|
|
|
if (!state[type][action.payload.calId]) {
|
|
return;
|
|
}
|
|
action.payload.events.forEach((event) => {
|
|
state[type][action.payload.calId].events[event.uid] = event;
|
|
});
|
|
Object.keys(state[type][action.payload.calId].events).forEach(
|
|
(id) => {
|
|
state[type][action.payload.calId].events[id].color =
|
|
state[type][action.payload.calId].color;
|
|
state[type][action.payload.calId].events[id].calId =
|
|
action.payload.calId;
|
|
if (!state[type][action.payload.calId].events[id].timezone) {
|
|
state[type][action.payload.calId].events[id].timezone =
|
|
Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
}
|
|
}
|
|
);
|
|
}
|
|
)
|
|
.addCase(
|
|
putEventAsync.fulfilled,
|
|
(
|
|
state,
|
|
action: PayloadAction<{
|
|
calId: string;
|
|
events: CalendarEvent[];
|
|
calType?: "temp";
|
|
}>
|
|
) => {
|
|
state.pending = false;
|
|
const type = action.payload.calType === "temp" ? "templist" : "list";
|
|
|
|
if (!state[type][action.payload.calId]) {
|
|
state[type][action.payload.calId] = {
|
|
id: action.payload.calId,
|
|
events: {},
|
|
} as Calendars;
|
|
}
|
|
action.payload.events.forEach((event) => {
|
|
state[type][action.payload.calId].events[event.uid] = event;
|
|
});
|
|
Object.keys(state[type][action.payload.calId].events).forEach(
|
|
(id) => {
|
|
state[type][action.payload.calId].events[id].color =
|
|
state[type][action.payload.calId].color;
|
|
state[type][action.payload.calId].events[id].calId =
|
|
action.payload.calId;
|
|
if (!state[type][action.payload.calId].events[id].timezone) {
|
|
state[type][action.payload.calId].events[id].timezone =
|
|
Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
}
|
|
}
|
|
);
|
|
}
|
|
)
|
|
.addCase(
|
|
getEventAsync.fulfilled,
|
|
(
|
|
state,
|
|
action: PayloadAction<{ calId: string; event: CalendarEvent }>
|
|
) => {
|
|
state.pending = false;
|
|
if (!state.list[action.payload.calId]) {
|
|
state.list[action.payload.calId] = {
|
|
id: action.payload.calId,
|
|
events: {},
|
|
} as Calendars;
|
|
}
|
|
|
|
state.list[action.payload.calId].events[action.payload.event.uid] =
|
|
action.payload.event;
|
|
}
|
|
)
|
|
.addCase(
|
|
moveEventAsync.fulfilled,
|
|
(
|
|
state,
|
|
action: PayloadAction<{ calId: string; events: CalendarEvent[] }>
|
|
) => {
|
|
state.pending = false;
|
|
if (!state.list[action.payload.calId]) {
|
|
state.list[action.payload.calId] = {
|
|
id: action.payload.calId,
|
|
events: {},
|
|
} as Calendars;
|
|
}
|
|
action.payload.events.forEach((event) => {
|
|
state.list[action.payload.calId].events[event.uid] = event;
|
|
});
|
|
Object.keys(state.list[action.payload.calId].events).forEach((id) => {
|
|
state.list[action.payload.calId].events[id].color =
|
|
state.list[action.payload.calId].color;
|
|
state.list[action.payload.calId].events[id].calId =
|
|
action.payload.calId;
|
|
if (!state.list[action.payload.calId].events[id].timezone) {
|
|
state.list[action.payload.calId].events[id].timezone =
|
|
Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
}
|
|
});
|
|
}
|
|
)
|
|
.addCase(deleteEventAsync.fulfilled, (state, action) => {
|
|
state.pending = false;
|
|
const [baseId, recurrenceId] = action.payload.eventId.split("/");
|
|
if (recurrenceId) {
|
|
Object.keys(state.list[action.payload.calId].events).forEach(
|
|
(element) => {
|
|
if (element.split("/")[0] === baseId) {
|
|
delete state.list[action.payload.calId].events[element];
|
|
}
|
|
}
|
|
);
|
|
} else {
|
|
delete state.list[action.payload.calId].events[
|
|
action.payload.eventId
|
|
];
|
|
}
|
|
state.error = null;
|
|
})
|
|
.addCase(deleteEventInstanceAsync.fulfilled, (state, action) => {
|
|
state.pending = false;
|
|
delete state.list[action.payload.calId].events[action.payload.eventId];
|
|
state.error = null;
|
|
})
|
|
.addCase(updateEventInstanceAsync.fulfilled, (state, action) => {
|
|
state.pending = false;
|
|
state.list[action.payload.calId].events[action.payload.event.uid] =
|
|
action.payload.event;
|
|
state.error = null;
|
|
})
|
|
.addCase(updateSeriesAsync.fulfilled, (state) => {
|
|
state.pending = false;
|
|
state.error = null;
|
|
})
|
|
.addCase(createCalendarAsync.fulfilled, (state, action) => {
|
|
state.pending = false;
|
|
state.list[`${action.payload.userId}/${action.payload.calId}`] = {
|
|
color: action.payload.color,
|
|
id: `${action.payload.userId}/${action.payload.calId}`,
|
|
link: `/calendars/${action.payload.userId}/${action.payload.calId}.json`,
|
|
description: action.payload.desc,
|
|
name: action.payload.name,
|
|
owner: action.payload.owner,
|
|
ownerEmails: action.payload.ownerEmails,
|
|
events: {},
|
|
} as Calendars;
|
|
state.error = null;
|
|
})
|
|
.addCase(patchCalendarAsync.fulfilled, (state, action) => {
|
|
state.pending = false;
|
|
if (action.payload.patch.color) {
|
|
state.list[action.payload.calId] = {
|
|
...state.list[action.payload.calId],
|
|
color: action.payload.patch.color,
|
|
};
|
|
Object.keys(state.list[action.payload.calId].events).forEach(
|
|
(evId) =>
|
|
(state.list[action.payload.calId].events[evId].color =
|
|
action.payload.patch.color)
|
|
);
|
|
}
|
|
if (action.payload.patch.desc) {
|
|
state.list[action.payload.calId] = {
|
|
...state.list[action.payload.calId],
|
|
description: action.payload.patch.desc,
|
|
};
|
|
}
|
|
if (action.payload.patch.name) {
|
|
state.list[action.payload.calId] = {
|
|
...state.list[action.payload.calId],
|
|
name: action.payload.patch.name,
|
|
};
|
|
}
|
|
state.error = null;
|
|
})
|
|
.addCase(addSharedCalendarAsync.fulfilled, (state, action) => {
|
|
state.pending = false;
|
|
state.list[action.payload.calId] = {
|
|
color: action.payload.color,
|
|
id: action.payload.calId,
|
|
link: action.payload.link,
|
|
description: action.payload.desc,
|
|
name: action.payload.name,
|
|
events: {},
|
|
owner: action.payload.owner,
|
|
ownerEmails: action.payload.ownerEmails,
|
|
} as Calendars;
|
|
state.error = null;
|
|
})
|
|
.addCase(removeCalendarAsync.fulfilled, (state, action) => {
|
|
state.pending = false;
|
|
delete state.list[action.payload.calId];
|
|
state.error = null;
|
|
})
|
|
.addCase(patchACLCalendarAsync.fulfilled, (state, action) => {
|
|
state.pending = false;
|
|
state.list[action.payload.calId].visibility =
|
|
action.payload.request !== "" ? "public" : "private";
|
|
state.error = null;
|
|
})
|
|
.addCase(importEventFromFileAsync.fulfilled, (state) => {
|
|
state.pending = false;
|
|
state.error = null;
|
|
})
|
|
// Pending cases
|
|
.addCase(getCalendarDetailAsync.pending, (state) => {
|
|
state.pending = true;
|
|
})
|
|
.addCase(getEventAsync.pending, (state) => {
|
|
state.pending = true;
|
|
})
|
|
.addCase(getCalendarsListAsync.pending, (state) => {
|
|
state.pending = true;
|
|
})
|
|
.addCase(putEventAsync.pending, (state) => {
|
|
state.pending = true;
|
|
})
|
|
.addCase(moveEventAsync.pending, (state) => {
|
|
state.pending = true;
|
|
})
|
|
.addCase(deleteEventAsync.pending, (state) => {
|
|
state.pending = true;
|
|
})
|
|
.addCase(deleteEventInstanceAsync.pending, (state) => {
|
|
state.pending = true;
|
|
})
|
|
.addCase(updateEventInstanceAsync.pending, (state) => {
|
|
state.pending = true;
|
|
})
|
|
.addCase(updateSeriesAsync.pending, (state) => {
|
|
state.pending = true;
|
|
})
|
|
.addCase(patchCalendarAsync.pending, (state) => {
|
|
state.pending = true;
|
|
})
|
|
.addCase(createCalendarAsync.pending, (state) => {
|
|
state.pending = true;
|
|
})
|
|
.addCase(getTempCalendarsListAsync.pending, (state) => {
|
|
state.pending = true;
|
|
})
|
|
.addCase(addSharedCalendarAsync.pending, (state) => {
|
|
state.pending = true;
|
|
})
|
|
.addCase(removeCalendarAsync.pending, (state) => {
|
|
state.pending = true;
|
|
})
|
|
.addCase(patchACLCalendarAsync.pending, (state) => {
|
|
state.pending = true;
|
|
})
|
|
.addCase(importEventFromFileAsync.pending, (state) => {
|
|
state.pending = true;
|
|
})
|
|
// Rejected cases
|
|
.addCase(getCalendarsListAsync.rejected, (state, action) => {
|
|
if (action.payload?.status !== 401) {
|
|
state.pending = false;
|
|
state.error =
|
|
action.payload?.message ||
|
|
action.error.message ||
|
|
"Failed to load calendars";
|
|
}
|
|
})
|
|
.addCase(getTempCalendarsListAsync.rejected, (state, action) => {
|
|
state.pending = false;
|
|
if (
|
|
action.payload?.message.includes("aborted") ||
|
|
action.error.name === "AbortError"
|
|
) {
|
|
return;
|
|
}
|
|
state.error =
|
|
action.payload?.message ||
|
|
action.error.message ||
|
|
"Failed to load temporary calendars";
|
|
})
|
|
.addCase(getCalendarDetailAsync.rejected, (state, action) => {
|
|
state.pending = false;
|
|
if (
|
|
action.payload?.message.includes("aborted") ||
|
|
action.error.name === "AbortError"
|
|
) {
|
|
return;
|
|
}
|
|
state.error =
|
|
action.payload?.message ||
|
|
action.error.message ||
|
|
"Failed to load calendar details";
|
|
})
|
|
.addCase(putEventAsync.rejected, (state, action) => {
|
|
state.pending = false;
|
|
state.error =
|
|
action.payload?.message ||
|
|
action.error.message ||
|
|
"Failed to create event";
|
|
})
|
|
.addCase(getEventAsync.rejected, (state, action) => {
|
|
state.pending = false;
|
|
state.error =
|
|
action.payload?.message ||
|
|
action.error.message ||
|
|
"Failed to load event";
|
|
})
|
|
.addCase(moveEventAsync.rejected, (state, action) => {
|
|
state.pending = false;
|
|
state.error =
|
|
action.payload?.message ||
|
|
action.error.message ||
|
|
"Failed to move event";
|
|
})
|
|
.addCase(deleteEventAsync.rejected, (state, action) => {
|
|
state.pending = false;
|
|
state.error =
|
|
action.payload?.message ||
|
|
action.error.message ||
|
|
"Failed to delete event";
|
|
})
|
|
.addCase(deleteEventInstanceAsync.rejected, (state, action) => {
|
|
state.pending = false;
|
|
state.error =
|
|
action.payload?.message ||
|
|
action.error.message ||
|
|
"Failed to delete event instance";
|
|
})
|
|
.addCase(updateEventInstanceAsync.rejected, (state, action) => {
|
|
state.pending = false;
|
|
state.error =
|
|
action.payload?.message ||
|
|
action.error.message ||
|
|
"Failed to update event instance";
|
|
})
|
|
.addCase(updateSeriesAsync.rejected, (state, action) => {
|
|
state.pending = false;
|
|
state.error =
|
|
action.payload?.message ||
|
|
action.error.message ||
|
|
"Failed to update event series";
|
|
})
|
|
.addCase(patchCalendarAsync.rejected, (state, action) => {
|
|
state.pending = false;
|
|
state.error =
|
|
action.payload?.message ||
|
|
action.error.message ||
|
|
"Failed to update calendar";
|
|
})
|
|
.addCase(createCalendarAsync.rejected, (state, action) => {
|
|
state.pending = false;
|
|
state.error =
|
|
action.payload?.message ||
|
|
action.error.message ||
|
|
"Failed to create calendar";
|
|
})
|
|
.addCase(addSharedCalendarAsync.rejected, (state, action) => {
|
|
state.pending = false;
|
|
state.error =
|
|
action.payload?.message ||
|
|
action.error.message ||
|
|
"Failed to add shared calendar";
|
|
})
|
|
.addCase(removeCalendarAsync.rejected, (state, action) => {
|
|
state.pending = false;
|
|
state.error =
|
|
action.payload?.message ||
|
|
action.error.message ||
|
|
"Failed to remove calendar";
|
|
})
|
|
.addCase(patchACLCalendarAsync.rejected, (state, action) => {
|
|
state.pending = false;
|
|
state.error =
|
|
action.payload?.message ||
|
|
action.error.message ||
|
|
"Failed to update calendar permissions";
|
|
})
|
|
.addCase(importEventFromFileAsync.rejected, (state, action) => {
|
|
state.pending = false;
|
|
state.error =
|
|
action.payload?.message ||
|
|
action.error.message ||
|
|
"Failed to import event from file";
|
|
});
|
|
},
|
|
});
|
|
|
|
export const {
|
|
addEvent,
|
|
removeEvent,
|
|
createCalendar,
|
|
updateEventLocal,
|
|
removeTempCal,
|
|
emptyEventsCal,
|
|
setTimeZone,
|
|
clearFetchCache,
|
|
clearError,
|
|
updateCalColor,
|
|
} = CalendarSlice.actions;
|
|
export default CalendarSlice.reducer;
|