diff --git a/__test__/components/RepeatEvent.test.tsx b/__test__/components/RepeatEvent.test.tsx new file mode 100644 index 0000000..0a89775 --- /dev/null +++ b/__test__/components/RepeatEvent.test.tsx @@ -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, state?: any) { + const setRepetition = jest.fn(); + renderWithProviders( + , + 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( + , + 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"); + }); +}); diff --git a/__test__/features/Events/EventDisplay.test.tsx b/__test__/features/Events/EventDisplay.test.tsx index d69b891..d878e95 100644 --- a/__test__/features/Events/EventDisplay.test.tsx +++ b/__test__/features/Events/EventDisplay.test.tsx @@ -474,6 +474,37 @@ describe("Event Preview Display", () => { const updatedEvent = spy.mock.calls[0][0].newEvent; 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( + , + preloadedState + ); + + fireEvent.click(screen.getByTestId("EditIcon")); + + await waitFor(() => { + expect(spy).toHaveBeenCalled(); + }); + expect(screen.getByText("Edit Event")).toBeInTheDocument(); + }); }); describe("Event Full Display", () => { @@ -917,18 +948,6 @@ describe("Event Full Display", () => { expect(updatedEvent.attendee[0].partstat).toBe("DECLINED"); }); 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( { fireEvent.click(screen.getByText("Show More")); }); - await waitFor(() => { - expect(spy).toHaveBeenCalled(); - }); - console.log(spy); await waitFor(() => { expect(screen.getByLabelText(/Alarm/i)).toBeInTheDocument(); expect(screen.getByLabelText(/Repetition/i)).toBeInTheDocument(); diff --git a/__test__/features/Events/EventModal.test.tsx b/__test__/features/Events/EventModal.test.tsx index 9016a34..c8d0db2 100644 --- a/__test__/features/Events/EventModal.test.tsx +++ b/__test__/features/Events/EventModal.test.tsx @@ -286,7 +286,6 @@ describe("EventPopover", () => { preloadedState.calendars.list["667037022b752d0026472254/cal1"] ); - console.log(receivedPayload.newEvent.start); expect(receivedPayload.newEvent.title).toBe(newEvent.title); expect(receivedPayload.newEvent.description).toBe(newEvent.description); expect( diff --git a/__test__/features/Events/eventUtils.test.ts b/__test__/features/Events/eventUtils.test.ts index 145af5e..2807740 100644 --- a/__test__/features/Events/eventUtils.test.ts +++ b/__test__/features/Events/eventUtils.test.ts @@ -280,7 +280,7 @@ describe("calendarEventToJCal", () => { allday: true, location: "Room 101", description: "Discuss project roadmap.", - repetition: { freq: "WEEKLY" }, + repetition: { freq: "WEEKLY", interval: 2 }, organizer: { cn: "Alice", cal_address: "alice@example.com", @@ -315,7 +315,7 @@ describe("calendarEventToJCal", () => { ["location", {}, "text", "Room 101"], ["description", {}, "text", "Discuss project roadmap."], ["x-openpaas-videoconference", {}, "unknown", null], - ["rrule", {}, "recur", { freq: "WEEKLY" }], + ["rrule", {}, "recur", { freq: "WEEKLY", interval: 2 }], [ "organizer", { cn: "Alice" }, diff --git a/src/components/Attendees/AttendeeSearch.tsx b/src/components/Attendees/AttendeeSearch.tsx index 44636b5..641579f 100644 --- a/src/components/Attendees/AttendeeSearch.tsx +++ b/src/components/Attendees/AttendeeSearch.tsx @@ -32,10 +32,12 @@ export default function UserSearch({ useEffect(() => { const delayDebounceFn = setTimeout(async () => { - setLoading(true); - const res = await searchUsers(query); - setOptions(res); - setLoading(false); + if (query) { + setLoading(true); + const res = await searchUsers(query); + setOptions(res); + setLoading(false); + } }, 300); return () => clearTimeout(delayDebounceFn); diff --git a/src/components/Calendar/Calendar.tsx b/src/components/Calendar/Calendar.tsx index e1fec6e..d8a631a 100644 --- a/src/components/Calendar/Calendar.tsx +++ b/src/components/Calendar/Calendar.tsx @@ -14,6 +14,7 @@ import { CalendarEvent } from "../../features/Events/EventsTypes"; import CalendarSelection from "./CalendarSelection"; import { getCalendarDetailAsync, + getEventAsync, putEventAsync, updateEventLocal, } from "../../features/Calendars/CalendarSlice"; @@ -326,11 +327,21 @@ export default function CalendarApp() { setEventDisplayedCalId(info.event.extendedProps.calId); } }} + eventAllow={(dropInfo, draggedEvent) => { + if ( + draggedEvent?.extendedProps.uid && + draggedEvent.extendedProps.uid.split("/")[1] + ) { + return false; + } + return true; + }} eventDrop={(arg) => { const event = calendars[arg.event._def.extendedProps.calId].events[ arg.event._def.extendedProps.uid ]; + const totalDeltaMs = getDeltaInMilliseconds(arg.delta); const originalStart = new Date(event.start); @@ -358,7 +369,9 @@ export default function CalendarApp() { calendars[arg.event._def.extendedProps.calId].events[ arg.event._def.extendedProps.uid ]; - + if (event.uid.split("/")[1]) { + dispatch(getEventAsync(event)); + } const originalStart = new Date(event.start); const computedNewStart = new Date( originalStart.getTime() + getDeltaInMilliseconds(arg.startDelta) @@ -372,7 +385,7 @@ export default function CalendarApp() { start: computedNewStart, end: computedNewEnd, } as CalendarEvent; - console.log(event , newEvent); + console.log(event, newEvent); dispatch( putEventAsync({ cal: calendars[newEvent.calId], newEvent }) ); diff --git a/src/components/Event/EventRepeat.tsx b/src/components/Event/EventRepeat.tsx index 433fc2f..8439955 100644 --- a/src/components/Event/EventRepeat.tsx +++ b/src/components/Event/EventRepeat.tsx @@ -6,46 +6,56 @@ import { MenuItem, Box, Stack, - Paper, Typography, TextField, Checkbox, - List, - ListItem, FormControlLabel, FormGroup, Radio, RadioGroup, } from "@mui/material"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { RepetitionObject } from "../../features/Events/EventsTypes"; export default function RepeatEvent({ repetition, + eventStart, setRepetition, isOwn = true, }: { repetition: RepetitionObject; + eventStart: Date; setRepetition: Function; isOwn?: boolean; }) { - console.log(JSON.stringify(repetition)); - const repetitionValues = ["day", "week", "month", "year"]; - const [interval, setInterval] = useState(repetition.interval ?? 0); - const [selectedDays, setSelectedDays] = useState( - 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 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) => { - setSelectedDays((prev: string[]) => - prev.includes(day) ? prev.filter((d) => d !== day) : [...prev, day] - ); + const updatedDays = repetition.selectedDays?.includes(day) + ? repetition.selectedDays.filter((d) => d !== day) + : [...(repetition.selectedDays ?? []), day]; + + setRepetition({ ...repetition, selectedDays: updatedDays }); }; + return ( Repetition @@ -54,9 +64,17 @@ export default function RepeatEvent({ value={repetition.freq ?? ""} disabled={!isOwn} label="Repetition" - onChange={(e: SelectChangeEvent) => - setRepetition({ ...repetition, freq: e.target.value }) - } + onChange={(e: SelectChangeEvent) => { + if (e.target.value === "weekly") { + setRepetition({ + ...repetition, + freq: e.target.value, + selectedDays: [days[day.getDay() - 1]], + }); + } else { + setRepetition({ ...repetition, freq: e.target.value }); + } + }} > No Repetition Repeat daily @@ -64,18 +82,24 @@ export default function RepeatEvent({ Repeat monthly Repeat yearly + {repetition.freq && ( + {/* Interval */} Interval: setInterval(Number(e.target.value))} + value={repetition.interval ?? 1} + onChange={(e) => + setRepetition({ + ...repetition, + interval: Number(e.target.value), + }) + } size="small" sx={{ width: 80 }} /> - { repetitionValues[ @@ -84,6 +108,8 @@ export default function RepeatEvent({ } + + {/* Weekly selection */} {repetition.freq === "weekly" && ( @@ -95,7 +121,9 @@ export default function RepeatEvent({ key={day} control={ handleDayChange(day)} /> } @@ -105,13 +133,36 @@ export default function RepeatEvent({ )} + + {/* End options */} End: 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), + }); + } + }} > setOccurrences(Number(e.target.value))} + value={repetition.occurrences ?? 0} + onChange={(e) => + setRepetition({ + ...repetition, + endDate: "", + occurrences: Number(e.target.value), + }) + } sx={{ width: 100 }} inputProps={{ min: 1 }} disabled={endOption !== "after"} @@ -147,9 +204,16 @@ export default function RepeatEvent({ On setEndDate(e.target.value)} + value={repetition.endDate ?? ""} + onChange={(e) => + setRepetition({ + ...repetition, + occurrences: 0, + endDate: e.target.value, + }) + } disabled={endOption !== "on"} /> diff --git a/src/features/Calendars/CalendarSlice.ts b/src/features/Calendars/CalendarSlice.ts index dca589e..e8273fe 100644 --- a/src/features/Calendars/CalendarSlice.ts +++ b/src/features/Calendars/CalendarSlice.ts @@ -71,11 +71,18 @@ export const putEventAsync = createAsyncThunk< { cal: Calendars; newEvent: CalendarEvent } // Arg type >("calendars/putEvent", async ({ cal, 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, { - start: formatDateToYYYYMMDDTHHMMSS(new Date(newEvent.start)), - end: formatDateToYYYYMMDDTHHMMSS( - new Date(new Date(newEvent.start).getTime() + 86400000) - ), + start: formatDateToYYYYMMDDTHHMMSS(weekStart), + end: formatDateToYYYYMMDDTHHMMSS(weekEnd), })) as Record; const events: CalendarEvent[] = calEvents._embedded["dav:item"].flatMap( (eventdata: any) => { @@ -291,7 +298,20 @@ const CalendarSlice = createSlice({ ) .addCase(deleteEventAsync.fulfilled, (state, action) => { 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) => { state.pending = true; diff --git a/src/features/Events/EventDisplay.tsx b/src/features/Events/EventDisplay.tsx index d716b34..e6c9ccd 100644 --- a/src/features/Events/EventDisplay.tsx +++ b/src/features/Events/EventDisplay.tsx @@ -124,7 +124,8 @@ export default function EventDisplayModal({ if (!event || !calendar) { onClose({}, "backdropClick"); } - }, [open, eventId, dispatch, onClose]); + setRepetition(event.repetition ?? ({} as RepetitionObject)); + }, [open, eventId, dispatch, onClose, event]); if (!event || !calendar) return null; @@ -161,6 +162,16 @@ export default function EventDisplayModal({ 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( putEventAsync({ cal: userPersonnalCalendars[calendarid], @@ -181,13 +192,7 @@ export default function EventDisplayModal({ onClose({}, "backdropClick"); }; - const [detailsLoaded, setDetailsLoaded] = useState(false); - const handleToggleShowMore = async () => { - if (!detailsLoaded) { - await dispatch(getEventAsync(event)); - setDetailsLoaded(true); - } setShowMore(!showMore); }; @@ -224,315 +229,332 @@ export default function EventDisplayModal({ return ( - - {/* Close button */} - - onClose({}, "backdropClick")}> - - - - - - - - {/* Title */} - setTitle(e.target.value)} - size="small" - margin="dense" - /> - - {/* RSVP */} - {currentUserAttendee && isOwnCal && ( - - - - - - - - - )} - - {/* Calendar selector */} - - Calendar - - + + + - {/* Dates */} - - setStart(formatLocalDateTime(new Date(e.target.value))) - } - size="small" - margin="dense" - InputLabelProps={{ shrink: true }} - /> + - - setEnd(formatLocalDateTime(new Date(e.target.value))) - } - size="small" - margin="dense" - InputLabelProps={{ shrink: true }} - /> + + {/* Title */} + setTitle(e.target.value)} + size="small" + margin="dense" + /> - + + + + + + + + )} + + {/* Calendar selector */} + + Calendar + setAlarm(e.target.value)} - > - No Alarm - 1 minute - 2 minutes - 10 minutes - 15 minutes - 30 minutes - 1 hours - 2 hours - 5 hours - 12 hours - 1 day - 2 days - 1 week - - - - - Visibility - - - {/* Error */} - {event.error && ( - - } - text={event.error} - error - /> - )} - - )} - - - - - {isOwn && ( - { - onClose({}, "backdropClick"); - dispatch( - deleteEventAsync({ calId, eventId, eventURL: event.URL }) - ); + labelId="calendar-select-label" + value={calendarid.toString()} + label="Calendar" + onChange={(e: SelectChangeEvent) => { + const newId = Number(e.target.value); + setCalendarid(newId); + setNewCalId(userPersonnalCalendars[newId].id); }} > - - - )} - + {calList} + + + + {/* Dates */} + + setStart(formatLocalDateTime(new Date(e.target.value))) + } + size="small" + margin="dense" + InputLabelProps={{ shrink: true }} + /> + + + setEnd(formatLocalDateTime(new Date(e.target.value))) + } + size="small" + margin="dense" + InputLabelProps={{ shrink: true }} + /> + + { + const endDate = new Date(end); + const startDate = new Date(start); + setAllDay(!allday); + if (endDate.getDate() === startDate.getDate()) { + endDate.setDate(startDate.getDate() + 1); + setEnd(formatLocalDateTime(endDate)); + } + }} + /> + } + label="All day" + /> + + {/* Description & Location */} + setDescription(e.target.value)} + size="small" + margin="dense" + multiline + rows={2} + /> {isOwn && ( - + { + const newAttendeeList = attendees.concat(value); + setAttendees(newAttendeeList); + }} + /> )} - - - + + setLocation(e.target.value)} + size="small" + margin="dense" + /> + + {/* Video */} + {event.x_openpass_videoconference && ( + } + text="Video conference available" + data={event.x_openpass_videoconference} + /> + )} + + {/* Attendees */} + {event.attendee?.length > 0 && ( + + Attendees: + {organizer.cal_address && + renderAttendeeBadge(organizer, "org", true)} + {(showAllAttendees + ? attendees + : attendees.slice(0, attendeeDisplayLimit) + ).map((a, idx) => ( + + {renderAttendeeBadge(a, idx.toString())} + {isOwn && ( + { + const newAttendeesList = [...attendees]; + newAttendeesList.splice(idx, 1); + setAttendees(newAttendeesList); + }} + > + + + )} + + ))} + {attendees.length > attendeeDisplayLimit && ( + setShowAllAttendees(!showAllAttendees)} + > + {showAllAttendees + ? "Show less" + : `Show more (${ + attendees.length - attendeeDisplayLimit + } more)`} + + )} + + )} + + + + {/* Extended options */} + {showMore && ( + <> + + + + Alarm + + + + + Visibility + + + {/* Error */} + {event.error && ( + + } + text={event.error} + error + /> + )} + + )} + + + + + {isOwn && ( + { + onClose({}, "backdropClick"); + dispatch( + deleteEventAsync({ calId, eventId, eventURL: event.URL }) + ); + }} + > + + + )} + + + {isOwn && ( + + )} + + + + ); } diff --git a/src/features/Events/EventDisplayPreview.tsx b/src/features/Events/EventDisplayPreview.tsx index 3f9a220..a70fb77 100644 --- a/src/features/Events/EventDisplayPreview.tsx +++ b/src/features/Events/EventDisplayPreview.tsx @@ -136,6 +136,7 @@ export default function EventPreviewModal({ size="small" onClick={async () => { setOpenFullDisplay(!openFullDisplay); + await dispatch(getEventAsync(event)); }} > diff --git a/src/features/Events/EventModal.tsx b/src/features/Events/EventModal.tsx index 4720a34..9b2fbd6 100644 --- a/src/features/Events/EventModal.tsx +++ b/src/features/Events/EventModal.tsx @@ -284,6 +284,7 @@ function EventPopover({ <> diff --git a/src/features/Events/eventUtils.ts b/src/features/Events/eventUtils.ts index 088eb0f..0e19bab 100644 --- a/src/features/Events/eventUtils.ts +++ b/src/features/Events/eventUtils.ts @@ -88,10 +88,10 @@ export function parseCalendarEvent( event.repetition.selectedDays = value.byday; } if (value.until) { - event.repetition.selectedDays = value.endDate; + event.repetition.endDate = value.until; } if (value.count) { - event.repetition.selectedDays = value.occurrences; + event.repetition.occurrences = value.count; } if (value.interval) { event.repetition.interval = value.interval;