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();
|
jest.clearAllMocks();
|
||||||
const dispatch = jest.fn() as ThunkDispatch<any, any, any>;
|
const dispatch = jest.fn() as ThunkDispatch<any, any, any>;
|
||||||
jest.spyOn(appHooks, "useAppDispatch").mockReturnValue(dispatch);
|
jest.spyOn(appHooks, "useAppDispatch").mockReturnValue(dispatch);
|
||||||
jest.useFakeTimers(); // optional
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderCalendar = () => {
|
const renderCalendar = () => {
|
||||||
@@ -36,7 +35,9 @@ describe("CalendarApp integration", () => {
|
|||||||
list: {
|
list: {
|
||||||
"667037022b752d0026472254/cal1": {
|
"667037022b752d0026472254/cal1": {
|
||||||
name: "Calendar 1",
|
name: "Calendar 1",
|
||||||
|
id: "667037022b752d0026472254/cal1",
|
||||||
color: "#FF0000",
|
color: "#FF0000",
|
||||||
|
ownerEmails: ["alice@example.com"],
|
||||||
events: {
|
events: {
|
||||||
event1: {
|
event1: {
|
||||||
id: "event1",
|
id: "event1",
|
||||||
@@ -45,6 +46,21 @@ describe("CalendarApp integration", () => {
|
|||||||
title: "Test Event",
|
title: "Test Event",
|
||||||
start: start.toISOString(),
|
start: start.toISOString(),
|
||||||
end: end.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": {
|
"667037022b752d0026472254/cal1": {
|
||||||
name: "Calendar 1",
|
name: "Calendar 1",
|
||||||
color: "#FF0000",
|
color: "#FF0000",
|
||||||
|
ownerEmails: ["test@test.com"],
|
||||||
events: {
|
events: {
|
||||||
event1: {
|
event1: {
|
||||||
calId: "667037022b752d0026472254/cal1",
|
calId: "667037022b752d0026472254/cal1",
|
||||||
@@ -139,6 +140,7 @@ describe("Found Bugs", () => {
|
|||||||
"667037022b752d0026472254/cal1": {
|
"667037022b752d0026472254/cal1": {
|
||||||
name: "Calendar 1",
|
name: "Calendar 1",
|
||||||
color: "#FF0000",
|
color: "#FF0000",
|
||||||
|
ownerEmails: ["test.test@test.com"],
|
||||||
events: {
|
events: {
|
||||||
event1: {
|
event1: {
|
||||||
calId: "667037022b752d0026472254/cal1",
|
calId: "667037022b752d0026472254/cal1",
|
||||||
|
|||||||
@@ -169,4 +169,286 @@ describe("Event Display", () => {
|
|||||||
|
|
||||||
expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
|
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"],
|
["SUMMARY", {}, "text", "Team Meeting"],
|
||||||
["DESCRIPTION", {}, "text", "Discuss roadmap"],
|
["DESCRIPTION", {}, "text", "Discuss roadmap"],
|
||||||
["LOCATION", {}, "text", "Zoom"],
|
["LOCATION", {}, "text", "Zoom"],
|
||||||
["ORGANIZER", { cn: "Alice" }, "cal-address", "mailto:alice@example.com"],
|
["ORGANIZER", { cn: "Alice" }, "cal-address", "alice@example.com"],
|
||||||
[
|
[
|
||||||
"ATTENDEE",
|
"ATTENDEE",
|
||||||
{ cn: "Bob", partstat: "ACCEPTED" },
|
{ cn: "Bob", partstat: "ACCEPTED" },
|
||||||
"cal-address",
|
"cal-address",
|
||||||
"mailto:bob@example.com",
|
"bob@example.com",
|
||||||
],
|
],
|
||||||
["X-OPENPAAS-VIDEOCONFERENCE", {}, "text", "https://meet.link"],
|
["X-OPENPAAS-VIDEOCONFERENCE", {}, "text", "https://meet.link"],
|
||||||
["STATUS", {}, "text", "CONFIRMED"],
|
["STATUS", {}, "text", "CONFIRMED"],
|
||||||
@@ -98,8 +98,8 @@ describe("parseCalendarEvent", () => {
|
|||||||
const rawData = [
|
const rawData = [
|
||||||
["UID", {}, "text", "event-4"],
|
["UID", {}, "text", "event-4"],
|
||||||
["DTSTART", {}, "date-time", "2025-07-18T09:00:00Z"],
|
["DTSTART", {}, "date-time", "2025-07-18T09:00:00Z"],
|
||||||
["ATTENDEE", {}, "cal-address", "mailto:john@example.com"],
|
["ATTENDEE", {}, "cal-address", "john@example.com"],
|
||||||
["ORGANIZER", {}, "cal-address", "mailto:jane@example.com"],
|
["ORGANIZER", {}, "cal-address", "jane@example.com"],
|
||||||
] as unknown as [string, Record<string, string>, string, any];
|
] as unknown as [string, Record<string, string>, string, any];
|
||||||
|
|
||||||
const result = parseCalendarEvent(rawData, baseColor, calendarId);
|
const result = parseCalendarEvent(rawData, baseColor, calendarId);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { clientConfig } from "../../../src/features/User/oidcAuth";
|
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";
|
import { api } from "../../../src/utils/apiUtils";
|
||||||
|
|
||||||
jest.mock("../../../src/utils/apiUtils");
|
jest.mock("../../../src/utils/apiUtils");
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ main {
|
|||||||
height: 90vh;
|
height: 90vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.calendarListHeader {
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
.sidebar {
|
.sidebar {
|
||||||
border-right: 2px gray;
|
border-right: 2px gray;
|
||||||
width: 20vw;
|
width: 20vw;
|
||||||
@@ -27,6 +30,18 @@ main {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.declined-event {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tentative-event {
|
||||||
|
}
|
||||||
|
|
||||||
|
.needs-action-event {
|
||||||
|
opacity: 0.7;
|
||||||
|
border: dashed 1px #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
.fc .fc-timegrid-slot {
|
.fc .fc-timegrid-slot {
|
||||||
height: 30px !important;
|
height: 30px !important;
|
||||||
min-height: 30px !important;
|
min-height: 30px !important;
|
||||||
|
|||||||
@@ -27,12 +27,17 @@ import { Calendars } from "../../features/Calendars/CalendarTypes";
|
|||||||
import { push } from "redux-first-history";
|
import { push } from "redux-first-history";
|
||||||
import EventDisplayModal from "../../features/Events/EventDisplay";
|
import EventDisplayModal from "../../features/Events/EventDisplay";
|
||||||
import { createSelector } from "@reduxjs/toolkit";
|
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() {
|
export default function CalendarApp() {
|
||||||
const calendarRef = useRef<CalendarApi | null>(null);
|
const calendarRef = useRef<CalendarApi | null>(null);
|
||||||
const [selectedDate, setSelectedDate] = useState(new Date());
|
const [selectedDate, setSelectedDate] = useState(new Date());
|
||||||
const [selectedMiniDate, setSelectedMiniDate] = useState(new Date());
|
const [selectedMiniDate, setSelectedMiniDate] = useState(new Date());
|
||||||
const tokens = useAppSelector((state) => state.user.tokens);
|
const tokens = useAppSelector((state) => state.user.tokens);
|
||||||
|
const user = useAppSelector((state) => state.user.userData);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
if (!tokens) {
|
if (!tokens) {
|
||||||
@@ -372,6 +377,82 @@ export default function CalendarApp() {
|
|||||||
center: "prev,today,next",
|
center: "prev,today,next",
|
||||||
right: "dayGridMonth,timeGridWeek,timeGridDay",
|
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
|
<EventPopover
|
||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
|
|||||||
@@ -7,11 +7,17 @@ export default function CalendarSelection({
|
|||||||
selectedCalendars: string[];
|
selectedCalendars: string[];
|
||||||
setSelectedCalendars: Function;
|
setSelectedCalendars: Function;
|
||||||
}) {
|
}) {
|
||||||
const tokens = useAppSelector((state) => state.user.tokens);
|
|
||||||
const userId = useAppSelector((state) => state.user.userData.openpaasId);
|
const userId = useAppSelector((state) => state.user.userData.openpaasId);
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const calendars = useAppSelector((state) => state.calendars.list);
|
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) => {
|
const handleCalendarToggle = (name: string) => {
|
||||||
setSelectedCalendars((prev: string[]) =>
|
setSelectedCalendars((prev: string[]) =>
|
||||||
prev.includes(name) ? prev.filter((n) => n !== name) : [...prev, name]
|
prev.includes(name) ? prev.filter((n) => n !== name) : [...prev, name]
|
||||||
@@ -20,28 +26,11 @@ export default function CalendarSelection({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3>personnalCalendars</h3>
|
<span className="calendarListHeader">
|
||||||
{Object.keys(calendars)
|
<h3>Personnal Calendars</h3>
|
||||||
.filter((id) => id.split("/")[0] === userId)
|
</span>
|
||||||
.map((id) => {
|
{personnalCalendars.map((id) => {
|
||||||
return (
|
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) => (
|
|
||||||
<div key={id}>
|
<div key={id}>
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
@@ -53,7 +42,48 @@ export default function CalendarSelection({
|
|||||||
{calendars[id].name}
|
{calendars[id].name}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ 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 getOpenPaasUser from "../User/userAPI";
|
import { getOpenPaasUser, getUserDetails } 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";
|
||||||
import { responsiveFontSizes } from "@mui/material";
|
|
||||||
|
|
||||||
export const getCalendarsListAsync = createAsyncThunk<
|
export const getCalendarsListAsync = createAsyncThunk<
|
||||||
Record<string, Calendars> // Return type
|
Record<string, Calendars> // Return type
|
||||||
@@ -19,14 +18,28 @@ export const getCalendarsListAsync = createAsyncThunk<
|
|||||||
for (const cal of rawCalendars) {
|
for (const cal of rawCalendars) {
|
||||||
const name = cal["dav:name"];
|
const name = cal["dav:name"];
|
||||||
const description = cal["dav:description"];
|
const description = cal["dav:description"];
|
||||||
const id = cal["calendarserver:source"]
|
let delegated = false;
|
||||||
|
let source = cal["calendarserver:source"]
|
||||||
? cal["calendarserver:source"]._links.self.href
|
? cal["calendarserver:source"]._links.self.href
|
||||||
.replace("/calendars/", "")
|
: cal._links.self.href;
|
||||||
.replace(".json", "")
|
if (cal["calendarserver:delegatedsource"]) {
|
||||||
: cal._links.self.href.replace("/calendars/", "").replace(".json", "");
|
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"];
|
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;
|
return importedCalendars;
|
||||||
@@ -41,8 +54,9 @@ export const getCalendarDetailAsync = createAsyncThunk<
|
|||||||
const events: CalendarEvent[] = calendar._embedded["dav:item"].flatMap(
|
const events: CalendarEvent[] = calendar._embedded["dav:item"].flatMap(
|
||||||
(eventdata: any) => {
|
(eventdata: any) => {
|
||||||
const vevents = eventdata.data[2] as any[][]; // array of ['vevent', RawEntry[], []]
|
const vevents = eventdata.data[2] as any[][]; // array of ['vevent', RawEntry[], []]
|
||||||
|
const eventURL = eventdata._links.self.href;
|
||||||
return vevents.map((vevent: any[]) => {
|
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
|
{ calId: string; events: CalendarEvent[] }, // Return type
|
||||||
{ cal: Calendars; newEvent: CalendarEvent } // Arg type
|
{ cal: Calendars; newEvent: CalendarEvent } // Arg type
|
||||||
>("calendars/putEvent", async ({ cal, newEvent }) => {
|
>("calendars/putEvent", async ({ cal, newEvent }) => {
|
||||||
const response = await putEvent(cal, newEvent);
|
const response = await putEvent(newEvent);
|
||||||
const calEvents = (await getCalendar(cal.id, {
|
const calEvents = (await getCalendar(cal.id, {
|
||||||
start: formatDateToYYYYMMDDTHHMMSS(newEvent.start),
|
start: formatDateToYYYYMMDDTHHMMSS(new Date(newEvent.start)),
|
||||||
end: formatDateToYYYYMMDDTHHMMSS(
|
end: formatDateToYYYYMMDDTHHMMSS(
|
||||||
new Date(newEvent.start.getTime() + 86400000)
|
new Date(new Date(newEvent.start).getTime() + 86400000)
|
||||||
),
|
),
|
||||||
})) as Record<string, any>;
|
})) as Record<string, any>;
|
||||||
const events: CalendarEvent[] = calEvents._embedded["dav:item"].flatMap(
|
const events: CalendarEvent[] = calEvents._embedded["dav:item"].flatMap(
|
||||||
(eventdata: any) => {
|
(eventdata: any) => {
|
||||||
const vevents = eventdata.data[2] as any[][];
|
const vevents = eventdata.data[2] as any[][];
|
||||||
|
const eventURL = eventdata._links.self.href;
|
||||||
return vevents.map((vevent: any[]) => {
|
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<
|
export const deleteEventAsync = createAsyncThunk<
|
||||||
{ calId: string; eventId: string }, // Return type
|
{ calId: string; eventId: string }, // Return type
|
||||||
{ calId: string; eventId: string } // Arg type
|
{ calId: string; eventId: string; eventURL: string } // Arg type
|
||||||
>("calendars/delEvent", async ({ calId, eventId }) => {
|
>("calendars/delEvent", async ({ calId, eventId, eventURL }) => {
|
||||||
const response = await deleteEvent(calId, eventId);
|
const response = await deleteEvent(eventURL);
|
||||||
return { calId, eventId };
|
return { calId, eventId };
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -105,6 +120,11 @@ const CalendarSlice = createSlice({
|
|||||||
}
|
}
|
||||||
state.list[action.payload.calendarUid].events[action.payload.event.uid] =
|
state.list[action.payload.calendarUid].events[action.payload.event.uid] =
|
||||||
action.payload.event;
|
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: (
|
removeEvent: (
|
||||||
state,
|
state,
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import { CalendarEvent } from "../Events/EventsTypes";
|
|||||||
export interface Calendars {
|
export interface Calendars {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
delegated?: boolean;
|
||||||
prodid?: string;
|
prodid?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
|
ownerEmails?: string[];
|
||||||
description?: string;
|
description?: string;
|
||||||
calscale?: string;
|
calscale?: string;
|
||||||
version?: string;
|
version?: string;
|
||||||
|
|||||||
@@ -1,28 +1,24 @@
|
|||||||
import { api } from "../../utils/apiUtils";
|
import { api } from "../../utils/apiUtils";
|
||||||
import { Calendars } from "../Calendars/CalendarTypes";
|
|
||||||
import { CalendarEvent } from "./EventsTypes";
|
import { CalendarEvent } from "./EventsTypes";
|
||||||
import { calendarEventToJCal } from "./eventUtils";
|
import { calendarEventToJCal } from "./eventUtils";
|
||||||
|
|
||||||
export async function putEvent(cal: Calendars, event: CalendarEvent) {
|
export async function putEvent(event: CalendarEvent) {
|
||||||
const response = await api(
|
const response = await api(`dav${event.URL}`, {
|
||||||
`dav/calendars/${cal.id}/${event.uid.split("/")[0]}.isc`,
|
method: "PUT",
|
||||||
{
|
body: JSON.stringify(calendarEventToJCal(event)),
|
||||||
method: "PUT",
|
headers: {
|
||||||
body: JSON.stringify(calendarEventToJCal(event)),
|
"content-type": "text/calendar; charset=utf-8",
|
||||||
headers: {
|
},
|
||||||
"content-type": "text/calendar; charset=utf-8",
|
});
|
||||||
},
|
if (response.status === 201) {
|
||||||
}
|
console.log("PUT (201) :", response.url);
|
||||||
).json();
|
}
|
||||||
return response;
|
return await response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteEvent(calId: string, eventId: string) {
|
export async function deleteEvent(eventURL: string) {
|
||||||
const response = await api(
|
const response = await api(eventURL, {
|
||||||
`dav/calendars/${calId}/${eventId.split("/")[0]}.isc`,
|
method: "DELETE",
|
||||||
{
|
}).json();
|
||||||
method: "DELETE",
|
|
||||||
}
|
|
||||||
).json();
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { deleteEventAsync } from "../Calendars/CalendarSlice";
|
import { deleteEventAsync, putEventAsync } from "../Calendars/CalendarSlice";
|
||||||
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
@@ -25,6 +25,7 @@ import CircleIcon from "@mui/icons-material/Circle";
|
|||||||
import CancelIcon from "@mui/icons-material/Cancel";
|
import CancelIcon from "@mui/icons-material/Cancel";
|
||||||
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
|
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
|
||||||
import { userAttendee } from "../User/userDataTypes";
|
import { userAttendee } from "../User/userDataTypes";
|
||||||
|
import { putEvent } from "./EventApi";
|
||||||
|
|
||||||
function EventDisplayModal({
|
function EventDisplayModal({
|
||||||
eventId,
|
eventId,
|
||||||
@@ -74,6 +75,18 @@ function EventDisplayModal({
|
|||||||
(a) => a.cal_address === event.organizer?.cal_address
|
(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 (
|
return (
|
||||||
<Popover open={open} anchorEl={anchorEl} onClose={onClose}>
|
<Popover open={open} anchorEl={anchorEl} onClose={onClose}>
|
||||||
<Card sx={{ width: 300, p: 2, position: "relative" }}>
|
<Card sx={{ width: 300, p: 2, position: "relative" }}>
|
||||||
@@ -195,9 +208,36 @@ function EventDisplayModal({
|
|||||||
Will you attend?
|
Will you attend?
|
||||||
</Typography>
|
</Typography>
|
||||||
<ButtonGroup size="small" fullWidth>
|
<ButtonGroup size="small" fullWidth>
|
||||||
<Button>Accept</Button>
|
<Button
|
||||||
<Button>Maybe</Button>
|
color={
|
||||||
<Button>Decline</Button>
|
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>
|
</ButtonGroup>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { userAttendee, userOrganiser } from "../User/userDataTypes";
|
import { userAttendee, userOrganiser } from "../User/userDataTypes";
|
||||||
|
|
||||||
export interface CalendarEvent {
|
export interface CalendarEvent {
|
||||||
|
URL: string;
|
||||||
calId: string;
|
calId: string;
|
||||||
uid: string;
|
uid: string;
|
||||||
transp?: string;
|
transp?: string;
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ type RawEntry = [string, Record<string, string>, string, any];
|
|||||||
export function parseCalendarEvent(
|
export function parseCalendarEvent(
|
||||||
data: RawEntry[],
|
data: RawEntry[],
|
||||||
color: string,
|
color: string,
|
||||||
calendarid: string
|
calendarid: string,
|
||||||
|
eventURL: string
|
||||||
): CalendarEvent {
|
): CalendarEvent {
|
||||||
const event: Partial<CalendarEvent> = { color, attendee: [] };
|
const event: Partial<CalendarEvent> = { color, attendee: [] };
|
||||||
let recurrenceId;
|
let recurrenceId;
|
||||||
@@ -75,6 +76,7 @@ export function parseCalendarEvent(
|
|||||||
event.uid = `${event.uid}/${recurrenceId}`;
|
event.uid = `${event.uid}/${recurrenceId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
event.URL = eventURL;
|
||||||
if (!event.uid || !event.start) {
|
if (!event.uid || !event.start) {
|
||||||
console.error(
|
console.error(
|
||||||
`missing crucial event param in calendar ${calendarid} `,
|
`missing crucial event param in calendar ${calendarid} `,
|
||||||
@@ -98,7 +100,7 @@ export function calendarEventToJCal(event: CalendarEvent): any[] {
|
|||||||
"dtstart",
|
"dtstart",
|
||||||
{ tzid },
|
{ tzid },
|
||||||
event.allday ? "date" : "date-time",
|
event.allday ? "date" : "date-time",
|
||||||
formatDateToICal(event.start, event.allday ?? false),
|
formatDateToICal(new Date(event.start), event.allday ?? false),
|
||||||
],
|
],
|
||||||
["class", {}, "text", event.class ?? "PUBLIC"],
|
["class", {}, "text", event.class ?? "PUBLIC"],
|
||||||
[
|
[
|
||||||
@@ -117,7 +119,7 @@ export function calendarEventToJCal(event: CalendarEvent): any[] {
|
|||||||
"dtend",
|
"dtend",
|
||||||
{ tzid },
|
{ tzid },
|
||||||
event.allday ? "date" : "date-time",
|
event.allday ? "date" : "date-time",
|
||||||
formatDateToICal(event.end, event.allday ?? false),
|
formatDateToICal(new Date(event.end), event.allday ?? false),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
if (event.organizer) {
|
if (event.organizer) {
|
||||||
@@ -139,15 +141,18 @@ export function calendarEventToJCal(event: CalendarEvent): any[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
event.attendee.forEach((att) => {
|
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([
|
vevent[1].push([
|
||||||
"attendee",
|
"attendee",
|
||||||
{
|
attendee,
|
||||||
cn: att.cn,
|
|
||||||
partstat: att.partstat,
|
|
||||||
rsvp: att.rsvp,
|
|
||||||
role: att.role,
|
|
||||||
cutype: att.cutype,
|
|
||||||
},
|
|
||||||
"cal-address",
|
"cal-address",
|
||||||
`mailto:${att.cal_address}`,
|
`mailto:${att.cal_address}`,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { api } from "../../utils/apiUtils";
|
import { api } from "../../utils/apiUtils";
|
||||||
|
|
||||||
export default async function getOpenPaasUser() {
|
export async function getOpenPaasUser() {
|
||||||
const user = await api.get(`api/user`).json();
|
const user = await api.get(`api/user`).json();
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
@@ -25,3 +25,8 @@ export async function searchUsers(query: string) {
|
|||||||
avatarUrl: user.photos?.[0]?.url || "",
|
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 {
|
export interface userAttendee {
|
||||||
cn: string;
|
cn?: string;
|
||||||
cal_address: string;
|
cal_address: string;
|
||||||
partstat: string;
|
partstat: string;
|
||||||
rsvp: string;
|
rsvp: string;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
|
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
|
||||||
import { userData, userOrganiser } from "./userDataTypes";
|
import { userData, userOrganiser } from "./userDataTypes";
|
||||||
import getOpenPaasUser from "./userAPI";
|
import { getOpenPaasUser } from "./userAPI";
|
||||||
|
|
||||||
export const getOpenPaasUserDataAsync = createAsyncThunk<any>(
|
export const getOpenPaasUserDataAsync = createAsyncThunk<any>(
|
||||||
"user/getOpenPaasUserData",
|
"user/getOpenPaasUserData",
|
||||||
|
|||||||
Reference in New Issue
Block a user