diff --git a/__test__/features/Events/EventModal.test.tsx b/__test__/features/Events/EventModal.test.tsx index 0fb59ab..57a19b6 100644 --- a/__test__/features/Events/EventModal.test.tsx +++ b/__test__/features/Events/EventModal.test.tsx @@ -1,20 +1,20 @@ -import { screen, fireEvent, waitFor } from "@testing-library/react"; -import * as eventThunks from "../../../src/features/Calendars/CalendarSlice"; -import { renderWithProviders } from "../../utils/Renderwithproviders"; -import EventPopover from "../../../src/features/Events/EventModal"; import { DateSelectArg } from "@fullcalendar/core"; +import { act, fireEvent, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import preview from "jest-preview"; +import * as eventThunks from "../../../src/features/Calendars/CalendarSlice"; +import EventPopover from "../../../src/features/Events/EventModal"; +import { api } from "../../../src/utils/apiUtils"; import { formatDateToYYYYMMDDTHHMMSS } from "../../../src/utils/dateUtils"; +import { renderWithProviders } from "../../utils/Renderwithproviders"; + +jest.mock("../../../src/utils/apiUtils"); describe("EventPopover", () => { const mockOnClose = jest.fn(); const mockSetSelectedRange = jest.fn(); const mockCalendarRef = { current: { select: jest.fn() } } as any; - beforeEach(() => { - jest.clearAllMocks(); - }); - const preloadedState = { user: { userData: { @@ -25,7 +25,7 @@ describe("EventPopover", () => { }, organiserData: { cn: "test", - cal_address: "mailto:test@test.com", + cal_address: "test@test.com", }, }, calendars: { @@ -45,6 +45,44 @@ describe("EventPopover", () => { }, }; + const mockUsers = [ + { + id: "john@example.com", + objectType: "user", + emailAddresses: [ + { + value: "john@example.com", + type: "default", + }, + ], + names: [ + { + displayName: "John Doe", + type: "default", + }, + ], + }, + { + id: "jane@example.com", + objectType: "user", + emailAddresses: [ + { + value: "jane@example.com", + type: "default", + }, + ], + names: [ + { + displayName: "Jane Smith", + type: "default", + }, + ], + }, + ]; + (api.post as jest.Mock).mockReturnValue({ + json: jest.fn().mockResolvedValue(mockUsers), + }); + const defaultSelectedRange = { startStr: "2025-07-18T09:00", endStr: "2025-07-18T10:00", @@ -130,6 +168,69 @@ describe("EventPopover", () => { expect(screen.getAllByRole("combobox")[0]).toHaveTextContent("Calendar 2"); }); + it("adds a attendee", async () => { + jest.useFakeTimers(); + renderPopover(); + fireEvent.change(screen.getByLabelText("Title"), { + target: { value: "newEvent" }, + }); + const select = screen.getByLabelText("Search user"); + + act(() => { + select.focus(); + fireEvent.mouseDown(select); + userEvent.type(select, "john"); + }); + await act(async () => { + jest.advanceTimersByTime(400); + }); + await waitFor(() => expect(api.post).toHaveBeenCalledTimes(1)); + + await waitFor(() => { + expect(screen.getByText("John Doe")).toBeInTheDocument(); + }); + await act(async () => { + userEvent.click(screen.getByText("John Doe")); + }); + + const spy = jest + .spyOn(eventThunks, "putEventAsync") + .mockImplementation((payload) => { + return () => Promise.resolve(payload) as any; + }); + + fireEvent.click(screen.getByText("Save")); + + await waitFor(() => { + expect(spy).toHaveBeenCalled(); + }); + + const receivedPayload = spy.mock.calls[0][0]; + + expect(receivedPayload.cal).toEqual( + preloadedState.calendars.list["667037022b752d0026472254/cal1"] + ); + + expect(receivedPayload.newEvent.attendee).toHaveLength(2); + expect(receivedPayload.newEvent.attendee).toStrictEqual([ + { + cn: "test", + cal_address: "test@test.com", + partstat: "ACCEPTED", + rsvp: "FALSE", + role: "CHAIR", + cutype: "INDIVIDUAL", + }, + { + cn: "John Doe", + cal_address: "john@example.com", + partstat: "NEED_ACTION", + rsvp: "FALSE", + role: "CHAIR", + cutype: "INDIVIDUAL", + }, + ]); + }); it("dispatches putEventAsync and calls onClose when Save is clicked", async () => { renderPopover(); @@ -142,7 +243,7 @@ describe("EventPopover", () => { description: "Discuss project", location: "Zoom", repetition: "", - organizer: { cn: "test", cal_address: "mailto:test@test.com" }, + organizer: { cn: "test", cal_address: "test@test.com" }, timezone: "Europe/Paris", transp: "OPAQUE", }; diff --git a/__test__/features/user/userAPI.test.tsx b/__test__/features/user/userAPI.test.tsx index 753a0c6..4739af2 100644 --- a/__test__/features/user/userAPI.test.tsx +++ b/__test__/features/user/userAPI.test.tsx @@ -1,5 +1,5 @@ import { clientConfig } from "../../../src/features/User/oidcAuth"; -import getOpenPaasUserId from "../../../src/features/User/userAPI"; +import getOpenPaasUser from "../../../src/features/User/userAPI"; import { api } from "../../../src/utils/apiUtils"; jest.mock("../../../src/utils/apiUtils"); @@ -14,7 +14,7 @@ describe("getOpenPaasUserId", () => { json: jest.fn().mockResolvedValue(mockUser), }); - const result = await getOpenPaasUserId(); + const result = await getOpenPaasUser(); expect(api.get).toHaveBeenCalledWith("api/user"); expect(result).toEqual(mockUser); diff --git a/src/components/Attendees/AttendeeSearch.tsx b/src/components/Attendees/AttendeeSearch.tsx new file mode 100644 index 0000000..6e17588 --- /dev/null +++ b/src/components/Attendees/AttendeeSearch.tsx @@ -0,0 +1,88 @@ +import { + Autocomplete, + Avatar, + CircularProgress, + ListItem, + ListItemAvatar, + ListItemText, + TextField, +} from "@mui/material"; +import { useEffect, useState } from "react"; +import { searchUsers } from "../../features/User/userAPI"; + +interface User { + email: string; + displayName: string; + avatarUrl: string; +} + +export default function UserSearch({ + setAttendees, +}: { + setAttendees: Function; +}) { + const [query, setQuery] = useState(""); + const [options, setOptions] = useState([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + const delayDebounceFn = setTimeout(async () => { + setLoading(true); + const res = await searchUsers(query); + setOptions(res); + setLoading(false); + }, 300); + + return () => clearTimeout(delayDebounceFn); + }, [query]); + + return ( + x} + fullWidth + getOptionLabel={(option) => option.displayName || option.email} + filterSelectedOptions + onInputChange={(event, value) => setQuery(value)} + onChange={(event, value) => + setAttendees( + value.map((a) => ({ + cn: a.displayName, + cal_address: a.email, + partstat: "NEED_ACTION", + rsvp: "FALSE", + role: "CHAIR", + cutype: "INDIVIDUAL", + })) + ) + } + renderInput={(params) => ( + + {loading ? ( + + ) : null} + {params.InputProps.endAdornment} + + ), + }} + /> + )} + renderOption={(props, option) => ( + + + + + + + )} + /> + ); +} diff --git a/src/features/Calendars/CalendarSlice.ts b/src/features/Calendars/CalendarSlice.ts index 0046c20..3c87908 100644 --- a/src/features/Calendars/CalendarSlice.ts +++ b/src/features/Calendars/CalendarSlice.ts @@ -2,7 +2,7 @@ import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"; import { Calendars } from "./CalendarTypes"; import { CalendarEvent } from "../Events/EventsTypes"; import { getCalendar, getCalendars } from "./CalendarApi"; -import getOpenPaasUserId from "../User/userAPI"; +import { getOpenPaasUserId } from "../User/userAPI"; import { parseCalendarEvent } from "../Events/eventUtils"; import { deleteEvent, putEvent } from "../Events/EventApi"; import { formatDateToYYYYMMDDTHHMMSS } from "../../utils/dateUtils"; diff --git a/src/features/Events/EventModal.tsx b/src/features/Events/EventModal.tsx index 2674abe..c269402 100644 --- a/src/features/Events/EventModal.tsx +++ b/src/features/Events/EventModal.tsx @@ -1,24 +1,25 @@ -import React, { useEffect, useState } from "react"; -import { addEvent, putEventAsync } from "../Calendars/CalendarSlice"; -import { CalendarEvent } from "./EventsTypes"; import { CalendarApi, DateSelectArg } from "@fullcalendar/core"; -import { useAppDispatch, useAppSelector } from "../../app/hooks"; import { - Popover, - TextField, - Button, Box, - Typography, - Select, - MenuItem, - SelectChangeEvent, + Button, FormControl, InputLabel, + MenuItem, + Popover, + Select, + SelectChangeEvent, + TextField, + Typography, } from "@mui/material"; -import { Calendars } from "../Calendars/CalendarTypes"; -import { putEvent } from "./EventApi"; +import React, { useEffect, useState } from "react"; +import { useAppDispatch, useAppSelector } from "../../app/hooks"; +import AttendeeSelector from "../../components/Attendees/AttendeeSearch"; import { TIMEZONES } from "../../utils/timezone-data"; -import { CheckBox, Repeat } from "@mui/icons-material"; +import { putEventAsync } from "../Calendars/CalendarSlice"; +import { Calendars } from "../Calendars/CalendarTypes"; +import { userAttendee } from "../User/userDataTypes"; +import { CalendarEvent } from "./EventsTypes"; +import { createSelector } from "@reduxjs/toolkit"; function EventPopover({ anchorEl, @@ -39,14 +40,21 @@ function EventPopover({ const organizer = useAppSelector((state) => state.user.organiserData); const userId = useAppSelector((state) => state.user.userData.openpaasId); - const userPersonnalCalendars: Calendars[] = useAppSelector((state) => - Object.keys(state.calendars.list).map((id) => { - if (id.split("/")[0] === userId) { - return state.calendars.list[id]; - } - return {} as Calendars; - }) - ).filter((calendar) => calendar.id); + const selectPersonnalCalendars = createSelector( + (state) => state.calendars, + (calendars) => + Object.keys(calendars.list) + .map((id) => { + if (id.split("/")[0] === userId) { + return calendars.list[id]; + } + return {} as Calendars; + }) + .filter((calendar) => calendar.id) + ); + const userPersonnalCalendars: Calendars[] = useAppSelector( + selectPersonnalCalendars + ); const timezones = TIMEZONES.aliases; const [title, setTitle] = useState(""); @@ -57,6 +65,7 @@ function EventPopover({ const [calendarid, setCalendarid] = useState(0); const [allday, setAllDay] = useState(false); const [repetition, setRepetition] = useState(""); + const [attendees, setAttendees] = useState([]); const [timezone, setTimezone] = useState( Intl.DateTimeFormat().resolvedOptions().timeZone ); @@ -96,6 +105,11 @@ function EventPopover({ if (end) { newEvent.end = new Date(end); } + + if (attendees.length > 0) { + newEvent.attendee = newEvent.attendee.concat(attendees); + } + dispatch( putEventAsync({ cal: userPersonnalCalendars[calendarid], @@ -246,7 +260,7 @@ function EventPopover({ Repeat yearly - + Time Zone