Merge pull request #95 from linagora/77-implement-complex-repetition-ui

implement complex repetition ui
This commit is contained in:
Camille Moussu
2025-09-09 11:50:37 +02:00
committed by GitHub
12 changed files with 869 additions and 366 deletions
+366
View File
@@ -0,0 +1,366 @@
import { screen, fireEvent, waitFor, act } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "../utils/Renderwithproviders";
import RepeatEvent from "../../src/components/Event/EventRepeat";
import EventPopover from "../../src/features/Events/EventModal";
import { RepetitionObject } from "../../src/features/Events/EventsTypes";
import { DateSelectArg } from "@fullcalendar/core";
import { formatDateToYYYYMMDDTHHMMSS } from "../../src/utils/dateUtils";
import * as eventThunks from "../../src/features/Calendars/CalendarSlice";
import * as apiUtils from "../../src/utils/apiUtils";
const baseRepetition: RepetitionObject = {
freq: "",
interval: 1,
occurrences: 0,
endDate: "",
selectedDays: [],
};
const mockOnClose = jest.fn();
const mockSetSelectedRange = jest.fn();
const mockCalendarRef = { current: { select: jest.fn() } } as any;
const preloadedState = {
user: {
userData: {
sub: "test",
email: "test@test.com",
sid: "aiYbWZSk2g0F+LrQeD7Dg4QcUMR8R/zTZdZBiA7N6Ro",
openpaasId: "667037022b752d0026472254",
},
organiserData: {
cn: "test",
cal_address: "test@test.com",
},
},
calendars: {
list: {
"667037022b752d0026472254/cal1": {
id: "667037022b752d0026472254/cal1",
name: "Calendar 1",
color: "#FF0000",
},
"667037022b752d0026472254/cal2": {
id: "667037022b752d0026472254/cal2",
name: "Calendar 2",
color: "#00FF00",
},
},
pending: false,
},
};
const defaultSelectedRange = {
startStr: "2025-07-18T09:00",
endStr: "2025-07-18T10:00",
start: new Date("2025-07-18T09:00"),
end: new Date("2025-07-18T10:00"),
allDay: false,
resource: undefined,
} as unknown as DateSelectArg;
function setupRepeatEvent(props?: Partial<RepetitionObject>, state?: any) {
const setRepetition = jest.fn();
renderWithProviders(
<RepeatEvent
repetition={{ ...baseRepetition, ...props }}
eventStart={defaultSelectedRange.start}
setRepetition={setRepetition}
isOwn={true}
/>,
state
);
return { setRepetition };
}
async function setupEventPopover(
overrides?: Partial<{ start: string; end: string }>
) {
jest
.spyOn(crypto, "randomUUID")
.mockReturnValue("fixed-uuid-with-correct-format");
const originalDateResolvedOptions =
new Intl.DateTimeFormat().resolvedOptions();
jest.spyOn(Intl.DateTimeFormat.prototype, "resolvedOptions").mockReturnValue({
...originalDateResolvedOptions,
timeZone: "UTC",
});
renderWithProviders(
<EventPopover
anchorEl={document.body}
open={true}
onClose={mockOnClose}
selectedRange={defaultSelectedRange}
setSelectedRange={mockSetSelectedRange}
calendarRef={mockCalendarRef}
/>,
preloadedState
);
act(() => {
fireEvent.change(screen.getByLabelText("Title"), {
target: { value: "Meeting" },
});
fireEvent.click(screen.getByLabelText("All day"));
fireEvent.change(screen.getByLabelText("Start"), {
target: {
value: (overrides?.start ?? "2025-07-18T00:00:00.000Z").split("T")[0],
},
});
fireEvent.change(screen.getByLabelText("End"), {
target: {
value: (overrides?.end ?? "2025-07-19T00:00:00.000Z").split("T")[0],
},
});
fireEvent.click(screen.getByText("Show More"));
});
const select = screen.getByLabelText(/repetition/i);
userEvent.click(select);
return jest.spyOn(apiUtils, "api");
}
async function expectRRule(expected: any) {
const spyAPi = jest.spyOn(apiUtils, "api");
act(() => fireEvent.click(screen.getByText("Save")));
await waitFor(() => {
expect(spyAPi).toHaveBeenCalled();
});
const receivedPayload: string =
spyAPi.mock.calls[0][1]?.body?.toString() ?? "";
const [, , [vevent]] = JSON.parse(receivedPayload);
const rrule = vevent[1].find(([name]: any) => name === "rrule");
if (rrule[3].byday) {
expect({
...rrule[3],
byday: rrule[3].byday.sort(),
}).toEqual({
...expected,
byday: expected.byday.sort(),
});
} else {
expect(rrule[3]).toEqual(expected);
}
}
describe("RepeatEvent", () => {
it("renders with no repetition by default", () => {
setupRepeatEvent();
expect(screen.getByLabelText(/repetition/i)).toBeInTheDocument();
expect(screen.queryByText(/daily/i)).not.toBeInTheDocument();
});
it("allows selecting repetition frequency", async () => {
const { setRepetition } = setupRepeatEvent();
const select = screen.getByLabelText(/repetition/i);
act(() => {
userEvent.click(select);
});
await waitFor(async () =>
userEvent.click(await screen.findByText(/repeat weekly/i))
);
expect(setRepetition).toHaveBeenCalledWith(
expect.objectContaining({ freq: "weekly" })
);
});
it("renders interval input when frequency is selected", () => {
setupRepeatEvent({ freq: "daily", interval: 2 });
expect(screen.getByText(/interval/i)).toBeInTheDocument();
expect(screen.getByDisplayValue("2")).toBeInTheDocument();
});
it("updates interval value", () => {
const { setRepetition } = setupRepeatEvent({ freq: "daily", interval: 1 });
const input = screen.getByDisplayValue("1");
fireEvent.change(input, { target: { value: "5" } });
expect(setRepetition).toHaveBeenCalledWith(
expect.objectContaining({ interval: 5 })
);
});
it("toggles day selection for weekly frequency", () => {
const { setRepetition } = setupRepeatEvent({
freq: "weekly",
selectedDays: [],
});
act(() => {
const mondayCheckbox = screen.getByLabelText("MO");
fireEvent.click(mondayCheckbox);
});
expect(setRepetition).toHaveBeenCalledWith(
expect.objectContaining({ selectedDays: ["MO"] })
);
});
});
describe("Repeat Event API calls", () => {
beforeEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});
it("sends correct CalendarEvent payload", async () => {
setupEventPopover();
userEvent.click(await screen.findByText(/repeat weekly/i));
const spy = jest
.spyOn(eventThunks, "putEventAsync")
.mockImplementation((payload) => () => Promise.resolve(payload) as any);
act(() => fireEvent.click(screen.getByText("Save")));
await waitFor(() => expect(spy).toHaveBeenCalled());
const received = spy.mock.calls[0][0];
expect(received.cal).toEqual(
preloadedState.calendars.list["667037022b752d0026472254/cal1"]
);
expect(received.newEvent.title).toBe("Meeting");
expect(
formatDateToYYYYMMDDTHHMMSS(received.newEvent.start).split("T")[0]
).toBe("20250718");
expect(
formatDateToYYYYMMDDTHHMMSS(received.newEvent.end || new Date()).split(
"T"
)[0]
).toBe("20250719");
expect(received.newEvent.organizer).toEqual(
preloadedState.user.organiserData
);
const day = new Date(received.newEvent.start)
.toLocaleString("en-UK", {
weekday: "short",
})
.slice(0, 2)
.toUpperCase();
expect(received.newEvent.repetition).toEqual({
freq: "weekly",
selectedDays: [day],
});
expect(received.newEvent.color).toEqual(
preloadedState.calendars.list["667037022b752d0026472254/cal1"].color
);
expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
});
it("sends correct API payload for repeat daily", async () => {
await setupEventPopover();
userEvent.click(await screen.findByText(/repeat daily/i));
await expectRRule({ freq: "daily" });
expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
});
it("sends correct API payload for repeat daily with 2 day interval", async () => {
await setupEventPopover();
userEvent.click(await screen.findByText(/repeat daily/i));
const intervalInput = screen.getByDisplayValue("1");
fireEvent.change(intervalInput, { target: { value: "2" } });
await expectRRule({ freq: "daily", interval: 2 });
expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
});
it("sends correct API payload for repeat daily for 5 repetitions", async () => {
await setupEventPopover();
userEvent.click(await screen.findByText(/repeat daily/i));
userEvent.click(screen.getByLabelText(/after/i));
const input = screen.getAllByRole("spinbutton")[1];
fireEvent.change(input, { target: { value: "5" } });
await expectRRule({ freq: "daily", count: 5 });
expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
});
it("sends correct API payload for repeat daily until now+5days", async () => {
await setupEventPopover();
userEvent.click(await screen.findByText(/repeat daily/i));
userEvent.click(screen.getAllByLabelText(/on/i)[3]);
const untilInput = screen.getByTestId("end-date");
const futureDate = new Date();
futureDate.setDate(futureDate.getDate() + 5);
fireEvent.change(untilInput, {
target: { value: futureDate.toISOString().split("T")[0] },
});
await expectRRule({
freq: "daily",
until: futureDate.toISOString().split("T")[0],
});
expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
});
it("sends correct API payload for repeat weekly on Thursday and event day (Friday)", async () => {
await setupEventPopover();
userEvent.click(await screen.findByText(/repeat weekly/i));
userEvent.click(screen.getByLabelText("TH"));
await expectRRule({ freq: "weekly", byday: ["TH", "FR"] });
expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
});
it("sends correct API payload for repeat weekly on Thursday and event day (Friday) and an interval of 3 weeks", async () => {
await setupEventPopover();
userEvent.click(await screen.findByText(/repeat weekly/i));
userEvent.click(screen.getByLabelText("TH"));
const intervalInput = screen.getByDisplayValue("1");
fireEvent.change(intervalInput, { target: { value: "3" } });
await expectRRule({ freq: "weekly", byday: ["TH", "FR"], interval: 3 });
expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
});
it("sends correct API payload for repeat monthly", async () => {
await setupEventPopover();
userEvent.click(await screen.findByText(/repeat monthly/i));
await expectRRule({ freq: "monthly" });
expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
});
it("sends correct API payload for repeat monthly and end after 5 occurrences", async () => {
await setupEventPopover();
userEvent.click(await screen.findByText(/repeat monthly/i));
userEvent.click(screen.getByLabelText(/after/i));
const input = screen.getAllByRole("spinbutton")[1];
fireEvent.change(input, { target: { value: "5" } });
await expectRRule({ freq: "monthly", count: 5 });
expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
});
it("sends correct API payload for repeat yearly", async () => {
await setupEventPopover();
userEvent.click(await screen.findByText(/repeat yearly/i));
await expectRRule({ freq: "yearly" });
expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
});
it("sends correct API payload for repeat yearly, but user first choose to end after 5 occurrences then changed mind and chose to not end", async () => {
await setupEventPopover();
userEvent.click(await screen.findByText(/repeat yearly/i));
userEvent.click(screen.getByLabelText(/after/i));
const input = screen.getAllByRole("spinbutton")[1];
fireEvent.change(input, { target: { value: "5" } });
userEvent.click(screen.getByLabelText(/never/i));
await expectRRule({ freq: "yearly" });
expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
});
});
+31 -16
View File
@@ -474,6 +474,37 @@ describe("Event Preview Display", () => {
const updatedEvent = spy.mock.calls[0][0].newEvent; const updatedEvent = spy.mock.calls[0][0].newEvent;
expect(updatedEvent.attendee[0].partstat).toBe("DECLINED"); expect(updatedEvent.attendee[0].partstat).toBe("DECLINED");
}); });
it("handles Edit click", async () => {
const spy = jest
.spyOn(eventThunks, "getEventAsync")
.mockImplementation((payload) => {
return () =>
Promise.resolve({
calId: payload.calId,
event:
preloadedState.calendars.list["667037022b752d0026472254/cal1"]
.events["event1"],
}) as any;
});
renderWithProviders(
<EventPreviewModal
anchorPosition={{ top: 0, left: 0 }}
open={true}
onClose={mockOnClose}
calId={"667037022b752d0026472254/cal1"}
eventId={"event1"}
/>,
preloadedState
);
fireEvent.click(screen.getByTestId("EditIcon"));
await waitFor(() => {
expect(spy).toHaveBeenCalled();
});
expect(screen.getByText("Edit Event")).toBeInTheDocument();
});
}); });
describe("Event Full Display", () => { describe("Event Full Display", () => {
@@ -917,18 +948,6 @@ describe("Event Full Display", () => {
expect(updatedEvent.attendee[0].partstat).toBe("DECLINED"); expect(updatedEvent.attendee[0].partstat).toBe("DECLINED");
}); });
it("toggle Show More reveals extra fields", async () => { it("toggle Show More reveals extra fields", async () => {
const spy = jest
.spyOn(eventThunks, "getEventAsync")
.mockImplementation((payload) => {
return () =>
Promise.resolve({
calId: payload.calId,
event:
preloadedState.calendars.list["667037022b752d0026472254/cal1"]
.events["event1"],
}) as any;
});
renderWithProviders( renderWithProviders(
<EventDisplayModal <EventDisplayModal
open={true} open={true}
@@ -942,10 +961,6 @@ describe("Event Full Display", () => {
fireEvent.click(screen.getByText("Show More")); fireEvent.click(screen.getByText("Show More"));
}); });
await waitFor(() => {
expect(spy).toHaveBeenCalled();
});
console.log(spy);
await waitFor(() => { await waitFor(() => {
expect(screen.getByLabelText(/Alarm/i)).toBeInTheDocument(); expect(screen.getByLabelText(/Alarm/i)).toBeInTheDocument();
expect(screen.getByLabelText(/Repetition/i)).toBeInTheDocument(); expect(screen.getByLabelText(/Repetition/i)).toBeInTheDocument();
@@ -286,7 +286,6 @@ describe("EventPopover", () => {
preloadedState.calendars.list["667037022b752d0026472254/cal1"] preloadedState.calendars.list["667037022b752d0026472254/cal1"]
); );
console.log(receivedPayload.newEvent.start);
expect(receivedPayload.newEvent.title).toBe(newEvent.title); expect(receivedPayload.newEvent.title).toBe(newEvent.title);
expect(receivedPayload.newEvent.description).toBe(newEvent.description); expect(receivedPayload.newEvent.description).toBe(newEvent.description);
expect( expect(
+2 -2
View File
@@ -280,7 +280,7 @@ describe("calendarEventToJCal", () => {
allday: true, allday: true,
location: "Room 101", location: "Room 101",
description: "Discuss project roadmap.", description: "Discuss project roadmap.",
repetition: { freq: "WEEKLY" }, repetition: { freq: "WEEKLY", interval: 2 },
organizer: { organizer: {
cn: "Alice", cn: "Alice",
cal_address: "alice@example.com", cal_address: "alice@example.com",
@@ -315,7 +315,7 @@ describe("calendarEventToJCal", () => {
["location", {}, "text", "Room 101"], ["location", {}, "text", "Room 101"],
["description", {}, "text", "Discuss project roadmap."], ["description", {}, "text", "Discuss project roadmap."],
["x-openpaas-videoconference", {}, "unknown", null], ["x-openpaas-videoconference", {}, "unknown", null],
["rrule", {}, "recur", { freq: "WEEKLY" }], ["rrule", {}, "recur", { freq: "WEEKLY", interval: 2 }],
[ [
"organizer", "organizer",
{ cn: "Alice" }, { cn: "Alice" },
@@ -32,10 +32,12 @@ export default function UserSearch({
useEffect(() => { useEffect(() => {
const delayDebounceFn = setTimeout(async () => { const delayDebounceFn = setTimeout(async () => {
if (query) {
setLoading(true); setLoading(true);
const res = await searchUsers(query); const res = await searchUsers(query);
setOptions(res); setOptions(res);
setLoading(false); setLoading(false);
}
}, 300); }, 300);
return () => clearTimeout(delayDebounceFn); return () => clearTimeout(delayDebounceFn);
+14 -1
View File
@@ -14,6 +14,7 @@ import { CalendarEvent } from "../../features/Events/EventsTypes";
import CalendarSelection from "./CalendarSelection"; import CalendarSelection from "./CalendarSelection";
import { import {
getCalendarDetailAsync, getCalendarDetailAsync,
getEventAsync,
putEventAsync, putEventAsync,
updateEventLocal, updateEventLocal,
} from "../../features/Calendars/CalendarSlice"; } from "../../features/Calendars/CalendarSlice";
@@ -326,11 +327,21 @@ export default function CalendarApp() {
setEventDisplayedCalId(info.event.extendedProps.calId); setEventDisplayedCalId(info.event.extendedProps.calId);
} }
}} }}
eventAllow={(dropInfo, draggedEvent) => {
if (
draggedEvent?.extendedProps.uid &&
draggedEvent.extendedProps.uid.split("/")[1]
) {
return false;
}
return true;
}}
eventDrop={(arg) => { eventDrop={(arg) => {
const event = const event =
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 totalDeltaMs = getDeltaInMilliseconds(arg.delta); const totalDeltaMs = getDeltaInMilliseconds(arg.delta);
const originalStart = new Date(event.start); const originalStart = new Date(event.start);
@@ -358,7 +369,9 @@ export default function CalendarApp() {
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]) {
dispatch(getEventAsync(event));
}
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)
+91 -27
View File
@@ -6,46 +6,56 @@ import {
MenuItem, MenuItem,
Box, Box,
Stack, Stack,
Paper,
Typography, Typography,
TextField, TextField,
Checkbox, Checkbox,
List,
ListItem,
FormControlLabel, FormControlLabel,
FormGroup, FormGroup,
Radio, Radio,
RadioGroup, RadioGroup,
} from "@mui/material"; } from "@mui/material";
import { useState } from "react"; import { useEffect, useState } from "react";
import { RepetitionObject } from "../../features/Events/EventsTypes"; import { RepetitionObject } from "../../features/Events/EventsTypes";
export default function RepeatEvent({ export default function RepeatEvent({
repetition, repetition,
eventStart,
setRepetition, setRepetition,
isOwn = true, isOwn = true,
}: { }: {
repetition: RepetitionObject; repetition: RepetitionObject;
eventStart: Date;
setRepetition: Function; setRepetition: Function;
isOwn?: boolean; isOwn?: boolean;
}) { }) {
console.log(JSON.stringify(repetition));
const repetitionValues = ["day", "week", "month", "year"]; const repetitionValues = ["day", "week", "month", "year"];
const [interval, setInterval] = useState(repetition.interval ?? 0);
const [selectedDays, setSelectedDays] = useState<string[]>(
repetition.selectedDays ?? []
);
const [endOption, setEndOption] = useState("");
const [occurrences, setOccurrences] = useState(repetition.occurrences) ?? 0;
const [endDate, setEndDate] = useState(repetition.endDate ?? "");
const days = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"]; 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";
if (repetition.endDate) return "on";
return "never";
};
const [endOption, setEndOption] = useState(getEndOption());
// keep endOption in sync if repetition changes from parent
useEffect(() => {
if (!endOption) {
setEndOption(getEndOption());
}
}, [repetition.occurrences, repetition.endDate]);
const handleDayChange = (day: string) => { const handleDayChange = (day: string) => {
setSelectedDays((prev: string[]) => const updatedDays = repetition.selectedDays?.includes(day)
prev.includes(day) ? prev.filter((d) => d !== day) : [...prev, day] ? repetition.selectedDays.filter((d) => d !== day)
); : [...(repetition.selectedDays ?? []), day];
setRepetition({ ...repetition, selectedDays: updatedDays });
}; };
return ( return (
<FormControl fullWidth margin="dense" size="small"> <FormControl fullWidth margin="dense" size="small">
<InputLabel id="repeat">Repetition</InputLabel> <InputLabel id="repeat">Repetition</InputLabel>
@@ -54,9 +64,17 @@ export default function RepeatEvent({
value={repetition.freq ?? ""} value={repetition.freq ?? ""}
disabled={!isOwn} disabled={!isOwn}
label="Repetition" label="Repetition"
onChange={(e: SelectChangeEvent) => onChange={(e: SelectChangeEvent) => {
setRepetition({ ...repetition, freq: e.target.value }) if (e.target.value === "weekly") {
setRepetition({
...repetition,
freq: e.target.value,
selectedDays: [days[day.getDay() - 1]],
});
} else {
setRepetition({ ...repetition, freq: e.target.value });
} }
}}
> >
<MenuItem value={""}>No Repetition</MenuItem> <MenuItem value={""}>No Repetition</MenuItem>
<MenuItem value={"daily"}>Repeat daily</MenuItem> <MenuItem value={"daily"}>Repeat daily</MenuItem>
@@ -64,18 +82,24 @@ export default function RepeatEvent({
<MenuItem value={"monthly"}>Repeat monthly</MenuItem> <MenuItem value={"monthly"}>Repeat monthly</MenuItem>
<MenuItem value={"yearly"}>Repeat yearly</MenuItem> <MenuItem value={"yearly"}>Repeat yearly</MenuItem>
</Select> </Select>
{repetition.freq && ( {repetition.freq && (
<Stack> <Stack>
{/* Interval */}
<Box display="flex" alignItems="center" gap={2} mb={2}> <Box display="flex" alignItems="center" gap={2} mb={2}>
<Typography>Interval:</Typography> <Typography>Interval:</Typography>
<TextField <TextField
type="number" type="number"
value={interval} value={repetition.interval ?? 1}
onChange={(e) => setInterval(Number(e.target.value))} onChange={(e) =>
setRepetition({
...repetition,
interval: Number(e.target.value),
})
}
size="small" size="small"
sx={{ width: 80 }} sx={{ width: 80 }}
/> />
<Typography> <Typography>
{ {
repetitionValues[ repetitionValues[
@@ -84,6 +108,8 @@ export default function RepeatEvent({
} }
</Typography> </Typography>
</Box> </Box>
{/* Weekly selection */}
{repetition.freq === "weekly" && ( {repetition.freq === "weekly" && (
<Box> <Box>
<Typography variant="body2" gutterBottom> <Typography variant="body2" gutterBottom>
@@ -95,7 +121,9 @@ export default function RepeatEvent({
key={day} key={day}
control={ control={
<Checkbox <Checkbox
checked={selectedDays.includes(day)} checked={
repetition.selectedDays?.includes(day) ?? false
}
onChange={() => handleDayChange(day)} onChange={() => handleDayChange(day)}
/> />
} }
@@ -105,13 +133,36 @@ export default function RepeatEvent({
</FormGroup> </FormGroup>
</Box> </Box>
)} )}
{/* End options */}
<Box> <Box>
<Typography variant="body2" gutterBottom sx={{ mt: 2 }}> <Typography variant="body2" gutterBottom sx={{ mt: 2 }}>
End: End:
</Typography> </Typography>
<RadioGroup <RadioGroup
value={endOption} value={endOption}
onChange={(e) => setEndOption(e.target.value)} onChange={(e) => {
const value = e.target.value;
setEndOption(value);
if (value === "never") {
setRepetition({ ...repetition, occurrences: 0, endDate: "" });
}
if (value === "after") {
setRepetition({
...repetition,
occurrences: 0,
endDate: "",
});
}
if (value === "on") {
setRepetition({
...repetition,
occurrences: 0,
endDate: new Date().toISOString().slice(0, 16),
});
}
}}
> >
<FormControlLabel <FormControlLabel
value="never" value="never"
@@ -128,8 +179,14 @@ export default function RepeatEvent({
<TextField <TextField
type="number" type="number"
size="small" size="small"
value={occurrences} value={repetition.occurrences ?? 0}
onChange={(e) => setOccurrences(Number(e.target.value))} onChange={(e) =>
setRepetition({
...repetition,
endDate: "",
occurrences: Number(e.target.value),
})
}
sx={{ width: 100 }} sx={{ width: 100 }}
inputProps={{ min: 1 }} inputProps={{ min: 1 }}
disabled={endOption !== "after"} disabled={endOption !== "after"}
@@ -147,9 +204,16 @@ export default function RepeatEvent({
On On
<TextField <TextField
type="date" type="date"
inputProps={{ "data-testid": "end-date" }}
size="small" size="small"
value={endDate} value={repetition.endDate ?? ""}
onChange={(e) => setEndDate(e.target.value)} onChange={(e) =>
setRepetition({
...repetition,
occurrences: 0,
endDate: e.target.value,
})
}
disabled={endOption !== "on"} disabled={endOption !== "on"}
/> />
</Box> </Box>
+25 -5
View File
@@ -71,11 +71,18 @@ export const putEventAsync = createAsyncThunk<
{ cal: Calendars; newEvent: CalendarEvent } // Arg type { cal: Calendars; newEvent: CalendarEvent } // Arg type
>("calendars/putEvent", async ({ cal, newEvent }) => { >("calendars/putEvent", async ({ cal, newEvent }) => {
const response = await putEvent(newEvent); const response = await putEvent(newEvent);
const eventDate = new Date(newEvent.start);
const weekStart = new Date(eventDate);
weekStart.setHours(0, 0, 0, 0);
weekStart.setDate(eventDate.getDate() - eventDate.getDay());
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekStart.getDate() + 7);
const calEvents = (await getCalendar(cal.id, { const calEvents = (await getCalendar(cal.id, {
start: formatDateToYYYYMMDDTHHMMSS(new Date(newEvent.start)), start: formatDateToYYYYMMDDTHHMMSS(weekStart),
end: formatDateToYYYYMMDDTHHMMSS( end: formatDateToYYYYMMDDTHHMMSS(weekEnd),
new Date(new Date(newEvent.start).getTime() + 86400000)
),
})) as Record<string, any>; })) as Record<string, any>;
const events: CalendarEvent[] = calEvents._embedded["dav:item"].flatMap( const events: CalendarEvent[] = calEvents._embedded["dav:item"].flatMap(
(eventdata: any) => { (eventdata: any) => {
@@ -291,7 +298,20 @@ const CalendarSlice = createSlice({
) )
.addCase(deleteEventAsync.fulfilled, (state, action) => { .addCase(deleteEventAsync.fulfilled, (state, action) => {
state.pending = false; state.pending = false;
delete state.list[action.payload.calId].events[action.payload.eventId]; 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
];
}
}) })
.addCase(getCalendarDetailAsync.pending, (state) => { .addCase(getCalendarDetailAsync.pending, (state) => {
state.pending = true; state.pending = true;
+33 -11
View File
@@ -124,7 +124,8 @@ export default function EventDisplayModal({
if (!event || !calendar) { if (!event || !calendar) {
onClose({}, "backdropClick"); onClose({}, "backdropClick");
} }
}, [open, eventId, dispatch, onClose]); setRepetition(event.repetition ?? ({} as RepetitionObject));
}, [open, eventId, dispatch, onClose, event]);
if (!event || !calendar) return null; if (!event || !calendar) return null;
@@ -161,6 +162,16 @@ export default function EventDisplayModal({
color: userPersonnalCalendars[calendarid]?.color, color: userPersonnalCalendars[calendarid]?.color,
}; };
const [baseId, recurrenceId] = event.uid.split("/");
if (recurrenceId) {
Object.keys(userPersonnalCalendars[calendarid].events).forEach(
(element) => {
if (element.split("/")[0] === baseId) {
dispatch(removeEvent({ calendarUid: calId, eventUid: element }));
}
}
);
}
await dispatch( await dispatch(
putEventAsync({ putEventAsync({
cal: userPersonnalCalendars[calendarid], cal: userPersonnalCalendars[calendarid],
@@ -181,13 +192,7 @@ export default function EventDisplayModal({
onClose({}, "backdropClick"); onClose({}, "backdropClick");
}; };
const [detailsLoaded, setDetailsLoaded] = useState(false);
const handleToggleShowMore = async () => { const handleToggleShowMore = async () => {
if (!detailsLoaded) {
await dispatch(getEventAsync(event));
setDetailsLoaded(true);
}
setShowMore(!showMore); setShowMore(!showMore);
}; };
@@ -224,17 +229,30 @@ export default function EventDisplayModal({
return ( return (
<Modal open={open} onClose={onClose}> <Modal open={open} onClose={onClose}>
<Card sx={{ minWidth: 300, width: "50vw", p: 2, position: "absolute" }}> <Box
sx={{
position: "absolute",
top: "5vh",
left: "50%",
transform: "translate(-50%, -50%)",
width: "50vw",
maxHeight: "80vh",
}}
>
<Card sx={{ p: 2, position: "absolute" }}>
{/* Close button */} {/* Close button */}
<Box sx={{ position: "absolute", top: 8, right: 8 }}> <Box sx={{ position: "absolute", top: 8, right: 8 }}>
<IconButton size="small" onClick={() => onClose({}, "backdropClick")}> <IconButton
size="small"
onClick={() => onClose({}, "backdropClick")}
>
<CloseIcon fontSize="small" /> <CloseIcon fontSize="small" />
</IconButton> </IconButton>
</Box> </Box>
<CardHeader title={isOwn ? "Edit Event" : "Event Details"} /> <CardHeader title={isOwn ? "Edit Event" : "Event Details"} />
<CardContent sx={{ overflow: "auto" }}> <CardContent sx={{ maxHeight: "75vh", overflow: "auto" }}>
{/* Title */} {/* Title */}
<TextField <TextField
fullWidth fullWidth
@@ -448,6 +466,7 @@ export default function EventDisplayModal({
<> <>
<RepeatEvent <RepeatEvent
repetition={repetition} repetition={repetition}
eventStart={event.start}
setRepetition={setRepetition} setRepetition={setRepetition}
isOwn={isOwn} isOwn={isOwn}
/> />
@@ -458,7 +477,9 @@ export default function EventDisplayModal({
labelId="alarm" labelId="alarm"
value={alarm} value={alarm}
disabled={!isOwn} disabled={!isOwn}
onChange={(e: SelectChangeEvent) => setAlarm(e.target.value)} onChange={(e: SelectChangeEvent) =>
setAlarm(e.target.value)
}
> >
<MenuItem value={""}>No Alarm</MenuItem> <MenuItem value={""}>No Alarm</MenuItem>
<MenuItem value={"-PT1M"}>1 minute</MenuItem> <MenuItem value={"-PT1M"}>1 minute</MenuItem>
@@ -533,6 +554,7 @@ export default function EventDisplayModal({
</ButtonGroup> </ButtonGroup>
</CardActions> </CardActions>
</Card> </Card>
</Box>
</Modal> </Modal>
); );
} }
@@ -136,6 +136,7 @@ export default function EventPreviewModal({
size="small" size="small"
onClick={async () => { onClick={async () => {
setOpenFullDisplay(!openFullDisplay); setOpenFullDisplay(!openFullDisplay);
await dispatch(getEventAsync(event));
}} }}
> >
<EditIcon fontSize="small" /> <EditIcon fontSize="small" />
+1
View File
@@ -284,6 +284,7 @@ function EventPopover({
<> <>
<RepeatEvent <RepeatEvent
repetition={repetition} repetition={repetition}
eventStart={selectedRange?.start ?? new Date()}
setRepetition={setRepetition} setRepetition={setRepetition}
/> />
<FormControl fullWidth margin="dense" size="small"> <FormControl fullWidth margin="dense" size="small">
+2 -2
View File
@@ -88,10 +88,10 @@ export function parseCalendarEvent(
event.repetition.selectedDays = value.byday; event.repetition.selectedDays = value.byday;
} }
if (value.until) { if (value.until) {
event.repetition.selectedDays = value.endDate; event.repetition.endDate = value.until;
} }
if (value.count) { if (value.count) {
event.repetition.selectedDays = value.occurrences; event.repetition.occurrences = value.count;
} }
if (value.interval) { if (value.interval) {
event.repetition.interval = value.interval; event.repetition.interval = value.interval;