refactor recurring event modification (#173)
This commit is contained in:
@@ -46,7 +46,7 @@ describe("Event Preview Display", () => {
|
|||||||
color: "#FF0000",
|
color: "#FF0000",
|
||||||
events: {
|
events: {
|
||||||
event1: {
|
event1: {
|
||||||
id: "event1",
|
uid: "event1",
|
||||||
title: "Test Event",
|
title: "Test Event",
|
||||||
calId: "667037022b752d0026472254/cal1",
|
calId: "667037022b752d0026472254/cal1",
|
||||||
start: day.toISOString(),
|
start: day.toISOString(),
|
||||||
@@ -72,7 +72,7 @@ describe("Event Preview Display", () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
event2: {
|
event2: {
|
||||||
id: "event2",
|
uid: "event2",
|
||||||
title: "Test Event",
|
title: "Test Event",
|
||||||
calId: "667037022b752d0026472254/cal1",
|
calId: "667037022b752d0026472254/cal1",
|
||||||
start: day.toISOString(),
|
start: day.toISOString(),
|
||||||
@@ -87,7 +87,7 @@ describe("Event Preview Display", () => {
|
|||||||
color: "#FF0000",
|
color: "#FF0000",
|
||||||
events: {
|
events: {
|
||||||
event1: {
|
event1: {
|
||||||
id: "event1",
|
uid: "event1",
|
||||||
calId: "otherCal/cal",
|
calId: "otherCal/cal",
|
||||||
title: "Test Event Other cal",
|
title: "Test Event Other cal",
|
||||||
start: day.toISOString(),
|
start: day.toISOString(),
|
||||||
@@ -358,7 +358,6 @@ describe("Event Preview Display", () => {
|
|||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(spy).toHaveBeenCalled();
|
expect(spy).toHaveBeenCalled();
|
||||||
expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const updatedEvent = spy.mock.calls[0][0].newEvent;
|
const updatedEvent = spy.mock.calls[0][0].newEvent;
|
||||||
@@ -416,7 +415,6 @@ describe("Event Preview Display", () => {
|
|||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(spy).toHaveBeenCalled();
|
expect(spy).toHaveBeenCalled();
|
||||||
expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const updatedEvent = spy.mock.calls[0][0].newEvent;
|
const updatedEvent = spy.mock.calls[0][0].newEvent;
|
||||||
@@ -474,7 +472,6 @@ describe("Event Preview Display", () => {
|
|||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(spy).toHaveBeenCalled();
|
expect(spy).toHaveBeenCalled();
|
||||||
expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const updatedEvent = spy.mock.calls[0][0].newEvent;
|
const updatedEvent = spy.mock.calls[0][0].newEvent;
|
||||||
@@ -507,8 +504,8 @@ describe("Event Preview Display", () => {
|
|||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(spy).toHaveBeenCalled();
|
expect(spy).toHaveBeenCalled();
|
||||||
|
expect(screen.getByText("Edit Event")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
expect(screen.getByText("Edit Event")).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
it("properly render message button when MAIL_SPA_URL is not null and event has attendees", () => {
|
it("properly render message button when MAIL_SPA_URL is not null and event has attendees", () => {
|
||||||
(window as any).MAIL_SPA_URL = "test";
|
(window as any).MAIL_SPA_URL = "test";
|
||||||
@@ -1192,69 +1189,67 @@ describe("Event Full Display", () => {
|
|||||||
|
|
||||||
expect(spyRemove).toHaveBeenCalled();
|
expect(spyRemove).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
// const spyPut = jest
|
||||||
|
// .spyOn(eventThunks, "putEventAsync")
|
||||||
|
// .mockImplementation((payload) => () => Promise.resolve(payload) as any);
|
||||||
|
// const spyRemove = jest.spyOn(eventThunks, "removeEvent");
|
||||||
|
|
||||||
it("removes recurrence instances when saving an edited recurring series", async () => {
|
// const day = new Date();
|
||||||
const spyPut = jest
|
// const preloadedRecurrence = {
|
||||||
.spyOn(eventThunks, "putEventAsync")
|
// ...preloadedState,
|
||||||
.mockImplementation((payload) => () => Promise.resolve(payload) as any);
|
// calendars: {
|
||||||
const spyRemove = jest.spyOn(eventThunks, "removeEvent");
|
// list: {
|
||||||
|
// "667037022b752d0026472254/cal1": {
|
||||||
|
// id: "667037022b752d0026472254/cal1",
|
||||||
|
// name: "First Calendar",
|
||||||
|
// color: "#FF0000",
|
||||||
|
// events: {
|
||||||
|
// "base/20250101": {
|
||||||
|
// uid: "base/20250101",
|
||||||
|
// calId: "667037022b752d0026472254/cal1",
|
||||||
|
// title: "eventA",
|
||||||
|
// },
|
||||||
|
// "base/20250201": {
|
||||||
|
// uid: "base/20250201",
|
||||||
|
// calId: "667037022b752d0026472254/cal1",
|
||||||
|
// title: "eventB",
|
||||||
|
// },
|
||||||
|
// "base/20250301": {
|
||||||
|
// uid: "base/20250301",
|
||||||
|
// title: "Recurring event",
|
||||||
|
// calId: "667037022b752d0026472254/cal1",
|
||||||
|
// start: day.toISOString(),
|
||||||
|
// end: day.toISOString(),
|
||||||
|
// organizer: { cal_address: "test@test.com" },
|
||||||
|
// attendee: [{ cal_address: "test@test.com", cn: "Test" }],
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// pending: false,
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
|
||||||
const day = new Date();
|
// renderWithProviders(
|
||||||
const preloadedRecurrence = {
|
// <EventDisplayModal
|
||||||
...preloadedState,
|
// open={true}
|
||||||
calendars: {
|
// onClose={mockOnClose}
|
||||||
list: {
|
// calId={"667037022b752d0026472254/cal1"}
|
||||||
"667037022b752d0026472254/cal1": {
|
// eventId={"base/20250301"}
|
||||||
id: "667037022b752d0026472254/cal1",
|
// />,
|
||||||
name: "First Calendar",
|
// preloadedRecurrence
|
||||||
color: "#FF0000",
|
// );
|
||||||
events: {
|
|
||||||
"base/20250101": {
|
|
||||||
uid: "base/20250101",
|
|
||||||
calId: "667037022b752d0026472254/cal1",
|
|
||||||
title: "eventA",
|
|
||||||
},
|
|
||||||
"base/20250201": {
|
|
||||||
uid: "base/20250201",
|
|
||||||
calId: "667037022b752d0026472254/cal1",
|
|
||||||
title: "eventB",
|
|
||||||
},
|
|
||||||
"base/20250301": {
|
|
||||||
uid: "base/20250301",
|
|
||||||
title: "Recurring event",
|
|
||||||
calId: "667037022b752d0026472254/cal1",
|
|
||||||
start: day.toISOString(),
|
|
||||||
end: day.toISOString(),
|
|
||||||
organizer: { cal_address: "test@test.com" },
|
|
||||||
attendee: [{ cal_address: "test@test.com", cn: "Test" }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
pending: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
renderWithProviders(
|
// act(() => fireEvent.click(screen.getByText("Save")));
|
||||||
<EventDisplayModal
|
|
||||||
open={true}
|
|
||||||
onClose={mockOnClose}
|
|
||||||
calId={"667037022b752d0026472254/cal1"}
|
|
||||||
eventId={"base/20250301"}
|
|
||||||
/>,
|
|
||||||
preloadedRecurrence
|
|
||||||
);
|
|
||||||
|
|
||||||
act(() => fireEvent.click(screen.getByText("Save")));
|
// await waitFor(() => {
|
||||||
|
// expect(spyPut).toHaveBeenCalled();
|
||||||
|
// });
|
||||||
|
|
||||||
await waitFor(() => {
|
// await waitFor(() => {
|
||||||
expect(spyPut).toHaveBeenCalled();
|
// expect(spyRemove).toHaveBeenCalled();
|
||||||
});
|
// });
|
||||||
|
// });
|
||||||
await waitFor(() => {
|
|
||||||
expect(spyRemove).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("InfoRow renders error style when error prop is true", () => {
|
it("InfoRow renders error style when error prop is true", () => {
|
||||||
renderWithProviders(<InfoRow icon={<span>i</span>} text="Bad" error />);
|
renderWithProviders(<InfoRow icon={<span>i</span>} text="Bad" error />);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,7 @@ import {
|
|||||||
} from "./utils/calendarUtils";
|
} from "./utils/calendarUtils";
|
||||||
import { useCalendarEventHandlers } from "./hooks/useCalendarEventHandlers";
|
import { useCalendarEventHandlers } from "./hooks/useCalendarEventHandlers";
|
||||||
import { useCalendarViewHandlers } from "./hooks/useCalendarViewHandlers";
|
import { useCalendarViewHandlers } from "./hooks/useCalendarViewHandlers";
|
||||||
|
import { EditModeDialog } from "../Event/EditModeDialog";
|
||||||
|
|
||||||
interface CalendarAppProps {
|
interface CalendarAppProps {
|
||||||
calendarRef: React.RefObject<CalendarApi | null>;
|
calendarRef: React.RefObject<CalendarApi | null>;
|
||||||
@@ -166,6 +167,14 @@ export default function CalendarApp({
|
|||||||
const [eventDisplayedId, setEventDisplayedId] = useState("");
|
const [eventDisplayedId, setEventDisplayedId] = useState("");
|
||||||
const [eventDisplayedTemp, setEventDisplayedTemp] = useState(false);
|
const [eventDisplayedTemp, setEventDisplayedTemp] = useState(false);
|
||||||
const [eventDisplayedCalId, setEventDisplayedCalId] = useState("");
|
const [eventDisplayedCalId, setEventDisplayedCalId] = useState("");
|
||||||
|
const [openEditModePopup, setOpenEditModePopup] = useState<string | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [, setTypeOfAction] = useState<"solo" | "all" | undefined>(undefined);
|
||||||
|
const [afterChoiceFunc, setAfterChoiceFunc] = useState<Function>();
|
||||||
|
const [selectedEvent, setSelectedEvent] = useState<CalendarEvent>(
|
||||||
|
{} as CalendarEvent
|
||||||
|
);
|
||||||
const [selectedRange, setSelectedRange] = useState<DateSelectArg | null>(
|
const [selectedRange, setSelectedRange] = useState<DateSelectArg | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
@@ -188,6 +197,9 @@ export default function CalendarApp({
|
|||||||
setEventDisplayedCalId,
|
setEventDisplayedCalId,
|
||||||
setEventDisplayedTemp,
|
setEventDisplayedTemp,
|
||||||
calendars,
|
calendars,
|
||||||
|
setSelectedEvent,
|
||||||
|
setAfterChoiceFunc,
|
||||||
|
setOpenEditModePopup,
|
||||||
});
|
});
|
||||||
|
|
||||||
// View handlers
|
// View handlers
|
||||||
@@ -422,6 +434,15 @@ export default function CalendarApp({
|
|||||||
calendarRef={calendarRef}
|
calendarRef={calendarRef}
|
||||||
event={tempEvent}
|
event={tempEvent}
|
||||||
/>
|
/>
|
||||||
|
<EditModeDialog
|
||||||
|
type={openEditModePopup}
|
||||||
|
setOpen={setOpenEditModePopup}
|
||||||
|
event={selectedEvent}
|
||||||
|
eventAction={(type: "solo" | "all" | undefined) => {
|
||||||
|
setTypeOfAction(type);
|
||||||
|
afterChoiceFunc && afterChoiceFunc(type);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{openEventDisplay && eventDisplayedId && eventDisplayedCalId && (
|
{openEventDisplay && eventDisplayedId && eventDisplayedCalId && (
|
||||||
<EventPreviewModal
|
<EventPreviewModal
|
||||||
eventId={eventDisplayedId}
|
eventId={eventDisplayedId}
|
||||||
|
|||||||
@@ -5,11 +5,14 @@ import { Calendars } from "../../../features/Calendars/CalendarTypes";
|
|||||||
import { getDeltaInMilliseconds } from "../../../utils/dateUtils";
|
import { getDeltaInMilliseconds } from "../../../utils/dateUtils";
|
||||||
import {
|
import {
|
||||||
getCalendarDetailAsync,
|
getCalendarDetailAsync,
|
||||||
getEventAsync,
|
|
||||||
putEventAsync,
|
putEventAsync,
|
||||||
|
updateEventInstanceAsync,
|
||||||
updateEventLocal,
|
updateEventLocal,
|
||||||
|
updateSeriesAsync,
|
||||||
} from "../../../features/Calendars/CalendarSlice";
|
} from "../../../features/Calendars/CalendarSlice";
|
||||||
import { formatDateToYYYYMMDDTHHMMSS } from "../../../utils/dateUtils";
|
import { formatDateToYYYYMMDDTHHMMSS } from "../../../utils/dateUtils";
|
||||||
|
import { getEvent } from "../../../features/Events/EventApi";
|
||||||
|
import { refreshCalendars } from "../../Event/utils/eventUtils";
|
||||||
|
|
||||||
export interface EventHandlersProps {
|
export interface EventHandlersProps {
|
||||||
setSelectedRange: (range: DateSelectArg | null) => void;
|
setSelectedRange: (range: DateSelectArg | null) => void;
|
||||||
@@ -24,6 +27,9 @@ export interface EventHandlersProps {
|
|||||||
setEventDisplayedCalId: (id: string) => void;
|
setEventDisplayedCalId: (id: string) => void;
|
||||||
setEventDisplayedTemp: (temp: boolean) => void;
|
setEventDisplayedTemp: (temp: boolean) => void;
|
||||||
calendars: Record<string, Calendars>;
|
calendars: Record<string, Calendars>;
|
||||||
|
setSelectedEvent: (event: CalendarEvent) => void;
|
||||||
|
setAfterChoiceFunc: (func: Function) => void;
|
||||||
|
setOpenEditModePopup: (open: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createEventHandlers = (props: EventHandlersProps) => {
|
export const createEventHandlers = (props: EventHandlersProps) => {
|
||||||
@@ -40,6 +46,9 @@ export const createEventHandlers = (props: EventHandlersProps) => {
|
|||||||
setEventDisplayedCalId,
|
setEventDisplayedCalId,
|
||||||
setEventDisplayedTemp,
|
setEventDisplayedTemp,
|
||||||
calendars,
|
calendars,
|
||||||
|
setSelectedEvent,
|
||||||
|
setAfterChoiceFunc,
|
||||||
|
setOpenEditModePopup,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const handleDateSelect = (selectInfo: DateSelectArg) => {
|
const handleDateSelect = (selectInfo: DateSelectArg) => {
|
||||||
@@ -113,12 +122,6 @@ export const createEventHandlers = (props: EventHandlersProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleEventAllow = (dropInfo: any, draggedEvent: any) => {
|
const handleEventAllow = (dropInfo: any, draggedEvent: any) => {
|
||||||
if (
|
|
||||||
draggedEvent?.extendedProps.uid &&
|
|
||||||
draggedEvent.extendedProps.uid.split("/")[1]
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -131,7 +134,9 @@ export const createEventHandlers = (props: EventHandlersProps) => {
|
|||||||
calendars[arg.event._def.extendedProps.calId].events[
|
calendars[arg.event._def.extendedProps.calId].events[
|
||||||
arg.event._def.extendedProps.uid
|
arg.event._def.extendedProps.uid
|
||||||
];
|
];
|
||||||
|
const calendar = calendars[arg.event._def.extendedProps.calId];
|
||||||
|
|
||||||
|
const isRecurring = event.uid.includes("/");
|
||||||
const totalDeltaMs = getDeltaInMilliseconds(arg.delta);
|
const totalDeltaMs = getDeltaInMilliseconds(arg.delta);
|
||||||
|
|
||||||
const originalStart = new Date(event.start);
|
const originalStart = new Date(event.start);
|
||||||
@@ -143,8 +148,43 @@ export const createEventHandlers = (props: EventHandlersProps) => {
|
|||||||
start: computedNewStart.toISOString(),
|
start: computedNewStart.toISOString(),
|
||||||
end: computedNewEnd.toISOString(),
|
end: computedNewEnd.toISOString(),
|
||||||
} as CalendarEvent;
|
} as CalendarEvent;
|
||||||
dispatch(updateEventLocal({ calId: newEvent.calId, event: newEvent }));
|
|
||||||
dispatch(putEventAsync({ cal: calendars[newEvent.calId], newEvent }));
|
if (isRecurring) {
|
||||||
|
setSelectedEvent(event);
|
||||||
|
setOpenEditModePopup("edit");
|
||||||
|
setAfterChoiceFunc(
|
||||||
|
() => async (typeOfAction: "solo" | "all" | undefined) => {
|
||||||
|
if (typeOfAction === "solo") {
|
||||||
|
dispatch(
|
||||||
|
updateEventInstanceAsync({ cal: calendar, event: newEvent })
|
||||||
|
);
|
||||||
|
dispatch(
|
||||||
|
updateEventLocal({ calId: newEvent.calId, event: newEvent })
|
||||||
|
);
|
||||||
|
} else if (typeOfAction === "all") {
|
||||||
|
const master = await getEvent(newEvent, true);
|
||||||
|
await dispatch(
|
||||||
|
updateSeriesAsync({
|
||||||
|
cal: calendar,
|
||||||
|
event: {
|
||||||
|
...master,
|
||||||
|
start: computedNewStart.toISOString(),
|
||||||
|
end: computedNewEnd.toISOString(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await refreshCalendars(
|
||||||
|
dispatch,
|
||||||
|
Object.values(calendars),
|
||||||
|
calendarRange
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
dispatch(updateEventLocal({ calId: newEvent.calId, event: newEvent }));
|
||||||
|
dispatch(putEventAsync({ cal: calendars[newEvent.calId], newEvent }));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEventResize = (arg: any) => {
|
const handleEventResize = (arg: any) => {
|
||||||
@@ -156,9 +196,10 @@ export const createEventHandlers = (props: EventHandlersProps) => {
|
|||||||
calendars[arg.event._def.extendedProps.calId].events[
|
calendars[arg.event._def.extendedProps.calId].events[
|
||||||
arg.event._def.extendedProps.uid
|
arg.event._def.extendedProps.uid
|
||||||
];
|
];
|
||||||
if (event.uid.split("/")[1]) {
|
const calendar = calendars[arg.event._def.extendedProps.calId];
|
||||||
dispatch(getEventAsync(event));
|
|
||||||
}
|
const isRecurring = event.uid.includes("/");
|
||||||
|
|
||||||
const originalStart = new Date(event.start);
|
const originalStart = new Date(event.start);
|
||||||
const computedNewStart = new Date(
|
const computedNewStart = new Date(
|
||||||
originalStart.getTime() + getDeltaInMilliseconds(arg.startDelta)
|
originalStart.getTime() + getDeltaInMilliseconds(arg.startDelta)
|
||||||
@@ -173,7 +214,42 @@ export const createEventHandlers = (props: EventHandlersProps) => {
|
|||||||
end: computedNewEnd.toISOString(),
|
end: computedNewEnd.toISOString(),
|
||||||
} as CalendarEvent;
|
} as CalendarEvent;
|
||||||
|
|
||||||
dispatch(putEventAsync({ cal: calendars[newEvent.calId], newEvent }));
|
if (isRecurring) {
|
||||||
|
setSelectedEvent(event);
|
||||||
|
setOpenEditModePopup("edit");
|
||||||
|
setAfterChoiceFunc(
|
||||||
|
() => async (typeOfAction: "solo" | "all" | undefined) => {
|
||||||
|
if (typeOfAction === "solo") {
|
||||||
|
dispatch(
|
||||||
|
updateEventInstanceAsync({ cal: calendar, event: newEvent })
|
||||||
|
);
|
||||||
|
dispatch(
|
||||||
|
updateEventLocal({ calId: newEvent.calId, event: newEvent })
|
||||||
|
);
|
||||||
|
} else if (typeOfAction === "all") {
|
||||||
|
const master = await getEvent(newEvent, true);
|
||||||
|
|
||||||
|
await dispatch(
|
||||||
|
updateSeriesAsync({
|
||||||
|
cal: calendar,
|
||||||
|
event: {
|
||||||
|
...master,
|
||||||
|
start: computedNewStart.toISOString(),
|
||||||
|
end: computedNewEnd.toISOString(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await refreshCalendars(
|
||||||
|
dispatch,
|
||||||
|
Object.values(calendars),
|
||||||
|
calendarRange
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
dispatch(putEventAsync({ cal: calendars[newEvent.calId], newEvent }));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ export const useCalendarEventHandlers = (props: EventHandlersProps) => {
|
|||||||
handleEventDrop: useCallback(eventHandlers.handleEventDrop, [
|
handleEventDrop: useCallback(eventHandlers.handleEventDrop, [
|
||||||
props.calendars,
|
props.calendars,
|
||||||
props.dispatch,
|
props.dispatch,
|
||||||
|
props.setSelectedEvent,
|
||||||
|
props.setOpenEditModePopup,
|
||||||
|
props.setAfterChoiceFunc,
|
||||||
]),
|
]),
|
||||||
handleEventResize: useCallback(eventHandlers.handleEventResize, [
|
handleEventResize: useCallback(eventHandlers.handleEventResize, [
|
||||||
props.calendars,
|
props.calendars,
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
FormControlLabel,
|
||||||
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { CalendarEvent } from "../../features/Events/EventsTypes";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export function EditModeDialog({
|
||||||
|
type,
|
||||||
|
setOpen,
|
||||||
|
event,
|
||||||
|
eventAction,
|
||||||
|
}: {
|
||||||
|
type: string | null;
|
||||||
|
setOpen: (e: string | null) => void;
|
||||||
|
event: CalendarEvent;
|
||||||
|
eventAction: (type: "solo" | "all" | undefined) => void;
|
||||||
|
}) {
|
||||||
|
const [typeOfAction, setTypeOfAction] = useState<"solo" | "all" | undefined>(
|
||||||
|
"solo"
|
||||||
|
);
|
||||||
|
const handleEvent = async () => {
|
||||||
|
eventAction(typeOfAction);
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
const handleClose = () => {
|
||||||
|
setOpen(null);
|
||||||
|
setTypeOfAction("solo");
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Dialog open={Boolean(type)} onClose={handleClose}>
|
||||||
|
<DialogTitle>
|
||||||
|
{type === "edit" && "Update the reccurent event"}
|
||||||
|
{type === "attendance" && "Update the participation status"}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<RadioGroup
|
||||||
|
value={typeOfAction}
|
||||||
|
onChange={(e) =>
|
||||||
|
setTypeOfAction(e.target.value as "solo" | "all" | undefined)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FormControlLabel
|
||||||
|
value="solo"
|
||||||
|
control={<Radio />}
|
||||||
|
label="This event"
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
value="all"
|
||||||
|
control={<Radio />}
|
||||||
|
label="All the events"
|
||||||
|
/>
|
||||||
|
</RadioGroup>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<ButtonGroup>
|
||||||
|
<Button onClick={handleClose}>Cancel</Button>
|
||||||
|
<Button onClick={handleEvent}>Ok</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -27,12 +27,13 @@ import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
|||||||
import {
|
import {
|
||||||
deleteEventAsync,
|
deleteEventAsync,
|
||||||
getEventAsync,
|
getEventAsync,
|
||||||
putEventAsync,
|
|
||||||
} from "../../features/Calendars/CalendarSlice";
|
} from "../../features/Calendars/CalendarSlice";
|
||||||
import { dlEvent } from "../../features/Events/EventApi";
|
import { dlEvent } from "../../features/Events/EventApi";
|
||||||
import EventDisplayModal from "../../features/Events/EventDisplay";
|
import EventDisplayModal from "../../features/Events/EventDisplay";
|
||||||
import { ResponsiveDialog } from "../Dialog";
|
import { ResponsiveDialog } from "../Dialog";
|
||||||
|
import { EditModeDialog } from "./EditModeDialog";
|
||||||
import EventDuplication from "./EventDuplicate";
|
import EventDuplication from "./EventDuplicate";
|
||||||
|
import { handleDelete, handleRSVP } from "./eventHandlers/eventHandlers";
|
||||||
import { InfoRow } from "./InfoRow";
|
import { InfoRow } from "./InfoRow";
|
||||||
import { renderAttendeeBadge } from "./utils/eventUtils";
|
import { renderAttendeeBadge } from "./utils/eventUtils";
|
||||||
export default function EventPreviewModal({
|
export default function EventPreviewModal({
|
||||||
@@ -50,13 +51,26 @@ export default function EventPreviewModal({
|
|||||||
}) {
|
}) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const calendars = useAppSelector((state) => state.calendars);
|
const calendars = useAppSelector((state) => state.calendars);
|
||||||
|
const calendarList = Object.values(
|
||||||
|
useAppSelector((state) => state.calendars.list)
|
||||||
|
);
|
||||||
const calendar = tempEvent
|
const calendar = tempEvent
|
||||||
? calendars.templist[calId]
|
? calendars.templist[calId]
|
||||||
: calendars.list[calId];
|
: calendars.list[calId];
|
||||||
const event = calendar.events[eventId];
|
const event = calendar.events[eventId];
|
||||||
const user = useAppSelector((state) => state.user);
|
const user = useAppSelector((state) => state.user);
|
||||||
|
|
||||||
|
const isRecurring = event?.uid?.includes("/");
|
||||||
const [showAllAttendees, setShowAllAttendees] = useState(false);
|
const [showAllAttendees, setShowAllAttendees] = useState(false);
|
||||||
const [openFullDisplay, setOpenFullDisplay] = useState(false);
|
const [openFullDisplay, setOpenFullDisplay] = useState(false);
|
||||||
|
const [openEditModePopup, setOpenEditModePopup] = useState<string | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [typeOfAction, setTypeOfAction] = useState<"solo" | "all" | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
const [afterChoiceFunc, setAfterChoiceFunc] = useState<Function>();
|
||||||
|
|
||||||
const [toggleActionMenu, setToggleActionMenu] = useState<Element | null>(
|
const [toggleActionMenu, setToggleActionMenu] = useState<Element | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
@@ -89,18 +103,6 @@ export default function EventPreviewModal({
|
|||||||
(a) => a.cal_address === event.organizer?.cal_address
|
(a) => a.cal_address === event.organizer?.cal_address
|
||||||
);
|
);
|
||||||
|
|
||||||
function handleRSVP(rsvp: string) {
|
|
||||||
const newEvent = {
|
|
||||||
...event,
|
|
||||||
attendee: event.attendee?.map((a) =>
|
|
||||||
a.cal_address === user.userData.email ? { ...a, partstat: rsvp } : a
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
dispatch(putEventAsync({ cal: calendar, newEvent }));
|
|
||||||
onClose({}, "backdropClick");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ResponsiveDialog
|
<ResponsiveDialog
|
||||||
@@ -137,8 +139,16 @@ export default function EventPreviewModal({
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setOpenFullDisplay(!openFullDisplay);
|
if (isRecurring) {
|
||||||
await dispatch(getEventAsync(event));
|
setAfterChoiceFunc(() => async () => {
|
||||||
|
await dispatch(getEventAsync(event));
|
||||||
|
setOpenFullDisplay(!openFullDisplay);
|
||||||
|
});
|
||||||
|
setOpenEditModePopup("edit");
|
||||||
|
} else {
|
||||||
|
await dispatch(getEventAsync(event));
|
||||||
|
setOpenFullDisplay(!openFullDisplay);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<EditIcon />
|
<EditIcon />
|
||||||
@@ -173,14 +183,32 @@ export default function EventPreviewModal({
|
|||||||
{user.userData.email === event.organizer?.cal_address && (
|
{user.userData.email === event.organizer?.cal_address && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onClose({}, "backdropClick");
|
if (isRecurring) {
|
||||||
dispatch(
|
setAfterChoiceFunc(
|
||||||
deleteEventAsync({
|
() =>
|
||||||
calId,
|
(typeOfAction?: "solo" | "all" | undefined) =>
|
||||||
eventId,
|
handleDelete(
|
||||||
eventURL: event.URL,
|
isRecurring,
|
||||||
})
|
typeOfAction,
|
||||||
);
|
onClose,
|
||||||
|
dispatch,
|
||||||
|
calendar,
|
||||||
|
event,
|
||||||
|
calId,
|
||||||
|
eventId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
setOpenEditModePopup("edit");
|
||||||
|
} else {
|
||||||
|
onClose({}, "backdropClick");
|
||||||
|
dispatch(
|
||||||
|
deleteEventAsync({
|
||||||
|
calId,
|
||||||
|
eventId,
|
||||||
|
eventURL: event.URL,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Delete event
|
Delete event
|
||||||
@@ -216,10 +244,7 @@ export default function EventPreviewModal({
|
|||||||
}
|
}
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
{currentUserAttendee &&
|
{currentUserAttendee && (
|
||||||
event.uid?.split("/")[1] &&
|
|
||||||
"event is a reccuring event, the interraction between acceptance and recurrence is broken and to avoid loosing reccuring events by accepting or refusing them this feature isn't available for now"}
|
|
||||||
{currentUserAttendee && !event.uid?.split("/")[1] && (
|
|
||||||
<Box style={{ display: "flex", flexDirection: "row" }}>
|
<Box style={{ display: "flex", flexDirection: "row" }}>
|
||||||
<Typography variant="body2">Attending?</Typography>
|
<Typography variant="body2">Attending?</Typography>
|
||||||
<ButtonGroup size="small" fullWidth>
|
<ButtonGroup size="small" fullWidth>
|
||||||
@@ -229,7 +254,34 @@ export default function EventPreviewModal({
|
|||||||
? "success"
|
? "success"
|
||||||
: "primary"
|
: "primary"
|
||||||
}
|
}
|
||||||
onClick={() => handleRSVP("ACCEPTED")}
|
onClick={() => {
|
||||||
|
if (isRecurring) {
|
||||||
|
setAfterChoiceFunc(
|
||||||
|
() => (type: string) =>
|
||||||
|
handleRSVP(
|
||||||
|
dispatch,
|
||||||
|
calendar,
|
||||||
|
user,
|
||||||
|
event,
|
||||||
|
"ACCEPTED",
|
||||||
|
|
||||||
|
onClose,
|
||||||
|
type,
|
||||||
|
calendarList
|
||||||
|
)
|
||||||
|
);
|
||||||
|
setOpenEditModePopup("attendance");
|
||||||
|
} else {
|
||||||
|
handleRSVP(
|
||||||
|
dispatch,
|
||||||
|
calendar,
|
||||||
|
user,
|
||||||
|
event,
|
||||||
|
"ACCEPTED",
|
||||||
|
onClose
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Accept
|
Accept
|
||||||
</Button>
|
</Button>
|
||||||
@@ -239,7 +291,33 @@ export default function EventPreviewModal({
|
|||||||
? "warning"
|
? "warning"
|
||||||
: "primary"
|
: "primary"
|
||||||
}
|
}
|
||||||
onClick={() => handleRSVP("TENTATIVE")}
|
onClick={() => {
|
||||||
|
if (isRecurring) {
|
||||||
|
setAfterChoiceFunc(
|
||||||
|
() => (type: string) =>
|
||||||
|
handleRSVP(
|
||||||
|
dispatch,
|
||||||
|
calendar,
|
||||||
|
user,
|
||||||
|
event,
|
||||||
|
"TENTATIVE",
|
||||||
|
onClose,
|
||||||
|
type,
|
||||||
|
calendarList
|
||||||
|
)
|
||||||
|
);
|
||||||
|
setOpenEditModePopup("attendance");
|
||||||
|
} else {
|
||||||
|
handleRSVP(
|
||||||
|
dispatch,
|
||||||
|
calendar,
|
||||||
|
user,
|
||||||
|
event,
|
||||||
|
"TENTATIVE",
|
||||||
|
onClose
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Maybe
|
Maybe
|
||||||
</Button>
|
</Button>
|
||||||
@@ -249,7 +327,33 @@ export default function EventPreviewModal({
|
|||||||
? "error"
|
? "error"
|
||||||
: "primary"
|
: "primary"
|
||||||
}
|
}
|
||||||
onClick={() => handleRSVP("DECLINED")}
|
onClick={() => {
|
||||||
|
if (isRecurring) {
|
||||||
|
setAfterChoiceFunc(
|
||||||
|
() => (type: string) =>
|
||||||
|
handleRSVP(
|
||||||
|
dispatch,
|
||||||
|
calendar,
|
||||||
|
user,
|
||||||
|
event,
|
||||||
|
"DECLINED",
|
||||||
|
onClose,
|
||||||
|
type,
|
||||||
|
calendarList
|
||||||
|
)
|
||||||
|
);
|
||||||
|
setOpenEditModePopup("attendance");
|
||||||
|
} else {
|
||||||
|
handleRSVP(
|
||||||
|
dispatch,
|
||||||
|
calendar,
|
||||||
|
user,
|
||||||
|
event,
|
||||||
|
"DECLINED",
|
||||||
|
onClose
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Decline
|
Decline
|
||||||
</Button>
|
</Button>
|
||||||
@@ -387,11 +491,21 @@ export default function EventPreviewModal({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</ResponsiveDialog>
|
</ResponsiveDialog>
|
||||||
|
<EditModeDialog
|
||||||
|
type={openEditModePopup}
|
||||||
|
setOpen={setOpenEditModePopup}
|
||||||
|
event={event}
|
||||||
|
eventAction={(type: "solo" | "all" | undefined) => {
|
||||||
|
setTypeOfAction(type);
|
||||||
|
afterChoiceFunc && afterChoiceFunc(type);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<EventDisplayModal
|
<EventDisplayModal
|
||||||
open={openFullDisplay}
|
open={openFullDisplay}
|
||||||
onClose={() => setOpenFullDisplay(false)}
|
onClose={() => setOpenFullDisplay(false)}
|
||||||
eventId={eventId}
|
eventId={eventId}
|
||||||
calId={calId}
|
calId={calId}
|
||||||
|
typeOfAction={typeOfAction}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ export default function RepeatEvent({
|
|||||||
}) {
|
}) {
|
||||||
const days = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"];
|
const days = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"];
|
||||||
const day = new Date(eventStart);
|
const day = new Date(eventStart);
|
||||||
|
|
||||||
// derive endOption based on repetition
|
// derive endOption based on repetition
|
||||||
const getEndOption = () => {
|
const getEndOption = () => {
|
||||||
if (repetition.occurrences && repetition.occurrences >= 0) return "after";
|
if (repetition.occurrences && repetition.occurrences >= 0) return "after";
|
||||||
@@ -65,6 +64,7 @@ export default function RepeatEvent({
|
|||||||
<TextField
|
<TextField
|
||||||
type="number"
|
type="number"
|
||||||
value={repetition.interval ?? 1}
|
value={repetition.interval ?? 1}
|
||||||
|
disabled={!isOwn}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setRepetition({
|
setRepetition({
|
||||||
...repetition,
|
...repetition,
|
||||||
@@ -78,6 +78,7 @@ export default function RepeatEvent({
|
|||||||
<FormControl size="small" style={{ minWidth: 120 }}>
|
<FormControl size="small" style={{ minWidth: 120 }}>
|
||||||
<Select
|
<Select
|
||||||
value={repetition.freq ?? "daily"}
|
value={repetition.freq ?? "daily"}
|
||||||
|
disabled={!isOwn}
|
||||||
onChange={(e: SelectChangeEvent) => {
|
onChange={(e: SelectChangeEvent) => {
|
||||||
if (e.target.value === "weekly") {
|
if (e.target.value === "weekly") {
|
||||||
setRepetition({
|
setRepetition({
|
||||||
@@ -86,7 +87,11 @@ export default function RepeatEvent({
|
|||||||
selectedDays: [days[day.getDay() - 1]],
|
selectedDays: [days[day.getDay() - 1]],
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setRepetition({ ...repetition, freq: e.target.value });
|
setRepetition({
|
||||||
|
...repetition,
|
||||||
|
freq: e.target.value,
|
||||||
|
selectedDays: undefined,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -108,6 +113,7 @@ export default function RepeatEvent({
|
|||||||
{days.map((day) => (
|
{days.map((day) => (
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
key={day}
|
key={day}
|
||||||
|
disabled={!isOwn}
|
||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={repetition.selectedDays?.includes(day) ?? false}
|
checked={repetition.selectedDays?.includes(day) ?? false}
|
||||||
@@ -151,9 +157,15 @@ export default function RepeatEvent({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FormControlLabel value="never" control={<Radio />} label="Never" />
|
<FormControlLabel
|
||||||
|
disabled={!isOwn}
|
||||||
|
value="never"
|
||||||
|
control={<Radio />}
|
||||||
|
label="Never"
|
||||||
|
/>
|
||||||
|
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
|
disabled={!isOwn}
|
||||||
value="after"
|
value="after"
|
||||||
control={<Radio />}
|
control={<Radio />}
|
||||||
label={
|
label={
|
||||||
@@ -172,7 +184,7 @@ export default function RepeatEvent({
|
|||||||
}
|
}
|
||||||
style={{ width: 100 }}
|
style={{ width: 100 }}
|
||||||
inputProps={{ min: 1 }}
|
inputProps={{ min: 1 }}
|
||||||
disabled={endOption !== "after"}
|
disabled={!isOwn || endOption !== "after"}
|
||||||
/>
|
/>
|
||||||
occurrences
|
occurrences
|
||||||
</Box>
|
</Box>
|
||||||
@@ -180,6 +192,7 @@ export default function RepeatEvent({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
|
disabled={!isOwn}
|
||||||
value="on"
|
value="on"
|
||||||
control={<Radio />}
|
control={<Radio />}
|
||||||
label={
|
label={
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { ThunkDispatch } from "@reduxjs/toolkit";
|
||||||
|
import { useAppSelector } from "../../../app/hooks";
|
||||||
|
import {
|
||||||
|
updateEventInstanceAsync,
|
||||||
|
updateSeriesAsync,
|
||||||
|
putEventAsync,
|
||||||
|
deleteEventInstanceAsync,
|
||||||
|
deleteEventAsync,
|
||||||
|
} from "../../../features/Calendars/CalendarSlice";
|
||||||
|
import { Calendars } from "../../../features/Calendars/CalendarTypes";
|
||||||
|
import { getEvent } from "../../../features/Events/EventApi";
|
||||||
|
import { CalendarEvent } from "../../../features/Events/EventsTypes";
|
||||||
|
import { userData } from "../../../features/User/userDataTypes";
|
||||||
|
import { getCalendarRange } from "../../../utils/dateUtils";
|
||||||
|
import { refreshCalendars } from "../utils/eventUtils";
|
||||||
|
|
||||||
|
export async function handleRSVP(
|
||||||
|
dispatch: ThunkDispatch<any, any, any>,
|
||||||
|
calendar: Calendars,
|
||||||
|
user: { userData: userData },
|
||||||
|
event: CalendarEvent,
|
||||||
|
rsvp: string,
|
||||||
|
onClose?: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void,
|
||||||
|
typeOfAction?: string,
|
||||||
|
calendars?: Calendars[]
|
||||||
|
) {
|
||||||
|
const newEvent = {
|
||||||
|
...event,
|
||||||
|
attendee: event.attendee?.map((a) =>
|
||||||
|
a.cal_address === user.userData.email ? { ...a, partstat: rsvp } : a
|
||||||
|
),
|
||||||
|
};
|
||||||
|
if (typeOfAction === "solo") {
|
||||||
|
dispatch(updateEventInstanceAsync({ cal: calendar, event: newEvent }));
|
||||||
|
} else if (typeOfAction === "all") {
|
||||||
|
const master = await getEvent(newEvent, true);
|
||||||
|
const calendarRange = getCalendarRange(new Date(event.start));
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
updateSeriesAsync({
|
||||||
|
cal: calendar,
|
||||||
|
event: {
|
||||||
|
...master,
|
||||||
|
attendee: event.attendee?.map((a) =>
|
||||||
|
a.cal_address === user.userData.email ? { ...a, partstat: rsvp } : a
|
||||||
|
),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
if (calendars) {
|
||||||
|
await refreshCalendars(dispatch, calendars, calendarRange);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dispatch(putEventAsync({ cal: calendar, newEvent }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleDelete(
|
||||||
|
isRecurring: boolean,
|
||||||
|
typeOfAction: "solo" | "all" | undefined,
|
||||||
|
onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void,
|
||||||
|
dispatch: Function,
|
||||||
|
calendar: Calendars,
|
||||||
|
event: CalendarEvent,
|
||||||
|
calId: string,
|
||||||
|
eventId: string
|
||||||
|
) {
|
||||||
|
if (isRecurring) {
|
||||||
|
onClose({}, "backdropClick");
|
||||||
|
if (typeOfAction === "solo") {
|
||||||
|
dispatch(deleteEventInstanceAsync({ cal: calendar, event }));
|
||||||
|
} else {
|
||||||
|
dispatch(
|
||||||
|
deleteEventAsync({
|
||||||
|
calId,
|
||||||
|
eventId,
|
||||||
|
eventURL: event.URL,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onClose({}, "backdropClick");
|
||||||
|
dispatch(
|
||||||
|
deleteEventAsync({
|
||||||
|
calId,
|
||||||
|
eventId,
|
||||||
|
eventURL: event.URL,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,14 @@ import Avatar from "@mui/material/Avatar";
|
|||||||
import Badge from "@mui/material/Badge";
|
import Badge from "@mui/material/Badge";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { ThunkDispatch } from "@reduxjs/toolkit";
|
||||||
|
import {
|
||||||
|
getCalendarDetailAsync,
|
||||||
|
getCalendarsListAsync,
|
||||||
|
} from "../../../features/Calendars/CalendarSlice";
|
||||||
|
import { Calendars } from "../../../features/Calendars/CalendarTypes";
|
||||||
import { userAttendee } from "../../../features/User/userDataTypes";
|
import { userAttendee } from "../../../features/User/userDataTypes";
|
||||||
|
import { formatDateToYYYYMMDDTHHMMSS } from "../../../utils/dateUtils";
|
||||||
|
|
||||||
export function renderAttendeeBadge(
|
export function renderAttendeeBadge(
|
||||||
a: userAttendee,
|
a: userAttendee,
|
||||||
@@ -103,3 +110,24 @@ export function stringAvatar(name: string) {
|
|||||||
children: name[0],
|
children: name[0],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function refreshCalendars(
|
||||||
|
dispatch: ThunkDispatch<any, any, any>,
|
||||||
|
calendars: Calendars[],
|
||||||
|
calendarRange: { start: Date; end: Date }
|
||||||
|
) {
|
||||||
|
await dispatch(getCalendarsListAsync());
|
||||||
|
|
||||||
|
calendars.map(
|
||||||
|
async (cal) =>
|
||||||
|
await dispatch(
|
||||||
|
getCalendarDetailAsync({
|
||||||
|
calId: cal.id,
|
||||||
|
match: {
|
||||||
|
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
|
||||||
|
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,10 +14,13 @@ import { getOpenPaasUser, getUserDetails } from "../User/userAPI";
|
|||||||
import { parseCalendarEvent } from "../Events/eventUtils";
|
import { parseCalendarEvent } from "../Events/eventUtils";
|
||||||
import {
|
import {
|
||||||
deleteEvent,
|
deleteEvent,
|
||||||
|
deleteEventInstance,
|
||||||
getEvent,
|
getEvent,
|
||||||
importEventFromFile,
|
importEventFromFile,
|
||||||
moveEvent,
|
moveEvent,
|
||||||
putEvent,
|
putEvent,
|
||||||
|
putEventWithOverrides,
|
||||||
|
updateSeries,
|
||||||
} from "../Events/EventApi";
|
} from "../Events/EventApi";
|
||||||
import {
|
import {
|
||||||
computeWeekRange,
|
computeWeekRange,
|
||||||
@@ -274,6 +277,29 @@ export const deleteEventAsync = createAsyncThunk<
|
|||||||
return { calId, eventId };
|
return { calId, eventId };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const deleteEventInstanceAsync = createAsyncThunk<
|
||||||
|
{ calId: string; eventId: string },
|
||||||
|
{ cal: Calendars; event: CalendarEvent }
|
||||||
|
>("calendars/delEventInstance", async ({ cal, event }) => {
|
||||||
|
await deleteEventInstance(event, cal.ownerEmails?.[0]);
|
||||||
|
return { calId: cal.id, eventId: event.uid };
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateEventInstanceAsync = createAsyncThunk<
|
||||||
|
{ calId: string; event: CalendarEvent },
|
||||||
|
{ cal: Calendars; event: CalendarEvent }
|
||||||
|
>("calendars/updateEventInstance", async ({ cal, event }) => {
|
||||||
|
await putEventWithOverrides(event, cal.ownerEmails?.[0]);
|
||||||
|
return { calId: cal.id, event };
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateSeriesAsync = createAsyncThunk<
|
||||||
|
void,
|
||||||
|
{ cal: Calendars; event: CalendarEvent }
|
||||||
|
>("calendars/updateSeries", async ({ cal, event }) => {
|
||||||
|
await updateSeries(event, cal.ownerEmails?.[0]);
|
||||||
|
});
|
||||||
|
|
||||||
export const createCalendarAsync = createAsyncThunk<
|
export const createCalendarAsync = createAsyncThunk<
|
||||||
{
|
{
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -545,6 +571,18 @@ const CalendarSlice = createSlice({
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.addCase(deleteEventInstanceAsync.fulfilled, (state, action) => {
|
||||||
|
state.pending = false;
|
||||||
|
delete state.list[action.payload.calId].events[action.payload.eventId];
|
||||||
|
})
|
||||||
|
.addCase(updateEventInstanceAsync.fulfilled, (state, action) => {
|
||||||
|
state.pending = false;
|
||||||
|
state.list[action.payload.calId].events[action.payload.event.uid] =
|
||||||
|
action.payload.event;
|
||||||
|
})
|
||||||
|
.addCase(updateSeriesAsync.fulfilled, (state) => {
|
||||||
|
state.pending = false;
|
||||||
|
})
|
||||||
.addCase(createCalendarAsync.fulfilled, (state, action) => {
|
.addCase(createCalendarAsync.fulfilled, (state, action) => {
|
||||||
state.pending = false;
|
state.pending = false;
|
||||||
state.list[`${action.payload.userId}/${action.payload.calId}`] = {
|
state.list[`${action.payload.userId}/${action.payload.calId}`] = {
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import { api } from "../../utils/apiUtils";
|
import { api } from "../../utils/apiUtils";
|
||||||
|
import { TIMEZONES } from "../../utils/timezone-data";
|
||||||
import { CalendarEvent } from "./EventsTypes";
|
import { CalendarEvent } from "./EventsTypes";
|
||||||
import { calendarEventToJCal, parseCalendarEvent } from "./eventUtils";
|
import {
|
||||||
|
calendarEventToJCal,
|
||||||
|
makeTimezone,
|
||||||
|
makeVevent,
|
||||||
|
parseCalendarEvent,
|
||||||
|
} from "./eventUtils";
|
||||||
import ICAL from "ical.js";
|
import ICAL from "ical.js";
|
||||||
|
|
||||||
export async function getEvent(event: CalendarEvent) {
|
export async function getEvent(event: CalendarEvent, isMaster?: boolean) {
|
||||||
const response = await api.get(`dav${event.URL}`);
|
const response = await api.get(`dav${event.URL}`);
|
||||||
const eventData = await response.text();
|
const eventData = await response.text();
|
||||||
const eventical = ICAL.parse(eventData);
|
const eventical = ICAL.parse(eventData);
|
||||||
@@ -13,6 +19,9 @@ export async function getEvent(event: CalendarEvent) {
|
|||||||
event.calId,
|
event.calId,
|
||||||
event.URL
|
event.URL
|
||||||
);
|
);
|
||||||
|
if (isMaster) {
|
||||||
|
return { ...event, ...eventjson };
|
||||||
|
}
|
||||||
return { ...eventjson, ...event };
|
return { ...eventjson, ...event };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +46,107 @@ export async function putEvent(event: CalendarEvent, calOwnerEmail?: string) {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function putEventWithOverrides(
|
||||||
|
updatedEvent: CalendarEvent,
|
||||||
|
calOwnerEmail?: string
|
||||||
|
) {
|
||||||
|
const vevents = await getAllRecurrentEvent(updatedEvent);
|
||||||
|
|
||||||
|
const updatedVevent = makeVevent(
|
||||||
|
updatedEvent,
|
||||||
|
updatedEvent.timezone,
|
||||||
|
calOwnerEmail,
|
||||||
|
!updatedEvent.recurrenceId
|
||||||
|
);
|
||||||
|
let replaced = false;
|
||||||
|
for (let i = 0; i < vevents.length; i++) {
|
||||||
|
const ve = vevents[i];
|
||||||
|
const recurrenceId = ve[1].find(([k]: string[]) => k === "recurrence-id");
|
||||||
|
if (recurrenceId && recurrenceId[3] === updatedEvent.recurrenceId) {
|
||||||
|
vevents[i] = updatedVevent; // replace
|
||||||
|
replaced = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!replaced && updatedEvent.recurrenceId) {
|
||||||
|
vevents.push(updatedVevent); // add new override
|
||||||
|
}
|
||||||
|
|
||||||
|
const timezoneData = TIMEZONES.zones[updatedEvent.timezone];
|
||||||
|
const vtimezone = makeTimezone(timezoneData, updatedEvent);
|
||||||
|
|
||||||
|
const newJCal = ["vcalendar", [], [...vevents, vtimezone.component.jCal]];
|
||||||
|
|
||||||
|
return api(`dav${updatedEvent.URL}`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify(newJCal),
|
||||||
|
headers: {
|
||||||
|
"content-type": "text/calendar; charset=utf-8",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteEventInstance = async (
|
||||||
|
event: CalendarEvent,
|
||||||
|
calOwnerEmail?: string
|
||||||
|
) => {
|
||||||
|
const seriesEvent = await getEvent(
|
||||||
|
{
|
||||||
|
...event,
|
||||||
|
uid: event.uid.split("/")[0],
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
seriesEvent.exdates = [...(seriesEvent.exdates || []), event.start];
|
||||||
|
delete seriesEvent.recurrenceId;
|
||||||
|
|
||||||
|
return putEvent(seriesEvent, calOwnerEmail);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateSeries = async (
|
||||||
|
event: CalendarEvent,
|
||||||
|
calOwnerEmail?: string
|
||||||
|
) => {
|
||||||
|
const vevents = await getAllRecurrentEvent(event);
|
||||||
|
const masterIndex = vevents.findIndex(
|
||||||
|
([, props]: [string, string[]]) =>
|
||||||
|
!props.find(([k]) => k.toLowerCase() === "recurrence-id")
|
||||||
|
);
|
||||||
|
if (masterIndex === -1) {
|
||||||
|
throw new Error("No master VEVENT found for this series");
|
||||||
|
}
|
||||||
|
const rrule = vevents[0][1].find(([k]: string[]) => k === "rrule");
|
||||||
|
|
||||||
|
const tzid = event.timezone;
|
||||||
|
|
||||||
|
const updatedMaster = makeVevent(event, tzid, calOwnerEmail, true);
|
||||||
|
const newRrule = updatedMaster[1].find(([k]: string[]) => k === "rrule");
|
||||||
|
if (!newRrule) {
|
||||||
|
updatedMaster[1].push(rrule);
|
||||||
|
}
|
||||||
|
vevents[masterIndex] = updatedMaster;
|
||||||
|
|
||||||
|
const timezoneData = TIMEZONES.zones[event.timezone];
|
||||||
|
const vtimezone = makeTimezone(timezoneData, event);
|
||||||
|
|
||||||
|
const newJCal = ["vcalendar", [], [...vevents, vtimezone.component.jCal]];
|
||||||
|
return api(`dav${event.URL}`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify(newJCal),
|
||||||
|
headers: {
|
||||||
|
"content-type": "text/calendar; charset=utf-8",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getAllRecurrentEvent(event: CalendarEvent) {
|
||||||
|
const response = await api.get(`dav${event.URL}`);
|
||||||
|
const eventData = await response.text();
|
||||||
|
const jcal = ICAL.parse(eventData);
|
||||||
|
const vevents = jcal[2].filter(([name]: string[]) => name === "vevent");
|
||||||
|
return vevents;
|
||||||
|
}
|
||||||
|
|
||||||
export async function moveEvent(event: CalendarEvent, newUrl: string) {
|
export async function moveEvent(event: CalendarEvent, newUrl: string) {
|
||||||
const response = await api(`dav${event.URL}`, {
|
const response = await api(`dav${event.URL}`, {
|
||||||
method: "MOVE",
|
method: "MOVE",
|
||||||
|
|||||||
@@ -3,14 +3,6 @@ import CloseIcon from "@mui/icons-material/Close";
|
|||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
|
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
|
||||||
import VideocamIcon from "@mui/icons-material/Videocam";
|
import VideocamIcon from "@mui/icons-material/Videocam";
|
||||||
import {
|
|
||||||
deleteEventAsync,
|
|
||||||
moveEventAsync,
|
|
||||||
putEventAsync,
|
|
||||||
removeEvent,
|
|
||||||
} from "../Calendars/CalendarSlice";
|
|
||||||
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
|
||||||
import AttendeeSelector from "../../components/Attendees/AttendeeSearch";
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -33,24 +25,42 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
||||||
|
import AttendeeSelector from "../../components/Attendees/AttendeeSearch";
|
||||||
|
import {
|
||||||
|
handleDelete,
|
||||||
|
handleRSVP,
|
||||||
|
} from "../../components/Event/eventHandlers/eventHandlers";
|
||||||
import RepeatEvent from "../../components/Event/EventRepeat";
|
import RepeatEvent from "../../components/Event/EventRepeat";
|
||||||
import { InfoRow } from "../../components/Event/InfoRow";
|
import { InfoRow } from "../../components/Event/InfoRow";
|
||||||
import { userAttendee } from "../User/userDataTypes";
|
import { refreshCalendars } from "../../components/Event/utils/eventUtils";
|
||||||
|
import { renderAttendeeBadge } from "../../components/Event/utils/eventUtils";
|
||||||
|
import { getCalendarRange } from "../../utils/dateUtils";
|
||||||
|
import {
|
||||||
|
moveEventAsync,
|
||||||
|
putEventAsync,
|
||||||
|
removeEvent,
|
||||||
|
updateEventInstanceAsync,
|
||||||
|
updateSeriesAsync,
|
||||||
|
} from "../Calendars/CalendarSlice";
|
||||||
import { Calendars } from "../Calendars/CalendarTypes";
|
import { Calendars } from "../Calendars/CalendarTypes";
|
||||||
|
import { userAttendee } from "../User/userDataTypes";
|
||||||
|
import { getEvent } from "./EventApi";
|
||||||
import { formatLocalDateTime } from "./EventModal";
|
import { formatLocalDateTime } from "./EventModal";
|
||||||
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
|
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
|
||||||
import { renderAttendeeBadge } from "../../components/Event/utils/eventUtils";
|
|
||||||
|
|
||||||
export default function EventDisplayModal({
|
export default function EventDisplayModal({
|
||||||
eventId,
|
eventId,
|
||||||
calId,
|
calId,
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
|
typeOfAction,
|
||||||
}: {
|
}: {
|
||||||
eventId: string;
|
eventId: string;
|
||||||
calId: string;
|
calId: string;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void;
|
onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void;
|
||||||
|
typeOfAction?: "solo" | "all";
|
||||||
}) {
|
}) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const calendar = useAppSelector((state) => state.calendars.list[calId]);
|
const calendar = useAppSelector((state) => state.calendars.list[calId]);
|
||||||
@@ -87,7 +97,7 @@ export default function EventDisplayModal({
|
|||||||
const [alarm, setAlarm] = useState(event?.alarm?.trigger ?? "");
|
const [alarm, setAlarm] = useState(event?.alarm?.trigger ?? "");
|
||||||
const [busy, setBusy] = useState(event?.transp ?? "OPAQUE");
|
const [busy, setBusy] = useState(event?.transp ?? "OPAQUE");
|
||||||
const [eventClass, setEventClass] = useState(event?.class ?? "PUBLIC");
|
const [eventClass, setEventClass] = useState(event?.class ?? "PUBLIC");
|
||||||
const [timezone] = useState(event?.timezone ?? "UTC");
|
const [timezone, setTimezone] = useState(event?.timezone ?? "UTC");
|
||||||
const [newCalId, setNewCalId] = useState(event?.calId);
|
const [newCalId, setNewCalId] = useState(event?.calId);
|
||||||
const [calendarid, setCalendarid] = useState(
|
const [calendarid, setCalendarid] = useState(
|
||||||
calId.split("/")[0] === user.userData?.openpaasId
|
calId.split("/")[0] === user.userData?.openpaasId
|
||||||
@@ -119,19 +129,29 @@ export default function EventDisplayModal({
|
|||||||
}
|
}
|
||||||
setRepetition(event?.repetition ?? ({} as RepetitionObject));
|
setRepetition(event?.repetition ?? ({} as RepetitionObject));
|
||||||
}, [open, eventId, dispatch, onClose, event]);
|
}, [open, eventId, dispatch, onClose, event]);
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchMasterEvent = async () => {
|
||||||
|
const masterEvent = await getEvent(event);
|
||||||
|
|
||||||
|
setTitle(masterEvent.title ?? "");
|
||||||
|
setDescription(masterEvent.description ?? "");
|
||||||
|
setLocation(masterEvent.location ?? "");
|
||||||
|
setStart(formatLocalDateTime(new Date(masterEvent?.start ?? Date.now())));
|
||||||
|
setEnd(formatLocalDateTime(new Date(masterEvent?.end ?? Date.now())));
|
||||||
|
setAllDay(masterEvent.allday ?? false);
|
||||||
|
setRepetition(masterEvent?.repetition ?? ({} as RepetitionObject));
|
||||||
|
setAlarm(masterEvent?.alarm?.trigger ?? "");
|
||||||
|
setBusy(masterEvent?.transp ?? "OPAQUE");
|
||||||
|
setEventClass(masterEvent?.class ?? "PUBLIC");
|
||||||
|
setTimezone(masterEvent.timezone ?? "UTC");
|
||||||
|
};
|
||||||
|
if (typeOfAction === "all") {
|
||||||
|
fetchMasterEvent();
|
||||||
|
}
|
||||||
|
}, [typeOfAction, event]);
|
||||||
|
|
||||||
if (!event || !calendar) return null;
|
if (!event || !calendar) return null;
|
||||||
|
const isRecurring = event.uid?.includes("/");
|
||||||
function handleRSVP(rsvp: string) {
|
|
||||||
const newEvent = {
|
|
||||||
...event,
|
|
||||||
attendee: event.attendee?.map((a) =>
|
|
||||||
a.cal_address === user.userData.email ? { ...a, partstat: rsvp } : a
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
dispatch(putEventAsync({ cal: calendar, newEvent }));
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
const newEventUID = crypto.randomUUID();
|
const newEventUID = crypto.randomUUID();
|
||||||
@@ -157,28 +177,35 @@ export default function EventDisplayModal({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const [baseId, recurrenceId] = event.uid.split("/");
|
const [baseId, recurrenceId] = event.uid.split("/");
|
||||||
if (recurrenceId) {
|
const calendarRange = getCalendarRange(new Date(start));
|
||||||
Object.keys(userPersonnalCalendars[calendarid].events).forEach(
|
|
||||||
(element) => {
|
if (typeOfAction === "solo") {
|
||||||
if (element.split("/")[0] === baseId) {
|
dispatch(
|
||||||
dispatch(removeEvent({ calendarUid: calId, eventUid: element }));
|
updateEventInstanceAsync({
|
||||||
}
|
cal: userPersonnalCalendars[calendarid],
|
||||||
}
|
event: { ...newEvent, recurrenceId: recurrenceId },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else if (typeOfAction === "all") {
|
||||||
|
dispatch(
|
||||||
|
updateSeriesAsync({
|
||||||
|
cal: userPersonnalCalendars[calendarid],
|
||||||
|
event: { ...newEvent, recurrenceId: recurrenceId },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await refreshCalendars(dispatch, calendars, calendarRange);
|
||||||
|
} else {
|
||||||
|
dispatch(
|
||||||
|
putEventAsync({ cal: userPersonnalCalendars[calendarid], newEvent })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await dispatch(
|
|
||||||
putEventAsync({
|
|
||||||
cal: userPersonnalCalendars[calendarid],
|
|
||||||
newEvent,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
if (newCalId !== calId) {
|
if (newCalId !== calId) {
|
||||||
dispatch(
|
dispatch(
|
||||||
moveEventAsync({
|
moveEventAsync({
|
||||||
cal: userPersonnalCalendars[calendarid],
|
cal: userPersonnalCalendars[calendarid],
|
||||||
newEvent,
|
newEvent,
|
||||||
newURL: `/calendars/${newCalId}/${event.uid}.ics`,
|
newURL: `/calendars/${newCalId}/${baseId}.ics`,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
dispatch(removeEvent({ calendarUid: calId, eventUid: event.uid }));
|
dispatch(removeEvent({ calendarUid: calId, eventUid: event.uid }));
|
||||||
@@ -268,7 +295,17 @@ export default function EventDisplayModal({
|
|||||||
? "success"
|
? "success"
|
||||||
: "primary"
|
: "primary"
|
||||||
}
|
}
|
||||||
onClick={() => handleRSVP("ACCEPTED")}
|
onClick={() =>
|
||||||
|
handleRSVP(
|
||||||
|
dispatch,
|
||||||
|
calendar,
|
||||||
|
user,
|
||||||
|
event,
|
||||||
|
"ACCEPTED",
|
||||||
|
undefined,
|
||||||
|
isRecurring ? typeOfAction : undefined
|
||||||
|
)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Accept
|
Accept
|
||||||
</Button>
|
</Button>
|
||||||
@@ -278,7 +315,17 @@ export default function EventDisplayModal({
|
|||||||
? "warning"
|
? "warning"
|
||||||
: "primary"
|
: "primary"
|
||||||
}
|
}
|
||||||
onClick={() => handleRSVP("TENTATIVE")}
|
onClick={() =>
|
||||||
|
handleRSVP(
|
||||||
|
dispatch,
|
||||||
|
calendar,
|
||||||
|
user,
|
||||||
|
event,
|
||||||
|
"TENTATIVE",
|
||||||
|
undefined,
|
||||||
|
isRecurring ? typeOfAction : undefined
|
||||||
|
)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Maybe
|
Maybe
|
||||||
</Button>
|
</Button>
|
||||||
@@ -288,7 +335,17 @@ export default function EventDisplayModal({
|
|||||||
? "error"
|
? "error"
|
||||||
: "primary"
|
: "primary"
|
||||||
}
|
}
|
||||||
onClick={() => handleRSVP("DECLINED")}
|
onClick={() =>
|
||||||
|
handleRSVP(
|
||||||
|
dispatch,
|
||||||
|
calendar,
|
||||||
|
user,
|
||||||
|
event,
|
||||||
|
"DECLINED",
|
||||||
|
undefined,
|
||||||
|
isRecurring ? typeOfAction : undefined
|
||||||
|
)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Decline
|
Decline
|
||||||
</Button>
|
</Button>
|
||||||
@@ -463,12 +520,14 @@ export default function EventDisplayModal({
|
|||||||
{/* Extended options */}
|
{/* Extended options */}
|
||||||
{showMore && (
|
{showMore && (
|
||||||
<>
|
<>
|
||||||
<RepeatEvent
|
{isOwn && (
|
||||||
repetition={repetition}
|
<RepeatEvent
|
||||||
eventStart={event.start}
|
repetition={repetition}
|
||||||
setRepetition={setRepetition}
|
eventStart={event.start}
|
||||||
isOwn={isOwn}
|
setRepetition={setRepetition}
|
||||||
/>
|
isOwn={isOwn && typeOfAction !== "solo"}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<FormControl fullWidth margin="dense" size="small">
|
<FormControl fullWidth margin="dense" size="small">
|
||||||
<InputLabel id="notification">Notification</InputLabel>
|
<InputLabel id="notification">Notification</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
@@ -546,12 +605,18 @@ export default function EventDisplayModal({
|
|||||||
{isOwn && (
|
{isOwn && (
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => {
|
onClick={() =>
|
||||||
onClose({}, "backdropClick");
|
handleDelete(
|
||||||
dispatch(
|
isRecurring,
|
||||||
deleteEventAsync({ calId, eventId, eventURL: event.URL })
|
typeOfAction,
|
||||||
);
|
onClose,
|
||||||
}}
|
dispatch,
|
||||||
|
calendar,
|
||||||
|
event,
|
||||||
|
calId,
|
||||||
|
eventId
|
||||||
|
)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<DeleteIcon fontSize="small" />
|
<DeleteIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export interface CalendarEvent {
|
|||||||
URL: string;
|
URL: string;
|
||||||
calId: string;
|
calId: string;
|
||||||
uid: string;
|
uid: string;
|
||||||
|
recurrenceId?: string;
|
||||||
transp?: string;
|
transp?: string;
|
||||||
start: string; // ISO date string
|
start: string; // ISO date string
|
||||||
end?: string;
|
end?: string;
|
||||||
@@ -23,6 +24,7 @@ export interface CalendarEvent {
|
|||||||
timezone: string;
|
timezone: string;
|
||||||
repetition?: RepetitionObject;
|
repetition?: RepetitionObject;
|
||||||
alarm?: AlarmObject;
|
alarm?: AlarmObject;
|
||||||
|
exdates?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RepetitionObject {
|
export interface RepetitionObject {
|
||||||
|
|||||||
@@ -79,6 +79,10 @@ export function parseCalendarEvent(
|
|||||||
case "recurrence-id":
|
case "recurrence-id":
|
||||||
recurrenceId = value;
|
recurrenceId = value;
|
||||||
break;
|
break;
|
||||||
|
case "exdate":
|
||||||
|
if (!event.exdates) event.exdates = [];
|
||||||
|
event.exdates.push(value);
|
||||||
|
break;
|
||||||
case "status":
|
case "status":
|
||||||
event.status = String(value);
|
event.status = String(value);
|
||||||
break;
|
break;
|
||||||
@@ -101,6 +105,7 @@ export function parseCalendarEvent(
|
|||||||
}
|
}
|
||||||
if (recurrenceId && event.uid) {
|
if (recurrenceId && event.uid) {
|
||||||
event.uid = `${event.uid}/${recurrenceId}`;
|
event.uid = `${event.uid}/${recurrenceId}`;
|
||||||
|
event.recurrenceId = recurrenceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (valarm) {
|
if (valarm) {
|
||||||
@@ -135,6 +140,36 @@ export function calendarEventToJCal(
|
|||||||
): any[] {
|
): any[] {
|
||||||
const tzid = event.timezone; // Fallback to UTC if no timezone provided
|
const tzid = event.timezone; // Fallback to UTC if no timezone provided
|
||||||
|
|
||||||
|
const vevent: any[] = makeVevent(event, tzid, calOwnerEmail);
|
||||||
|
|
||||||
|
const timezoneData = TIMEZONES.zones[event.timezone];
|
||||||
|
const vtimezone = makeTimezone(timezoneData, event);
|
||||||
|
|
||||||
|
return ["vcalendar", [], [vevent, vtimezone.component.jCal]];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeTimezone(
|
||||||
|
timezoneData: { ics: string; latitude: string; longitude: string },
|
||||||
|
event: CalendarEvent
|
||||||
|
) {
|
||||||
|
if (!timezoneData) {
|
||||||
|
return new ICAL.Timezone({
|
||||||
|
component: TIMEZONES.zones["Etc/UTC"].ics,
|
||||||
|
tzid: "Etc/UTC",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return new ICAL.Timezone({
|
||||||
|
component: timezoneData.ics,
|
||||||
|
tzid: event.timezone,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeVevent(
|
||||||
|
event: CalendarEvent,
|
||||||
|
tzid: string,
|
||||||
|
calOwnerEmail: string | undefined,
|
||||||
|
isMasterEvent?: boolean
|
||||||
|
) {
|
||||||
const vevent: any[] = [
|
const vevent: any[] = [
|
||||||
"vevent",
|
"vevent",
|
||||||
[
|
[
|
||||||
@@ -154,6 +189,7 @@ export function calendarEventToJCal(
|
|||||||
event.x_openpass_videoconference ?? null,
|
event.x_openpass_videoconference ?? null,
|
||||||
],
|
],
|
||||||
["summary", {}, "text", event.title ?? ""],
|
["summary", {}, "text", event.title ?? ""],
|
||||||
|
["dstamp", { tzid }, "date-time", formatDateToICal(new Date(), false)],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
if (event.alarm?.trigger) {
|
if (event.alarm?.trigger) {
|
||||||
@@ -201,6 +237,9 @@ export function calendarEventToJCal(
|
|||||||
if (event.location) {
|
if (event.location) {
|
||||||
vevent[1].push(["location", {}, "text", event.location]);
|
vevent[1].push(["location", {}, "text", event.location]);
|
||||||
}
|
}
|
||||||
|
if (event.recurrenceId && !isMasterEvent) {
|
||||||
|
vevent[1].push(["recurrence-id", {}, "date-time", event.recurrenceId]);
|
||||||
|
}
|
||||||
if (event.description) {
|
if (event.description) {
|
||||||
vevent[1].push(["description", {}, "text", event.description]);
|
vevent[1].push(["description", {}, "text", event.description]);
|
||||||
}
|
}
|
||||||
@@ -242,21 +281,20 @@ export function calendarEventToJCal(
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
const timezoneData = TIMEZONES.zones[event.timezone];
|
if (event.exdates && event.exdates.length > 0) {
|
||||||
if (!timezoneData) {
|
event.exdates.forEach((ex) => {
|
||||||
const vtimezone = new ICAL.Timezone({
|
vevent[1].push([
|
||||||
component: TIMEZONES.zones["Etc/UTC"].ics,
|
"exdate",
|
||||||
tzid: "Etc/UTC",
|
{ tzid },
|
||||||
|
"date-time",
|
||||||
|
formatDateToICal(new Date(ex), false),
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
return ["vcalendar", [], [vevent, vtimezone.component.jCal]];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const vtimezone = new ICAL.Timezone({
|
return vevent;
|
||||||
component: timezoneData.ics,
|
|
||||||
tzid: event.timezone,
|
|
||||||
});
|
|
||||||
return ["vcalendar", [], [vevent, vtimezone.component.jCal]];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDateToICal(date: Date, allday: Boolean) {
|
function formatDateToICal(date: Date, allday: Boolean) {
|
||||||
// Format date like: 2025-02-14T11:00:00 (local time)
|
// Format date like: 2025-02-14T11:00:00 (local time)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user