Merge pull request #102 from linagora/72-event-action-duplicate-event

72 event action duplicate event
This commit is contained in:
Camille Moussu
2025-09-11 18:29:58 +02:00
committed by GitHub
6 changed files with 261 additions and 29 deletions
+176
View File
@@ -0,0 +1,176 @@
import { fireEvent, screen } from "@testing-library/react";
import EventDuplication from "../../src/components/Event/EventDuplicate";
import EventDisplayModal from "../../src/features/Events/EventDisplay";
import EventPopover from "../../src/features/Events/EventModal";
import { renderWithProviders } from "../utils/Renderwithproviders";
import EventPreviewModal from "../../src/features/Events/EventDisplayPreview";
const day = new Date();
const preloadedState = {
user: {
userData: {
sub: "test",
email: "test@test.com",
sid: "aiYbWZSk2g0F+LrQeD7Dg4QcUMR8R/zTZdZBiA7N6Ro",
openpaasId: "667037022b752d0026472254",
},
organiserData: {
cn: "test",
cal_address: "mailto:test@test.com",
},
},
calendars: {
list: {
"667037022b752d0026472254/cal1": {
id: "667037022b752d0026472254/cal1",
name: "Calendar",
color: "#FF0000",
events: {
event1: {
uid: "event1",
URL: "calendars/667037022b752d0026472254/cal1/event1.ics",
title: "Test Event",
calId: "667037022b752d0026472254/cal1",
start: day,
end: day,
timezone: "UTC",
organizer: { cn: "test", cal_address: "test@test.com" },
attendee: [
{
cn: "test",
cal_address: "test@test.com",
partstat: "NEEDS-ACTION",
rsvp: "TRUE",
role: "REQ-PARTICIPANT",
cutype: "INDIVIDUAL",
},
{
cn: "John",
cal_address: "john@test.com",
partstat: "NEEDS-ACTION",
rsvp: "TRUE",
role: "REQ-PARTICIPANT",
cutype: "INDIVIDUAL",
},
],
},
},
},
},
pending: false,
},
};
describe("EventDuplication", () => {
it("opens EventPopover when button clicked", () => {
const handleClose = jest.fn();
renderWithProviders(
<EventDuplication
event={
preloadedState.calendars.list["667037022b752d0026472254/cal1"].events
.event1
}
onClose={handleClose}
/>,
preloadedState
);
const button = screen.getByRole("button");
fireEvent.click(button);
expect(screen.getByText(/Duplicate Event/i)).toBeInTheDocument();
});
it("calls onClose when closing the popover", () => {
const handleClose = jest.fn();
renderWithProviders(
<EventDuplication
event={
preloadedState.calendars.list["667037022b752d0026472254/cal1"].events
.event1
}
onClose={handleClose}
/>,
preloadedState
);
fireEvent.click(screen.getByRole("button"));
fireEvent.click(screen.getByRole("button", { name: /Cancel/i }));
expect(handleClose).toHaveBeenCalled();
});
});
describe("EventPopover", () => {
it("renders with event data", () => {
renderWithProviders(
<EventPopover
anchorEl={null}
open={true}
onClose={jest.fn()}
selectedRange={null}
setSelectedRange={jest.fn()}
calendarRef={{ current: null }}
event={
preloadedState.calendars.list["667037022b752d0026472254/cal1"].events
.event1
}
/>,
preloadedState
);
expect(screen.getByDisplayValue(/Test Event/i)).toBeInTheDocument();
});
it("saves duplicated event when Save is clicked", () => {
const onClose = jest.fn();
renderWithProviders(
<EventPopover
anchorEl={null}
open={true}
onClose={onClose}
selectedRange={null}
setSelectedRange={jest.fn()}
calendarRef={{ current: null }}
event={
preloadedState.calendars.list["667037022b752d0026472254/cal1"].events
.event1
}
/>,
preloadedState
);
fireEvent.change(screen.getByLabelText(/Title/i), {
target: { value: "Duplicated Event" },
});
fireEvent.click(screen.getByRole("button", { name: /Save/i }));
expect(onClose).toHaveBeenCalled();
});
});
describe("EventDisplayModal", () => {
it("shows duplication button and opens duplication form", () => {
renderWithProviders(
<EventPreviewModal
eventId="event1"
calId="667037022b752d0026472254/cal1"
open={true}
onClose={jest.fn()}
anchorPosition={{ top: 0, left: 0 }}
/>,
preloadedState
);
fireEvent.click(screen.getByTestId("AddToPhotosIcon"));
expect(screen.getByText(/Duplicate Event/i)).toBeInTheDocument();
expect(
screen.getByDisplayValue(
preloadedState.calendars.list["667037022b752d0026472254/cal1"].events
.event1.title
)
).toBeInTheDocument();
});
});
+2 -14
View File
@@ -1137,13 +1137,7 @@ describe("Event Full Display", () => {
const day = new Date(); const day = new Date();
const preloadedTwoCals = { const preloadedTwoCals = {
user: { ...preloadedState,
userData: {
sub: "test",
email: "test@test.com",
openpaasId: "667037022b752d0026472254",
},
},
calendars: { calendars: {
list: { list: {
"667037022b752d0026472254/cal1": { "667037022b752d0026472254/cal1": {
@@ -1210,13 +1204,7 @@ describe("Event Full Display", () => {
const day = new Date(); const day = new Date();
const preloadedRecurrence = { const preloadedRecurrence = {
user: { ...preloadedState,
userData: {
sub: "test",
email: "test@test.com",
openpaasId: "667037022b752d0026472254",
},
},
calendars: { calendars: {
list: { list: {
"667037022b752d0026472254/cal1": { "667037022b752d0026472254/cal1": {
+52
View File
@@ -0,0 +1,52 @@
import { IconButton } from "@mui/material";
import { CalendarEvent } from "../../features/Events/EventsTypes";
import AddToPhotosIcon from "@mui/icons-material/AddToPhotos";
import { useRef, useState } from "react";
import EventPopover from "../../features/Events/EventModal";
import { CalendarApi, DateSelectArg } from "@fullcalendar/core";
export default function EventDuplication({
onClose,
event,
}: {
onClose: Function;
event: CalendarEvent;
}) {
const [openModal, setOpenModal] = useState(false);
const [selectedRange, setSelectedRange] = useState<DateSelectArg | null>({
start: new Date(event.start),
startStr: new Date(event.start).toISOString(),
end: new Date(event.end ?? ""),
endStr: new Date(event.end ?? "").toISOString(),
allDay: event.allday ?? false,
} as DateSelectArg);
const calendarRef = useRef<CalendarApi | null>(null);
const handleClosePopover = () => {
calendarRef.current?.unselect();
setSelectedRange(null);
setOpenModal(!openModal);
onClose({}, "backdropClick");
};
return (
<>
<IconButton
size="small"
onClick={() => {
setOpenModal(true);
}}
>
<AddToPhotosIcon fontSize="small" />
</IconButton>
<EventPopover
anchorEl={null}
open={openModal}
selectedRange={selectedRange}
setSelectedRange={setSelectedRange}
calendarRef={calendarRef}
onClose={handleClosePopover}
event={event}
/>
</>
);
}
@@ -39,6 +39,7 @@ import EventDisplayModal, {
} from "./EventDisplay"; } from "./EventDisplay";
import { getEvent } from "./EventApi"; import { getEvent } from "./EventApi";
import { CalendarEvent } from "./EventsTypes"; import { CalendarEvent } from "./EventsTypes";
import EventDuplication from "../../components/Event/EventDuplicate";
export default function EventPreviewModal({ export default function EventPreviewModal({
eventId, eventId,
@@ -122,6 +123,7 @@ export default function EventPreviewModal({
gap: 1, gap: 1,
}} }}
> >
<EventDuplication event={event} onClose={onClose} />
{mailSpaUrl && attendees.length > 0 && ( {mailSpaUrl && attendees.length > 0 && (
<IconButton <IconButton
size="small" size="small"
+28 -14
View File
@@ -33,6 +33,7 @@ function EventPopover({
selectedRange, selectedRange,
setSelectedRange, setSelectedRange,
calendarRef, calendarRef,
event,
}: { }: {
anchorEl: HTMLElement | null; anchorEl: HTMLElement | null;
open: boolean; open: boolean;
@@ -40,6 +41,7 @@ function EventPopover({
selectedRange: DateSelectArg | null; selectedRange: DateSelectArg | null;
setSelectedRange: Function; setSelectedRange: Function;
calendarRef: React.RefObject<CalendarApi | null>; calendarRef: React.RefObject<CalendarApi | null>;
event?: CalendarEvent;
}) { }) {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@@ -63,20 +65,32 @@ function EventPopover({
const timezones = TIMEZONES.aliases; const timezones = TIMEZONES.aliases;
const [showMore, setShowMore] = useState(false); const [showMore, setShowMore] = useState(false);
const [title, setTitle] = useState(""); const [title, setTitle] = useState(event?.title ?? "");
const [description, setDescription] = useState(""); const [description, setDescription] = useState(event?.description ?? "");
const [location, setLocation] = useState(""); const [location, setLocation] = useState(event?.location ?? "");
const [start, setStart] = useState(""); const [start, setStart] = useState(
const [end, setEnd] = useState(""); event?.start ? new Date(event.start).toISOString() : ""
const [calendarid, setCalendarid] = useState(0);
const [allday, setAllDay] = useState(false);
const [repetition, setRepetition] = useState<RepetitionObject>(
{} as RepetitionObject
); );
const [attendees, setAttendees] = useState<userAttendee[]>([]); const [end, setEnd] = useState(
const [alarm, setAlarm] = useState(""); event?.end ? new Date(event.end)?.toISOString() : ""
const [eventClass, setEventClass] = useState("PUBLIC"); );
const [busy, setBusy] = useState("OPAQUE"); const [calendarid, setCalendarid] = useState(
event?.calId
? userPersonnalCalendars.findIndex((e) => e.id === event?.calId)
: 0
);
const [allday, setAllDay] = useState(event?.allday ?? false);
const [repetition, setRepetition] = useState<RepetitionObject>(
event?.repetition ?? ({} as RepetitionObject)
);
const [attendees, setAttendees] = useState<userAttendee[]>(
event?.attendee
? event.attendee.filter((a) => a.cal_address !== organizer.cal_address)
: []
);
const [alarm, setAlarm] = useState(event?.alarm?.trigger ?? "");
const [eventClass, setEventClass] = useState(event?.class ?? "PUBLIC");
const [busy, setBusy] = useState(event?.transp ?? "OPAQUE");
const [timezone, setTimezone] = useState( const [timezone, setTimezone] = useState(
Intl.DateTimeFormat().resolvedOptions().timeZone Intl.DateTimeFormat().resolvedOptions().timeZone
@@ -157,7 +171,7 @@ function EventPopover({
}} }}
> >
<Card> <Card>
<CardHeader title="Create Event" /> <CardHeader title={event ? "Duplicate Event" : "Create Event"} />
<CardContent <CardContent
sx={{ maxHeight: "85vh", maxWidth: "40vw", overflow: "auto" }} sx={{ maxHeight: "85vh", maxWidth: "40vw", overflow: "auto" }}
> >
+1 -1
View File
@@ -22,7 +22,7 @@ export interface CalendarEvent {
status?: string; status?: string;
timezone: string; timezone: string;
repetition?: RepetitionObject; repetition?: RepetitionObject;
alarm: AlarmObject; alarm?: AlarmObject;
} }
export interface RepetitionObject { export interface RepetitionObject {