Merge pull request #54 from linagora/4-story-manage-attendance
4 story manage attendance
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
import { CalendarApi } from "@fullcalendar/core";
|
||||
import { jest } from "@jest/globals";
|
||||
import { ThunkDispatch } from "@reduxjs/toolkit";
|
||||
import "@testing-library/jest-dom";
|
||||
import { screen } from "@testing-library/react";
|
||||
import * as appHooks from "../../src/app/hooks";
|
||||
import CalendarApp from "../../src/components/Calendar/Calendar";
|
||||
import { renderWithProviders } from "../utils/Renderwithproviders";
|
||||
import CalendarSelection from "../../src/components/Calendar/CalendarSelection";
|
||||
|
||||
describe("CalendarSelection", () => {
|
||||
const today = new Date();
|
||||
const start = new Date(today);
|
||||
start.setHours(10, 0, 0, 0);
|
||||
const end = new Date(today);
|
||||
end.setHours(11, 0, 0, 0);
|
||||
|
||||
it("renders the all the calendars in list", async () => {
|
||||
const preloadedState = {
|
||||
user: {
|
||||
userData: {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
sid: "mockSid",
|
||||
openpaasId: "user1",
|
||||
},
|
||||
tokens: { accessToken: "token" }, // required to avoid redirect
|
||||
},
|
||||
calendars: {
|
||||
list: {
|
||||
"user1/cal1": {
|
||||
name: "Calendar personnal",
|
||||
id: "user1/cal1",
|
||||
color: "#FF0000",
|
||||
ownerEmails: ["alice@example.com"],
|
||||
events: {
|
||||
event1: {
|
||||
id: "event1",
|
||||
calId: "user1/cal1",
|
||||
uid: "event1",
|
||||
title: "Test Event",
|
||||
start: start.toISOString(),
|
||||
end: end.toISOString(),
|
||||
partstat: "ACCEPTED",
|
||||
organizer: {
|
||||
cn: "Alice",
|
||||
cal_address: "alice@example.com",
|
||||
},
|
||||
attendee: [
|
||||
{
|
||||
cn: "Alice",
|
||||
partstat: "ACCEPTED",
|
||||
rsvp: "TRUE",
|
||||
role: "REQ-PARTICIPANT",
|
||||
cutype: "INDIVIDUAL",
|
||||
cal_address: "alice@example.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
"user2/cal1": {
|
||||
name: "Calendar delegated",
|
||||
delegated: true,
|
||||
id: "user2/cal1",
|
||||
color: "#FF0000",
|
||||
ownerEmails: ["alice@example.com"],
|
||||
events: {
|
||||
event1: {
|
||||
id: "event1",
|
||||
calId: "user2/cal1",
|
||||
uid: "event1",
|
||||
title: "Test Event",
|
||||
start: start.toISOString(),
|
||||
end: end.toISOString(),
|
||||
partstat: "ACCEPTED",
|
||||
organizer: {
|
||||
cn: "Alice",
|
||||
cal_address: "alice@example.com",
|
||||
},
|
||||
attendee: [
|
||||
{
|
||||
cn: "Alice",
|
||||
partstat: "ACCEPTED",
|
||||
rsvp: "TRUE",
|
||||
role: "REQ-PARTICIPANT",
|
||||
cutype: "INDIVIDUAL",
|
||||
cal_address: "alice@example.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
"user3/cal1": {
|
||||
name: "Calendar shared",
|
||||
id: "user3/cal1",
|
||||
color: "#FF0000",
|
||||
ownerEmails: ["alice@example.com"],
|
||||
events: {
|
||||
event1: {
|
||||
id: "event1",
|
||||
calId: "user3/cal1",
|
||||
uid: "event1",
|
||||
title: "Test Event",
|
||||
start: start.toISOString(),
|
||||
end: end.toISOString(),
|
||||
partstat: "ACCEPTED",
|
||||
organizer: {
|
||||
cn: "Alice",
|
||||
cal_address: "alice@example.com",
|
||||
},
|
||||
attendee: [
|
||||
{
|
||||
cn: "Alice",
|
||||
partstat: "ACCEPTED",
|
||||
rsvp: "TRUE",
|
||||
role: "REQ-PARTICIPANT",
|
||||
cutype: "INDIVIDUAL",
|
||||
cal_address: "alice@example.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
pending: false,
|
||||
},
|
||||
};
|
||||
renderWithProviders(<CalendarApp />, preloadedState);
|
||||
expect(screen.getByText("Personnal Calendars")).toBeInTheDocument();
|
||||
expect(screen.getByText("Delegated Calendars")).toBeInTheDocument();
|
||||
expect(screen.getByText("Shared Calendars")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByLabelText("Calendar personnal")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Calendar delegated")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Calendar shared")).toBeInTheDocument();
|
||||
});
|
||||
it("renders only personnal calendars in list", async () => {
|
||||
const preloadedState = {
|
||||
user: {
|
||||
userData: {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
sid: "mockSid",
|
||||
openpaasId: "user1",
|
||||
},
|
||||
tokens: { accessToken: "token" }, // required to avoid redirect
|
||||
},
|
||||
calendars: {
|
||||
list: {
|
||||
"user1/cal1": {
|
||||
name: "Calendar personnal",
|
||||
id: "user1/cal1",
|
||||
color: "#FF0000",
|
||||
ownerEmails: ["alice@example.com"],
|
||||
events: {
|
||||
event1: {
|
||||
id: "event1",
|
||||
calId: "user1/cal1",
|
||||
uid: "event1",
|
||||
title: "Test Event",
|
||||
start: start.toISOString(),
|
||||
end: end.toISOString(),
|
||||
partstat: "ACCEPTED",
|
||||
organizer: {
|
||||
cn: "Alice",
|
||||
cal_address: "alice@example.com",
|
||||
},
|
||||
attendee: [
|
||||
{
|
||||
cn: "Alice",
|
||||
partstat: "ACCEPTED",
|
||||
rsvp: "TRUE",
|
||||
role: "REQ-PARTICIPANT",
|
||||
cutype: "INDIVIDUAL",
|
||||
cal_address: "alice@example.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
pending: false,
|
||||
},
|
||||
};
|
||||
renderWithProviders(<CalendarApp />, preloadedState);
|
||||
expect(screen.getByText("Personnal Calendars")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Delegated Calendars")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Shared Calendars")).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.getByLabelText("Calendar personnal")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -18,7 +18,6 @@ describe("CalendarApp integration", () => {
|
||||
jest.clearAllMocks();
|
||||
const dispatch = jest.fn() as ThunkDispatch<any, any, any>;
|
||||
jest.spyOn(appHooks, "useAppDispatch").mockReturnValue(dispatch);
|
||||
jest.useFakeTimers(); // optional
|
||||
});
|
||||
|
||||
const renderCalendar = () => {
|
||||
@@ -36,7 +35,9 @@ describe("CalendarApp integration", () => {
|
||||
list: {
|
||||
"667037022b752d0026472254/cal1": {
|
||||
name: "Calendar 1",
|
||||
id: "667037022b752d0026472254/cal1",
|
||||
color: "#FF0000",
|
||||
ownerEmails: ["alice@example.com"],
|
||||
events: {
|
||||
event1: {
|
||||
id: "event1",
|
||||
@@ -45,6 +46,21 @@ describe("CalendarApp integration", () => {
|
||||
title: "Test Event",
|
||||
start: start.toISOString(),
|
||||
end: end.toISOString(),
|
||||
partstat: "ACCEPTED",
|
||||
organizer: {
|
||||
cn: "Alice",
|
||||
cal_address: "alice@example.com",
|
||||
},
|
||||
attendee: [
|
||||
{
|
||||
cn: "Alice",
|
||||
partstat: "ACCEPTED",
|
||||
rsvp: "TRUE",
|
||||
role: "REQ-PARTICIPANT",
|
||||
cutype: "INDIVIDUAL",
|
||||
cal_address: "alice@example.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -30,6 +30,7 @@ describe("MiniCalendar", () => {
|
||||
"667037022b752d0026472254/cal1": {
|
||||
name: "Calendar 1",
|
||||
color: "#FF0000",
|
||||
ownerEmails: ["test@test.com"],
|
||||
events: {
|
||||
event1: {
|
||||
calId: "667037022b752d0026472254/cal1",
|
||||
@@ -139,6 +140,7 @@ describe("Found Bugs", () => {
|
||||
"667037022b752d0026472254/cal1": {
|
||||
name: "Calendar 1",
|
||||
color: "#FF0000",
|
||||
ownerEmails: ["test.test@test.com"],
|
||||
events: {
|
||||
event1: {
|
||||
calId: "667037022b752d0026472254/cal1",
|
||||
|
||||
@@ -169,4 +169,286 @@ describe("Event Display", () => {
|
||||
|
||||
expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
|
||||
});
|
||||
it("renders RSVP buttons when user is an attendee", () => {
|
||||
const rsvpStateIsOrga = {
|
||||
...preloadedState,
|
||||
calendars: {
|
||||
...preloadedState.calendars,
|
||||
list: {
|
||||
...preloadedState.calendars.list,
|
||||
"667037022b752d0026472254/cal1": {
|
||||
...preloadedState.calendars.list["667037022b752d0026472254/cal1"],
|
||||
events: {
|
||||
event1: {
|
||||
...preloadedState.calendars.list[
|
||||
"667037022b752d0026472254/cal1"
|
||||
].events.event1,
|
||||
attendee: [
|
||||
{
|
||||
cal_address: "test@test.com",
|
||||
cn: "Test User",
|
||||
partstat: "NEEDS-ACTION",
|
||||
},
|
||||
{
|
||||
cal_address: "organizer@test.com",
|
||||
cn: "Test Organizer",
|
||||
partstat: "NEEDS-ACTION",
|
||||
},
|
||||
],
|
||||
organizer: {
|
||||
cal_address: "organizer@test.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithProviders(
|
||||
<EventDisplayModal
|
||||
anchorEl={document.body}
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
/>,
|
||||
rsvpStateIsOrga
|
||||
);
|
||||
|
||||
expect(screen.getByText("Will you attend?")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Accept" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Maybe" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Decline" })).toBeInTheDocument();
|
||||
});
|
||||
it("doesnt renders RSVP buttons when user isnt an attendee", () => {
|
||||
const rsvpStateIsOrga = {
|
||||
...preloadedState,
|
||||
calendars: {
|
||||
...preloadedState.calendars,
|
||||
list: {
|
||||
...preloadedState.calendars.list,
|
||||
"667037022b752d0026472254/cal1": {
|
||||
...preloadedState.calendars.list["667037022b752d0026472254/cal1"],
|
||||
events: {
|
||||
event1: {
|
||||
...preloadedState.calendars.list[
|
||||
"667037022b752d0026472254/cal1"
|
||||
].events.event1,
|
||||
attendee: [
|
||||
{
|
||||
cal_address: "organizer@test.com",
|
||||
cn: "Test Organizer",
|
||||
partstat: "NEEDS-ACTION",
|
||||
},
|
||||
],
|
||||
organizer: {
|
||||
cal_address: "organizer@test.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithProviders(
|
||||
<EventDisplayModal
|
||||
anchorEl={document.body}
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
/>,
|
||||
rsvpStateIsOrga
|
||||
);
|
||||
|
||||
expect(screen.queryByText("Will you attend?")).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "Accept" })
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "Maybe" })
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "Decline" })
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("handles RSVP Accept click", async () => {
|
||||
const spy = jest
|
||||
.spyOn(eventThunks, "putEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
return () => Promise.resolve(payload) as any;
|
||||
});
|
||||
|
||||
const rsvpState = {
|
||||
...preloadedState,
|
||||
calendars: {
|
||||
...preloadedState.calendars,
|
||||
list: {
|
||||
...preloadedState.calendars.list,
|
||||
"667037022b752d0026472254/cal1": {
|
||||
...preloadedState.calendars.list["667037022b752d0026472254/cal1"],
|
||||
events: {
|
||||
event1: {
|
||||
...preloadedState.calendars.list[
|
||||
"667037022b752d0026472254/cal1"
|
||||
].events.event1,
|
||||
attendee: [
|
||||
{
|
||||
cal_address: "test@test.com",
|
||||
cn: "Test User",
|
||||
partstat: "NEEDS-ACTION",
|
||||
},
|
||||
],
|
||||
organizer: {
|
||||
cal_address: "organizer@test.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithProviders(
|
||||
<EventDisplayModal
|
||||
anchorEl={document.body}
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
/>,
|
||||
rsvpState
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Accept" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
|
||||
});
|
||||
|
||||
const updatedEvent = spy.mock.calls[0][0].newEvent;
|
||||
expect(updatedEvent.attendee[0].partstat).toBe("ACCEPTED");
|
||||
});
|
||||
|
||||
it("handles RSVP Maybe click", async () => {
|
||||
const spy = jest
|
||||
.spyOn(eventThunks, "putEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
return () => Promise.resolve(payload) as any;
|
||||
});
|
||||
|
||||
const rsvpState = {
|
||||
...preloadedState,
|
||||
calendars: {
|
||||
...preloadedState.calendars,
|
||||
list: {
|
||||
...preloadedState.calendars.list,
|
||||
"667037022b752d0026472254/cal1": {
|
||||
...preloadedState.calendars.list["667037022b752d0026472254/cal1"],
|
||||
events: {
|
||||
event1: {
|
||||
...preloadedState.calendars.list[
|
||||
"667037022b752d0026472254/cal1"
|
||||
].events.event1,
|
||||
attendee: [
|
||||
{
|
||||
cal_address: "test@test.com",
|
||||
cn: "Test User",
|
||||
partstat: "NEEDS-ACTION",
|
||||
},
|
||||
],
|
||||
organizer: {
|
||||
cal_address: "organizer@test.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithProviders(
|
||||
<EventDisplayModal
|
||||
anchorEl={document.body}
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
/>,
|
||||
rsvpState
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Maybe" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
|
||||
});
|
||||
|
||||
const updatedEvent = spy.mock.calls[0][0].newEvent;
|
||||
expect(updatedEvent.attendee[0].partstat).toBe("TENTATIVE");
|
||||
});
|
||||
|
||||
it("handles RSVP Decline click", async () => {
|
||||
const spy = jest
|
||||
.spyOn(eventThunks, "putEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
return () => Promise.resolve(payload) as any;
|
||||
});
|
||||
|
||||
const rsvpState = {
|
||||
...preloadedState,
|
||||
calendars: {
|
||||
...preloadedState.calendars,
|
||||
list: {
|
||||
...preloadedState.calendars.list,
|
||||
"667037022b752d0026472254/cal1": {
|
||||
...preloadedState.calendars.list["667037022b752d0026472254/cal1"],
|
||||
events: {
|
||||
event1: {
|
||||
...preloadedState.calendars.list[
|
||||
"667037022b752d0026472254/cal1"
|
||||
].events.event1,
|
||||
attendee: [
|
||||
{
|
||||
cal_address: "test@test.com",
|
||||
cn: "Test User",
|
||||
partstat: "NEEDS-ACTION",
|
||||
},
|
||||
],
|
||||
organizer: {
|
||||
cal_address: "organizer@test.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithProviders(
|
||||
<EventDisplayModal
|
||||
anchorEl={document.body}
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
/>,
|
||||
rsvpState
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Decline" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
|
||||
});
|
||||
|
||||
const updatedEvent = spy.mock.calls[0][0].newEvent;
|
||||
expect(updatedEvent.attendee[0].partstat).toBe("DECLINED");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,12 +16,12 @@ describe("parseCalendarEvent", () => {
|
||||
["SUMMARY", {}, "text", "Team Meeting"],
|
||||
["DESCRIPTION", {}, "text", "Discuss roadmap"],
|
||||
["LOCATION", {}, "text", "Zoom"],
|
||||
["ORGANIZER", { cn: "Alice" }, "cal-address", "mailto:alice@example.com"],
|
||||
["ORGANIZER", { cn: "Alice" }, "cal-address", "alice@example.com"],
|
||||
[
|
||||
"ATTENDEE",
|
||||
{ cn: "Bob", partstat: "ACCEPTED" },
|
||||
"cal-address",
|
||||
"mailto:bob@example.com",
|
||||
"bob@example.com",
|
||||
],
|
||||
["X-OPENPAAS-VIDEOCONFERENCE", {}, "text", "https://meet.link"],
|
||||
["STATUS", {}, "text", "CONFIRMED"],
|
||||
@@ -98,8 +98,8 @@ describe("parseCalendarEvent", () => {
|
||||
const rawData = [
|
||||
["UID", {}, "text", "event-4"],
|
||||
["DTSTART", {}, "date-time", "2025-07-18T09:00:00Z"],
|
||||
["ATTENDEE", {}, "cal-address", "mailto:john@example.com"],
|
||||
["ORGANIZER", {}, "cal-address", "mailto:jane@example.com"],
|
||||
["ATTENDEE", {}, "cal-address", "john@example.com"],
|
||||
["ORGANIZER", {}, "cal-address", "jane@example.com"],
|
||||
] as unknown as [string, Record<string, string>, string, any];
|
||||
|
||||
const result = parseCalendarEvent(rawData, baseColor, calendarId);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { clientConfig } from "../../../src/features/User/oidcAuth";
|
||||
import getOpenPaasUser from "../../../src/features/User/userAPI";
|
||||
import { getOpenPaasUser } from "../../../src/features/User/userAPI";
|
||||
import { api } from "../../../src/utils/apiUtils";
|
||||
|
||||
jest.mock("../../../src/utils/apiUtils");
|
||||
|
||||
@@ -9,6 +9,9 @@ main {
|
||||
height: 90vh;
|
||||
}
|
||||
|
||||
.calendarListHeader {
|
||||
padding-top: 10px;
|
||||
}
|
||||
.sidebar {
|
||||
border-right: 2px gray;
|
||||
width: 20vw;
|
||||
@@ -27,6 +30,18 @@ main {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.declined-event {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.tentative-event {
|
||||
}
|
||||
|
||||
.needs-action-event {
|
||||
opacity: 0.7;
|
||||
border: dashed 1px #ffffff;
|
||||
}
|
||||
|
||||
.fc .fc-timegrid-slot {
|
||||
height: 30px !important;
|
||||
min-height: 30px !important;
|
||||
|
||||
@@ -27,12 +27,17 @@ import { Calendars } from "../../features/Calendars/CalendarTypes";
|
||||
import { push } from "redux-first-history";
|
||||
import EventDisplayModal from "../../features/Events/EventDisplay";
|
||||
import { createSelector } from "@reduxjs/toolkit";
|
||||
import HelpOutlineIcon from "@mui/icons-material/HelpOutline";
|
||||
import ClearIcon from "@mui/icons-material/Clear";
|
||||
import AccessTimeIcon from "@mui/icons-material/AccessTime";
|
||||
import { userAttendee } from "../../features/User/userDataTypes";
|
||||
|
||||
export default function CalendarApp() {
|
||||
const calendarRef = useRef<CalendarApi | null>(null);
|
||||
const [selectedDate, setSelectedDate] = useState(new Date());
|
||||
const [selectedMiniDate, setSelectedMiniDate] = useState(new Date());
|
||||
const tokens = useAppSelector((state) => state.user.tokens);
|
||||
const user = useAppSelector((state) => state.user.userData);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
if (!tokens) {
|
||||
@@ -372,6 +377,82 @@ export default function CalendarApp() {
|
||||
center: "prev,today,next",
|
||||
right: "dayGridMonth,timeGridWeek,timeGridDay",
|
||||
}}
|
||||
eventContent={(arg) => {
|
||||
const event = arg.event;
|
||||
if (!calendars[arg.event._def.extendedProps.calId]) return;
|
||||
|
||||
const attendees = event._def.extendedProps.attendee || [];
|
||||
let Icon = null;
|
||||
let titleStyle: React.CSSProperties = {};
|
||||
const ownerEmails = new Set(
|
||||
calendars[arg.event._def.extendedProps.calId].ownerEmails?.map(
|
||||
(email) => email.toLowerCase()
|
||||
)
|
||||
);
|
||||
const showSpecialDisplay = attendees.filter((att: userAttendee) =>
|
||||
ownerEmails.has(att.cal_address.toLowerCase())
|
||||
);
|
||||
if (!showSpecialDisplay[0]) return;
|
||||
switch (showSpecialDisplay[0].partstat) {
|
||||
case "DECLINED":
|
||||
Icon = null;
|
||||
titleStyle.textDecoration = "line-through";
|
||||
break;
|
||||
case "TENTATIVE":
|
||||
Icon = HelpOutlineIcon;
|
||||
break;
|
||||
case "NEEDS-ACTION":
|
||||
Icon = AccessTimeIcon;
|
||||
break;
|
||||
case "ACCEPTED":
|
||||
Icon = null;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{Icon && <Icon fontSize="small" />}
|
||||
<span style={titleStyle}>{event.title}</span>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
eventDidMount={(arg) => {
|
||||
const attendees = arg.event._def.extendedProps.attendee || [];
|
||||
if (!calendars[arg.event._def.extendedProps.calId]) return;
|
||||
const ownerEmails = new Set(
|
||||
calendars[arg.event._def.extendedProps.calId].ownerEmails?.map(
|
||||
(email) => email.toLowerCase()
|
||||
)
|
||||
);
|
||||
const showSpecialDisplay = attendees.filter((att: userAttendee) =>
|
||||
ownerEmails.has(att.cal_address.toLowerCase())
|
||||
);
|
||||
|
||||
if (!showSpecialDisplay[0]) return;
|
||||
|
||||
// Clear previously applied classes
|
||||
arg.el.classList.remove(
|
||||
"declined-event",
|
||||
"tentative-event",
|
||||
"needs-action-event"
|
||||
);
|
||||
|
||||
switch (showSpecialDisplay[0].partstat) {
|
||||
case "DECLINED":
|
||||
arg.el.classList.add("declined-event");
|
||||
break;
|
||||
case "TENTATIVE":
|
||||
arg.el.classList.add("tentative-event");
|
||||
break;
|
||||
case "NEEDS-ACTION":
|
||||
arg.el.classList.add("needs-action-event");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<EventPopover
|
||||
anchorEl={anchorEl}
|
||||
|
||||
@@ -7,11 +7,17 @@ export default function CalendarSelection({
|
||||
selectedCalendars: string[];
|
||||
setSelectedCalendars: Function;
|
||||
}) {
|
||||
const tokens = useAppSelector((state) => state.user.tokens);
|
||||
const userId = useAppSelector((state) => state.user.userData.openpaasId);
|
||||
const dispatch = useAppDispatch();
|
||||
const calendars = useAppSelector((state) => state.calendars.list);
|
||||
|
||||
const personnalCalendars = Object.keys(calendars).filter(
|
||||
(id) => id.split("/")[0] === userId
|
||||
);
|
||||
const delegatedCalendars = Object.keys(calendars).filter(
|
||||
(id) => id.split("/")[0] !== userId && calendars[id].delegated
|
||||
);
|
||||
const sharedCalendars = Object.keys(calendars).filter(
|
||||
(id) => id.split("/")[0] !== userId && !calendars[id].delegated
|
||||
);
|
||||
const handleCalendarToggle = (name: string) => {
|
||||
setSelectedCalendars((prev: string[]) =>
|
||||
prev.includes(name) ? prev.filter((n) => n !== name) : [...prev, name]
|
||||
@@ -20,28 +26,11 @@ export default function CalendarSelection({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>personnalCalendars</h3>
|
||||
{Object.keys(calendars)
|
||||
.filter((id) => id.split("/")[0] === userId)
|
||||
.map((id) => {
|
||||
return (
|
||||
<div key={id}>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
style={{ backgroundColor: calendars[id].color }}
|
||||
checked={selectedCalendars.includes(id)}
|
||||
onChange={() => handleCalendarToggle(id)}
|
||||
/>
|
||||
{calendars[id].name}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<h3>sharedCalendars</h3>
|
||||
{Object.keys(calendars)
|
||||
.filter((id) => id.split("/")[0] !== userId)
|
||||
.map((id) => (
|
||||
<span className="calendarListHeader">
|
||||
<h3>Personnal Calendars</h3>
|
||||
</span>
|
||||
{personnalCalendars.map((id) => {
|
||||
return (
|
||||
<div key={id}>
|
||||
<label>
|
||||
<input
|
||||
@@ -53,7 +42,48 @@ export default function CalendarSelection({
|
||||
{calendars[id].name}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
{delegatedCalendars.length > 0 && (
|
||||
<>
|
||||
<span className="calendarListHeader">
|
||||
<h3>Delegated Calendars</h3>
|
||||
</span>
|
||||
{delegatedCalendars.map((id) => (
|
||||
<div key={id}>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
style={{ backgroundColor: calendars[id].color }}
|
||||
checked={selectedCalendars.includes(id)}
|
||||
onChange={() => handleCalendarToggle(id)}
|
||||
/>
|
||||
{calendars[id].name}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{sharedCalendars.length > 0 && (
|
||||
<>
|
||||
<span className="calendarListHeader">
|
||||
<h3>Shared Calendars</h3>
|
||||
</span>
|
||||
{sharedCalendars.map((id) => (
|
||||
<div key={id}>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
style={{ backgroundColor: calendars[id].color }}
|
||||
checked={selectedCalendars.includes(id)}
|
||||
onChange={() => handleCalendarToggle(id)}
|
||||
/>
|
||||
{calendars[id].name}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,11 +2,10 @@ import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { Calendars } from "./CalendarTypes";
|
||||
import { CalendarEvent } from "../Events/EventsTypes";
|
||||
import { getCalendar, getCalendars } from "./CalendarApi";
|
||||
import getOpenPaasUser from "../User/userAPI";
|
||||
import { getOpenPaasUser, getUserDetails } from "../User/userAPI";
|
||||
import { parseCalendarEvent } from "../Events/eventUtils";
|
||||
import { deleteEvent, putEvent } from "../Events/EventApi";
|
||||
import { formatDateToYYYYMMDDTHHMMSS } from "../../utils/dateUtils";
|
||||
import { responsiveFontSizes } from "@mui/material";
|
||||
|
||||
export const getCalendarsListAsync = createAsyncThunk<
|
||||
Record<string, Calendars> // Return type
|
||||
@@ -19,14 +18,28 @@ export const getCalendarsListAsync = createAsyncThunk<
|
||||
for (const cal of rawCalendars) {
|
||||
const name = cal["dav:name"];
|
||||
const description = cal["dav:description"];
|
||||
const id = cal["calendarserver:source"]
|
||||
let delegated = false;
|
||||
let source = cal["calendarserver:source"]
|
||||
? cal["calendarserver:source"]._links.self.href
|
||||
.replace("/calendars/", "")
|
||||
.replace(".json", "")
|
||||
: cal._links.self.href.replace("/calendars/", "").replace(".json", "");
|
||||
: cal._links.self.href;
|
||||
if (cal["calendarserver:delegatedsource"]) {
|
||||
source = cal["calendarserver:delegatedsource"];
|
||||
delegated = true;
|
||||
}
|
||||
console.log(source);
|
||||
const id = source.replace("/calendars/", "").replace(".json", "");
|
||||
|
||||
const ownerData: any = await getUserDetails(id.split("/")[0]);
|
||||
const color = cal["apple:color"];
|
||||
importedCalendars[id] = { id, name, description, color, events: {} };
|
||||
importedCalendars[id] = {
|
||||
id,
|
||||
name,
|
||||
ownerEmails: ownerData.emails,
|
||||
description,
|
||||
delegated,
|
||||
color,
|
||||
events: {},
|
||||
};
|
||||
}
|
||||
|
||||
return importedCalendars;
|
||||
@@ -41,8 +54,9 @@ export const getCalendarDetailAsync = createAsyncThunk<
|
||||
const events: CalendarEvent[] = calendar._embedded["dav:item"].flatMap(
|
||||
(eventdata: any) => {
|
||||
const vevents = eventdata.data[2] as any[][]; // array of ['vevent', RawEntry[], []]
|
||||
const eventURL = eventdata._links.self.href;
|
||||
return vevents.map((vevent: any[]) => {
|
||||
return parseCalendarEvent(vevent[1], color, calId);
|
||||
return parseCalendarEvent(vevent[1], color, calId, eventURL);
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -54,18 +68,19 @@ export const putEventAsync = createAsyncThunk<
|
||||
{ calId: string; events: CalendarEvent[] }, // Return type
|
||||
{ cal: Calendars; newEvent: CalendarEvent } // Arg type
|
||||
>("calendars/putEvent", async ({ cal, newEvent }) => {
|
||||
const response = await putEvent(cal, newEvent);
|
||||
const response = await putEvent(newEvent);
|
||||
const calEvents = (await getCalendar(cal.id, {
|
||||
start: formatDateToYYYYMMDDTHHMMSS(newEvent.start),
|
||||
start: formatDateToYYYYMMDDTHHMMSS(new Date(newEvent.start)),
|
||||
end: formatDateToYYYYMMDDTHHMMSS(
|
||||
new Date(newEvent.start.getTime() + 86400000)
|
||||
new Date(new Date(newEvent.start).getTime() + 86400000)
|
||||
),
|
||||
})) as Record<string, any>;
|
||||
const events: CalendarEvent[] = calEvents._embedded["dav:item"].flatMap(
|
||||
(eventdata: any) => {
|
||||
const vevents = eventdata.data[2] as any[][];
|
||||
const eventURL = eventdata._links.self.href;
|
||||
return vevents.map((vevent: any[]) => {
|
||||
return parseCalendarEvent(vevent[1], cal.color ?? "", cal.id);
|
||||
return parseCalendarEvent(vevent[1], cal.color ?? "", cal.id, eventURL);
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -78,9 +93,9 @@ export const putEventAsync = createAsyncThunk<
|
||||
|
||||
export const deleteEventAsync = createAsyncThunk<
|
||||
{ calId: string; eventId: string }, // Return type
|
||||
{ calId: string; eventId: string } // Arg type
|
||||
>("calendars/delEvent", async ({ calId, eventId }) => {
|
||||
const response = await deleteEvent(calId, eventId);
|
||||
{ calId: string; eventId: string; eventURL: string } // Arg type
|
||||
>("calendars/delEvent", async ({ calId, eventId, eventURL }) => {
|
||||
const response = await deleteEvent(eventURL);
|
||||
return { calId, eventId };
|
||||
});
|
||||
|
||||
@@ -105,6 +120,11 @@ const CalendarSlice = createSlice({
|
||||
}
|
||||
state.list[action.payload.calendarUid].events[action.payload.event.uid] =
|
||||
action.payload.event;
|
||||
state.list[action.payload.calendarUid].events[
|
||||
action.payload.event.uid
|
||||
].URL = `dav/calendars/${action.payload.calendarUid}/${
|
||||
action.payload.event.uid.split("/")[0]
|
||||
}.isc`;
|
||||
},
|
||||
removeEvent: (
|
||||
state,
|
||||
|
||||
@@ -3,8 +3,10 @@ import { CalendarEvent } from "../Events/EventsTypes";
|
||||
export interface Calendars {
|
||||
id: string;
|
||||
name: string;
|
||||
delegated?: boolean;
|
||||
prodid?: string;
|
||||
color?: string;
|
||||
ownerEmails?: string[];
|
||||
description?: string;
|
||||
calscale?: string;
|
||||
version?: string;
|
||||
|
||||
@@ -1,28 +1,24 @@
|
||||
import { api } from "../../utils/apiUtils";
|
||||
import { Calendars } from "../Calendars/CalendarTypes";
|
||||
import { CalendarEvent } from "./EventsTypes";
|
||||
import { calendarEventToJCal } from "./eventUtils";
|
||||
|
||||
export async function putEvent(cal: Calendars, event: CalendarEvent) {
|
||||
const response = await api(
|
||||
`dav/calendars/${cal.id}/${event.uid.split("/")[0]}.isc`,
|
||||
{
|
||||
method: "PUT",
|
||||
body: JSON.stringify(calendarEventToJCal(event)),
|
||||
headers: {
|
||||
"content-type": "text/calendar; charset=utf-8",
|
||||
},
|
||||
}
|
||||
).json();
|
||||
return response;
|
||||
export async function putEvent(event: CalendarEvent) {
|
||||
const response = await api(`dav${event.URL}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(calendarEventToJCal(event)),
|
||||
headers: {
|
||||
"content-type": "text/calendar; charset=utf-8",
|
||||
},
|
||||
});
|
||||
if (response.status === 201) {
|
||||
console.log("PUT (201) :", response.url);
|
||||
}
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
export async function deleteEvent(calId: string, eventId: string) {
|
||||
const response = await api(
|
||||
`dav/calendars/${calId}/${eventId.split("/")[0]}.isc`,
|
||||
{
|
||||
method: "DELETE",
|
||||
}
|
||||
).json();
|
||||
export async function deleteEvent(eventURL: string) {
|
||||
const response = await api(eventURL, {
|
||||
method: "DELETE",
|
||||
}).json();
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { deleteEventAsync } from "../Calendars/CalendarSlice";
|
||||
import { deleteEventAsync, putEventAsync } from "../Calendars/CalendarSlice";
|
||||
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
||||
import {
|
||||
Popover,
|
||||
@@ -25,6 +25,7 @@ import CircleIcon from "@mui/icons-material/Circle";
|
||||
import CancelIcon from "@mui/icons-material/Cancel";
|
||||
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
|
||||
import { userAttendee } from "../User/userDataTypes";
|
||||
import { putEvent } from "./EventApi";
|
||||
|
||||
function EventDisplayModal({
|
||||
eventId,
|
||||
@@ -74,6 +75,18 @@ function EventDisplayModal({
|
||||
(a) => a.cal_address === event.organizer?.cal_address
|
||||
);
|
||||
|
||||
function handleRSVP(rsvp: string) {
|
||||
const newEvent = {
|
||||
...event,
|
||||
attendee: event.attendee?.map((a) =>
|
||||
a.cal_address === user.userData.email ? { ...a, partstat: rsvp } : a
|
||||
),
|
||||
};
|
||||
|
||||
dispatch(putEventAsync({ cal: calendar, newEvent }));
|
||||
onClose({}, "backdropClick");
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover open={open} anchorEl={anchorEl} onClose={onClose}>
|
||||
<Card sx={{ width: 300, p: 2, position: "relative" }}>
|
||||
@@ -195,9 +208,36 @@ function EventDisplayModal({
|
||||
Will you attend?
|
||||
</Typography>
|
||||
<ButtonGroup size="small" fullWidth>
|
||||
<Button>Accept</Button>
|
||||
<Button>Maybe</Button>
|
||||
<Button>Decline</Button>
|
||||
<Button
|
||||
color={
|
||||
currentUserAttendee.partstat === "ACCEPTED"
|
||||
? "success"
|
||||
: "primary"
|
||||
}
|
||||
onClick={() => handleRSVP("ACCEPTED")}
|
||||
>
|
||||
Accept
|
||||
</Button>
|
||||
<Button
|
||||
color={
|
||||
currentUserAttendee.partstat === "TENTATIVE"
|
||||
? "warning"
|
||||
: "primary"
|
||||
}
|
||||
onClick={() => handleRSVP("TENTATIVE")}
|
||||
>
|
||||
Maybe
|
||||
</Button>
|
||||
<Button
|
||||
color={
|
||||
currentUserAttendee.partstat === "DECLINED"
|
||||
? "error"
|
||||
: "primary"
|
||||
}
|
||||
onClick={() => handleRSVP("DECLINED")}
|
||||
>
|
||||
Decline
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { userAttendee, userOrganiser } from "../User/userDataTypes";
|
||||
|
||||
export interface CalendarEvent {
|
||||
URL: string;
|
||||
calId: string;
|
||||
uid: string;
|
||||
transp?: string;
|
||||
|
||||
@@ -8,7 +8,8 @@ type RawEntry = [string, Record<string, string>, string, any];
|
||||
export function parseCalendarEvent(
|
||||
data: RawEntry[],
|
||||
color: string,
|
||||
calendarid: string
|
||||
calendarid: string,
|
||||
eventURL: string
|
||||
): CalendarEvent {
|
||||
const event: Partial<CalendarEvent> = { color, attendee: [] };
|
||||
let recurrenceId;
|
||||
@@ -75,6 +76,7 @@ export function parseCalendarEvent(
|
||||
event.uid = `${event.uid}/${recurrenceId}`;
|
||||
}
|
||||
|
||||
event.URL = eventURL;
|
||||
if (!event.uid || !event.start) {
|
||||
console.error(
|
||||
`missing crucial event param in calendar ${calendarid} `,
|
||||
@@ -98,7 +100,7 @@ export function calendarEventToJCal(event: CalendarEvent): any[] {
|
||||
"dtstart",
|
||||
{ tzid },
|
||||
event.allday ? "date" : "date-time",
|
||||
formatDateToICal(event.start, event.allday ?? false),
|
||||
formatDateToICal(new Date(event.start), event.allday ?? false),
|
||||
],
|
||||
["class", {}, "text", event.class ?? "PUBLIC"],
|
||||
[
|
||||
@@ -117,7 +119,7 @@ export function calendarEventToJCal(event: CalendarEvent): any[] {
|
||||
"dtend",
|
||||
{ tzid },
|
||||
event.allday ? "date" : "date-time",
|
||||
formatDateToICal(event.end, event.allday ?? false),
|
||||
formatDateToICal(new Date(event.end), event.allday ?? false),
|
||||
]);
|
||||
}
|
||||
if (event.organizer) {
|
||||
@@ -139,15 +141,18 @@ export function calendarEventToJCal(event: CalendarEvent): any[] {
|
||||
}
|
||||
|
||||
event.attendee.forEach((att) => {
|
||||
const attendee: Record<string, string> = {
|
||||
partstat: att.partstat,
|
||||
rsvp: att.rsvp,
|
||||
role: att.role,
|
||||
cutype: att.cutype,
|
||||
};
|
||||
if (att.cn) {
|
||||
attendee.cn = att.cn;
|
||||
}
|
||||
vevent[1].push([
|
||||
"attendee",
|
||||
{
|
||||
cn: att.cn,
|
||||
partstat: att.partstat,
|
||||
rsvp: att.rsvp,
|
||||
role: att.role,
|
||||
cutype: att.cutype,
|
||||
},
|
||||
attendee,
|
||||
"cal-address",
|
||||
`mailto:${att.cal_address}`,
|
||||
]);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { api } from "../../utils/apiUtils";
|
||||
|
||||
export default async function getOpenPaasUser() {
|
||||
export async function getOpenPaasUser() {
|
||||
const user = await api.get(`api/user`).json();
|
||||
return user;
|
||||
}
|
||||
@@ -25,3 +25,8 @@ export async function searchUsers(query: string) {
|
||||
avatarUrl: user.photos?.[0]?.url || "",
|
||||
}));
|
||||
}
|
||||
|
||||
export async function getUserDetails(id: string) {
|
||||
const user = await api.get(`api/users/${id}`).json();
|
||||
return user;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export interface userOrganiser {
|
||||
}
|
||||
|
||||
export interface userAttendee {
|
||||
cn: string;
|
||||
cn?: string;
|
||||
cal_address: string;
|
||||
partstat: string;
|
||||
rsvp: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
|
||||
import { userData, userOrganiser } from "./userDataTypes";
|
||||
import getOpenPaasUser from "./userAPI";
|
||||
import { getOpenPaasUser } from "./userAPI";
|
||||
|
||||
export const getOpenPaasUserDataAsync = createAsyncThunk<any>(
|
||||
"user/getOpenPaasUserData",
|
||||
|
||||
Reference in New Issue
Block a user