[#50] added attendee search and tests
This commit is contained in:
committed by
Benoit TELLIER
parent
9a58c485ac
commit
d7b3d4e077
@@ -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 { 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 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 { formatDateToYYYYMMDDTHHMMSS } from "../../../src/utils/dateUtils";
|
||||||
|
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||||
|
|
||||||
|
jest.mock("../../../src/utils/apiUtils");
|
||||||
|
|
||||||
describe("EventPopover", () => {
|
describe("EventPopover", () => {
|
||||||
const mockOnClose = jest.fn();
|
const mockOnClose = jest.fn();
|
||||||
const mockSetSelectedRange = jest.fn();
|
const mockSetSelectedRange = jest.fn();
|
||||||
const mockCalendarRef = { current: { select: jest.fn() } } as any;
|
const mockCalendarRef = { current: { select: jest.fn() } } as any;
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
const preloadedState = {
|
const preloadedState = {
|
||||||
user: {
|
user: {
|
||||||
userData: {
|
userData: {
|
||||||
@@ -25,7 +25,7 @@ describe("EventPopover", () => {
|
|||||||
},
|
},
|
||||||
organiserData: {
|
organiserData: {
|
||||||
cn: "test",
|
cn: "test",
|
||||||
cal_address: "mailto:test@test.com",
|
cal_address: "test@test.com",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
calendars: {
|
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 = {
|
const defaultSelectedRange = {
|
||||||
startStr: "2025-07-18T09:00",
|
startStr: "2025-07-18T09:00",
|
||||||
endStr: "2025-07-18T10:00",
|
endStr: "2025-07-18T10:00",
|
||||||
@@ -130,6 +168,69 @@ describe("EventPopover", () => {
|
|||||||
|
|
||||||
expect(screen.getAllByRole("combobox")[0]).toHaveTextContent("Calendar 2");
|
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 () => {
|
it("dispatches putEventAsync and calls onClose when Save is clicked", async () => {
|
||||||
renderPopover();
|
renderPopover();
|
||||||
@@ -142,7 +243,7 @@ describe("EventPopover", () => {
|
|||||||
description: "Discuss project",
|
description: "Discuss project",
|
||||||
location: "Zoom",
|
location: "Zoom",
|
||||||
repetition: "",
|
repetition: "",
|
||||||
organizer: { cn: "test", cal_address: "mailto:test@test.com" },
|
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||||
timezone: "Europe/Paris",
|
timezone: "Europe/Paris",
|
||||||
transp: "OPAQUE",
|
transp: "OPAQUE",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { clientConfig } from "../../../src/features/User/oidcAuth";
|
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";
|
import { api } from "../../../src/utils/apiUtils";
|
||||||
|
|
||||||
jest.mock("../../../src/utils/apiUtils");
|
jest.mock("../../../src/utils/apiUtils");
|
||||||
@@ -14,7 +14,7 @@ describe("getOpenPaasUserId", () => {
|
|||||||
json: jest.fn().mockResolvedValue(mockUser),
|
json: jest.fn().mockResolvedValue(mockUser),
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await getOpenPaasUserId();
|
const result = await getOpenPaasUser();
|
||||||
|
|
||||||
expect(api.get).toHaveBeenCalledWith("api/user");
|
expect(api.get).toHaveBeenCalledWith("api/user");
|
||||||
expect(result).toEqual(mockUser);
|
expect(result).toEqual(mockUser);
|
||||||
|
|||||||
@@ -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<User[]>([]);
|
||||||
|
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 (
|
||||||
|
<Autocomplete
|
||||||
|
multiple
|
||||||
|
options={options}
|
||||||
|
loading={loading}
|
||||||
|
filterOptions={(x) => 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) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
label="Search user"
|
||||||
|
InputProps={{
|
||||||
|
...params.InputProps,
|
||||||
|
endAdornment: (
|
||||||
|
<>
|
||||||
|
{loading ? (
|
||||||
|
<CircularProgress color="inherit" size={20} />
|
||||||
|
) : null}
|
||||||
|
{params.InputProps.endAdornment}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
renderOption={(props, option) => (
|
||||||
|
<ListItem {...props} key={option.email} disableGutters>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar src={option.avatarUrl} alt={option.displayName} />
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText primary={option.displayName} secondary={option.email} />
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit";
|
|||||||
import { Calendars } from "./CalendarTypes";
|
import { Calendars } from "./CalendarTypes";
|
||||||
import { CalendarEvent } from "../Events/EventsTypes";
|
import { CalendarEvent } from "../Events/EventsTypes";
|
||||||
import { getCalendar, getCalendars } from "./CalendarApi";
|
import { getCalendar, getCalendars } from "./CalendarApi";
|
||||||
import getOpenPaasUserId from "../User/userAPI";
|
import { getOpenPaasUserId } from "../User/userAPI";
|
||||||
import { parseCalendarEvent } from "../Events/eventUtils";
|
import { parseCalendarEvent } from "../Events/eventUtils";
|
||||||
import { deleteEvent, putEvent } from "../Events/EventApi";
|
import { deleteEvent, putEvent } from "../Events/EventApi";
|
||||||
import { formatDateToYYYYMMDDTHHMMSS } from "../../utils/dateUtils";
|
import { formatDateToYYYYMMDDTHHMMSS } from "../../utils/dateUtils";
|
||||||
|
|||||||
@@ -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 { CalendarApi, DateSelectArg } from "@fullcalendar/core";
|
||||||
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
|
||||||
import {
|
import {
|
||||||
Popover,
|
|
||||||
TextField,
|
|
||||||
Button,
|
|
||||||
Box,
|
Box,
|
||||||
Typography,
|
Button,
|
||||||
Select,
|
|
||||||
MenuItem,
|
|
||||||
SelectChangeEvent,
|
|
||||||
FormControl,
|
FormControl,
|
||||||
InputLabel,
|
InputLabel,
|
||||||
|
MenuItem,
|
||||||
|
Popover,
|
||||||
|
Select,
|
||||||
|
SelectChangeEvent,
|
||||||
|
TextField,
|
||||||
|
Typography,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { Calendars } from "../Calendars/CalendarTypes";
|
import React, { useEffect, useState } from "react";
|
||||||
import { putEvent } from "./EventApi";
|
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
||||||
|
import AttendeeSelector from "../../components/Attendees/AttendeeSearch";
|
||||||
import { TIMEZONES } from "../../utils/timezone-data";
|
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({
|
function EventPopover({
|
||||||
anchorEl,
|
anchorEl,
|
||||||
@@ -39,14 +40,21 @@ function EventPopover({
|
|||||||
|
|
||||||
const organizer = useAppSelector((state) => state.user.organiserData);
|
const organizer = useAppSelector((state) => state.user.organiserData);
|
||||||
const userId = useAppSelector((state) => state.user.userData.openpaasId);
|
const userId = useAppSelector((state) => state.user.userData.openpaasId);
|
||||||
const userPersonnalCalendars: Calendars[] = useAppSelector((state) =>
|
const selectPersonnalCalendars = createSelector(
|
||||||
Object.keys(state.calendars.list).map((id) => {
|
(state) => state.calendars,
|
||||||
if (id.split("/")[0] === userId) {
|
(calendars) =>
|
||||||
return state.calendars.list[id];
|
Object.keys(calendars.list)
|
||||||
}
|
.map((id) => {
|
||||||
return {} as Calendars;
|
if (id.split("/")[0] === userId) {
|
||||||
})
|
return calendars.list[id];
|
||||||
).filter((calendar) => calendar.id);
|
}
|
||||||
|
return {} as Calendars;
|
||||||
|
})
|
||||||
|
.filter((calendar) => calendar.id)
|
||||||
|
);
|
||||||
|
const userPersonnalCalendars: Calendars[] = useAppSelector(
|
||||||
|
selectPersonnalCalendars
|
||||||
|
);
|
||||||
const timezones = TIMEZONES.aliases;
|
const timezones = TIMEZONES.aliases;
|
||||||
|
|
||||||
const [title, setTitle] = useState("");
|
const [title, setTitle] = useState("");
|
||||||
@@ -57,6 +65,7 @@ function EventPopover({
|
|||||||
const [calendarid, setCalendarid] = useState(0);
|
const [calendarid, setCalendarid] = useState(0);
|
||||||
const [allday, setAllDay] = useState(false);
|
const [allday, setAllDay] = useState(false);
|
||||||
const [repetition, setRepetition] = useState("");
|
const [repetition, setRepetition] = useState("");
|
||||||
|
const [attendees, setAttendees] = useState<userAttendee[]>([]);
|
||||||
const [timezone, setTimezone] = useState(
|
const [timezone, setTimezone] = useState(
|
||||||
Intl.DateTimeFormat().resolvedOptions().timeZone
|
Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||||
);
|
);
|
||||||
@@ -96,6 +105,11 @@ function EventPopover({
|
|||||||
if (end) {
|
if (end) {
|
||||||
newEvent.end = new Date(end);
|
newEvent.end = new Date(end);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (attendees.length > 0) {
|
||||||
|
newEvent.attendee = newEvent.attendee.concat(attendees);
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
putEventAsync({
|
putEventAsync({
|
||||||
cal: userPersonnalCalendars[calendarid],
|
cal: userPersonnalCalendars[calendarid],
|
||||||
@@ -246,7 +260,7 @@ function EventPopover({
|
|||||||
<MenuItem value={"yearly"}>Repeat yearly</MenuItem>
|
<MenuItem value={"yearly"}>Repeat yearly</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<AttendeeSelector setAttendees={setAttendees} />
|
||||||
<FormControl fullWidth margin="dense" size="small">
|
<FormControl fullWidth margin="dense" size="small">
|
||||||
<InputLabel id="timezone-select-label">Time Zone</InputLabel>
|
<InputLabel id="timezone-select-label">Time Zone</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
|
|||||||
@@ -4,3 +4,24 @@ export default async function getOpenPaasUser() {
|
|||||||
const user = await api.get(`api/user`).json();
|
const user = await api.get(`api/user`).json();
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function searchUsers(query: string) {
|
||||||
|
const response: any[] = await api
|
||||||
|
.post(`api/people/search/`, {
|
||||||
|
body: JSON.stringify({
|
||||||
|
limit: 10,
|
||||||
|
objectTypes: ["user", "group", "contact", "ldap"],
|
||||||
|
q: query,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
|
||||||
|
return response
|
||||||
|
.filter((user) => user.objectType === "user")
|
||||||
|
.map((user) => ({
|
||||||
|
email: user.emailAddresses?.[0]?.value || "",
|
||||||
|
displayName:
|
||||||
|
user.names?.[0]?.displayName || user.emailAddresses?.[0]?.value,
|
||||||
|
avatarUrl: user.photos?.[0]?.url || "",
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user