refactor recurring event modification (#173)

This commit is contained in:
Camille Moussu
2025-10-07 18:16:00 +02:00
committed by GitHub
parent 3f8f3eff23
commit c86db8fa49
15 changed files with 1924 additions and 176 deletions
+70
View File
@@ -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>
);
}
+144 -30
View File
@@ -27,12 +27,13 @@ import { useAppDispatch, useAppSelector } from "../../app/hooks";
import {
deleteEventAsync,
getEventAsync,
putEventAsync,
} from "../../features/Calendars/CalendarSlice";
import { dlEvent } from "../../features/Events/EventApi";
import EventDisplayModal from "../../features/Events/EventDisplay";
import { ResponsiveDialog } from "../Dialog";
import { EditModeDialog } from "./EditModeDialog";
import EventDuplication from "./EventDuplicate";
import { handleDelete, handleRSVP } from "./eventHandlers/eventHandlers";
import { InfoRow } from "./InfoRow";
import { renderAttendeeBadge } from "./utils/eventUtils";
export default function EventPreviewModal({
@@ -50,13 +51,26 @@ export default function EventPreviewModal({
}) {
const dispatch = useAppDispatch();
const calendars = useAppSelector((state) => state.calendars);
const calendarList = Object.values(
useAppSelector((state) => state.calendars.list)
);
const calendar = tempEvent
? calendars.templist[calId]
: calendars.list[calId];
const event = calendar.events[eventId];
const user = useAppSelector((state) => state.user);
const isRecurring = event?.uid?.includes("/");
const [showAllAttendees, setShowAllAttendees] = 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>(
null
);
@@ -89,18 +103,6 @@ export default function EventPreviewModal({
(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 (
<>
<ResponsiveDialog
@@ -137,8 +139,16 @@ export default function EventPreviewModal({
<IconButton
size="small"
onClick={async () => {
setOpenFullDisplay(!openFullDisplay);
await dispatch(getEventAsync(event));
if (isRecurring) {
setAfterChoiceFunc(() => async () => {
await dispatch(getEventAsync(event));
setOpenFullDisplay(!openFullDisplay);
});
setOpenEditModePopup("edit");
} else {
await dispatch(getEventAsync(event));
setOpenFullDisplay(!openFullDisplay);
}
}}
>
<EditIcon />
@@ -173,14 +183,32 @@ export default function EventPreviewModal({
{user.userData.email === event.organizer?.cal_address && (
<MenuItem
onClick={() => {
onClose({}, "backdropClick");
dispatch(
deleteEventAsync({
calId,
eventId,
eventURL: event.URL,
})
);
if (isRecurring) {
setAfterChoiceFunc(
() =>
(typeOfAction?: "solo" | "all" | undefined) =>
handleDelete(
isRecurring,
typeOfAction,
onClose,
dispatch,
calendar,
event,
calId,
eventId
)
);
setOpenEditModePopup("edit");
} else {
onClose({}, "backdropClick");
dispatch(
deleteEventAsync({
calId,
eventId,
eventURL: event.URL,
})
);
}
}}
>
Delete event
@@ -216,10 +244,7 @@ export default function EventPreviewModal({
}
actions={
<>
{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] && (
{currentUserAttendee && (
<Box style={{ display: "flex", flexDirection: "row" }}>
<Typography variant="body2">Attending?</Typography>
<ButtonGroup size="small" fullWidth>
@@ -229,7 +254,34 @@ export default function EventPreviewModal({
? "success"
: "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
</Button>
@@ -239,7 +291,33 @@ export default function EventPreviewModal({
? "warning"
: "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
</Button>
@@ -249,7 +327,33 @@ export default function EventPreviewModal({
? "error"
: "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
</Button>
@@ -387,11 +491,21 @@ export default function EventPreviewModal({
</>
)}
</ResponsiveDialog>
<EditModeDialog
type={openEditModePopup}
setOpen={setOpenEditModePopup}
event={event}
eventAction={(type: "solo" | "all" | undefined) => {
setTypeOfAction(type);
afterChoiceFunc && afterChoiceFunc(type);
}}
/>
<EventDisplayModal
open={openFullDisplay}
onClose={() => setOpenFullDisplay(false)}
eventId={eventId}
calId={calId}
typeOfAction={typeOfAction}
/>
</>
);
+17 -4
View File
@@ -29,7 +29,6 @@ export default function RepeatEvent({
}) {
const days = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"];
const day = new Date(eventStart);
// derive endOption based on repetition
const getEndOption = () => {
if (repetition.occurrences && repetition.occurrences >= 0) return "after";
@@ -65,6 +64,7 @@ export default function RepeatEvent({
<TextField
type="number"
value={repetition.interval ?? 1}
disabled={!isOwn}
onChange={(e) =>
setRepetition({
...repetition,
@@ -78,6 +78,7 @@ export default function RepeatEvent({
<FormControl size="small" style={{ minWidth: 120 }}>
<Select
value={repetition.freq ?? "daily"}
disabled={!isOwn}
onChange={(e: SelectChangeEvent) => {
if (e.target.value === "weekly") {
setRepetition({
@@ -86,7 +87,11 @@ export default function RepeatEvent({
selectedDays: [days[day.getDay() - 1]],
});
} 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) => (
<FormControlLabel
key={day}
disabled={!isOwn}
control={
<Checkbox
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
disabled={!isOwn}
value="after"
control={<Radio />}
label={
@@ -172,7 +184,7 @@ export default function RepeatEvent({
}
style={{ width: 100 }}
inputProps={{ min: 1 }}
disabled={endOption !== "after"}
disabled={!isOwn || endOption !== "after"}
/>
occurrences
</Box>
@@ -180,6 +192,7 @@ export default function RepeatEvent({
/>
<FormControlLabel
disabled={!isOwn}
value="on"
control={<Radio />}
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,
})
);
}
}
+28
View File
@@ -4,7 +4,14 @@ import Avatar from "@mui/material/Avatar";
import Badge from "@mui/material/Badge";
import Box from "@mui/material/Box";
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 { formatDateToYYYYMMDDTHHMMSS } from "../../../utils/dateUtils";
export function renderAttendeeBadge(
a: userAttendee,
@@ -103,3 +110,24 @@ export function stringAvatar(name: string) {
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),
},
})
)
);
}