sidebar availability search (#128)
* [123] added people search in left side bar * [#123] added toggle temp calendars * [#123] added tests] * fixup! [123] added people search in left side bar * fixup! [#123] added toggle temp calendars * [#123] fixed event creation on Enter * fixup! [#123] fixed event creation on Enter * fixup! [#123] fixed event creation on Enter * fixup! [#123] fixed event creation on Enter * fixup! [#123] fixed event creation on Enter * fixup! [#123] fixed event creation on Enter * fixup! [#123] fixed event creation on Enter * fixup! [#123] fixed event creation on Enter * [#123] added color diff for temp calendars * fixup! [#123] fixed event creation on Enter * fixup! [#123] added tests] * fixup! [#123] added toggle temp calendars --------- Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
@@ -2,6 +2,13 @@ import { fireEvent, screen, waitFor } from "@testing-library/react";
|
|||||||
import CalendarApp from "../../src/components/Calendar/Calendar";
|
import CalendarApp from "../../src/components/Calendar/Calendar";
|
||||||
import * as eventThunks from "../../src/features/Calendars/CalendarSlice";
|
import * as eventThunks from "../../src/features/Calendars/CalendarSlice";
|
||||||
import { renderWithProviders } from "../utils/Renderwithproviders";
|
import { renderWithProviders } from "../utils/Renderwithproviders";
|
||||||
|
import { searchUsers } from "../../src/features/User/userAPI";
|
||||||
|
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
jest.mock("../../src/features/User/userAPI");
|
||||||
|
const mockedSearchUsers = searchUsers as jest.MockedFunction<
|
||||||
|
typeof searchUsers
|
||||||
|
>;
|
||||||
|
|
||||||
describe("CalendarSelection", () => {
|
describe("CalendarSelection", () => {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
@@ -157,15 +164,93 @@ describe("CalendarSelection", () => {
|
|||||||
expect(screen.getByLabelText("Calendar delegated")).toBeInTheDocument();
|
expect(screen.getByLabelText("Calendar delegated")).toBeInTheDocument();
|
||||||
expect(screen.getByLabelText("Calendar shared")).toBeInTheDocument();
|
expect(screen.getByLabelText("Calendar shared")).toBeInTheDocument();
|
||||||
|
|
||||||
const delegatedAccordionSummary = screen
|
const sharedAccordionSummary = screen
|
||||||
.getByText("Delegated Calendars")
|
.getByText("Other Calendars")
|
||||||
.closest(".MuiAccordionSummary-root");
|
.closest(".MuiAccordionSummary-root");
|
||||||
|
|
||||||
const addButton = screen.getAllByTestId("AddIcon")[1];
|
const addButton = screen.getAllByTestId("AddIcon")[2];
|
||||||
fireEvent.click(addButton);
|
fireEvent.click(addButton);
|
||||||
expect(delegatedAccordionSummary).toHaveAttribute("aria-expanded", "true");
|
expect(sharedAccordionSummary).toHaveAttribute("aria-expanded", "true");
|
||||||
|
|
||||||
fireEvent.click(addButton);
|
fireEvent.click(addButton);
|
||||||
expect(delegatedAccordionSummary).toHaveAttribute("aria-expanded", "true");
|
expect(sharedAccordionSummary).toHaveAttribute("aria-expanded", "true");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("calendar Availability search", () => {
|
||||||
|
const preloadedState = {
|
||||||
|
user: {
|
||||||
|
userData: {
|
||||||
|
sub: "test",
|
||||||
|
email: "test@test.com",
|
||||||
|
sid: "mockSid",
|
||||||
|
openpaasId: "user1",
|
||||||
|
},
|
||||||
|
tokens: { accessToken: "token" },
|
||||||
|
},
|
||||||
|
calendars: {
|
||||||
|
list: {
|
||||||
|
"user1/cal1": {
|
||||||
|
name: "Calendar personnal",
|
||||||
|
id: "user1/cal1",
|
||||||
|
color: "#FF0000",
|
||||||
|
ownerEmails: ["alice@example.com"],
|
||||||
|
events: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pending: false,
|
||||||
|
templist: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it("imports temporary calendars when selecting new users", async () => {
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(eventThunks, "getTempCalendarsListAsync")
|
||||||
|
.mockImplementation((payload) => {
|
||||||
|
return () => Promise.resolve(payload) as any;
|
||||||
|
});
|
||||||
|
mockedSearchUsers.mockResolvedValueOnce([
|
||||||
|
{
|
||||||
|
email: "newuser@example.com",
|
||||||
|
displayName: "New User",
|
||||||
|
avatarUrl: "image.png",
|
||||||
|
openpaasId: "1234567890",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
renderWithProviders(<CalendarApp />, preloadedState);
|
||||||
|
|
||||||
|
const input = screen.getByPlaceholderText(/search user/i);
|
||||||
|
userEvent.type(input, "New");
|
||||||
|
|
||||||
|
const option = await screen.findByText("New User");
|
||||||
|
fireEvent.click(option);
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not import temp calendars if user already has a calendar but toggles the shared one", async () => {
|
||||||
|
mockedSearchUsers.mockResolvedValueOnce([
|
||||||
|
{
|
||||||
|
email: "alice@example.com",
|
||||||
|
displayName: "Alice",
|
||||||
|
avatarUrl: "image.png",
|
||||||
|
openpaasId: "1234567890",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(eventThunks, "getTempCalendarsListAsync")
|
||||||
|
.mockImplementation((payload) => {
|
||||||
|
return () => Promise.resolve(payload) as any;
|
||||||
|
});
|
||||||
|
renderWithProviders(<CalendarApp />, preloadedState);
|
||||||
|
|
||||||
|
const input = screen.getByPlaceholderText(/search user/i);
|
||||||
|
userEvent.type(input, "Alice");
|
||||||
|
|
||||||
|
const option = await screen.findByText("Alice");
|
||||||
|
fireEvent.click(option);
|
||||||
|
|
||||||
|
expect(spy).not.toHaveBeenCalledWith();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -190,9 +190,9 @@ describe("CalendarSelection", () => {
|
|||||||
.closest(".MuiAccordionSummary-root");
|
.closest(".MuiAccordionSummary-root");
|
||||||
|
|
||||||
fireEvent.click(delegatedAccordionSummary!);
|
fireEvent.click(delegatedAccordionSummary!);
|
||||||
expect(delegatedAccordionSummary).toHaveAttribute("aria-expanded", "false");
|
expect(delegatedAccordionSummary).toHaveAttribute("aria-expanded", "true");
|
||||||
|
|
||||||
fireEvent.click(delegatedAccordionSummary!);
|
fireEvent.click(delegatedAccordionSummary!);
|
||||||
expect(delegatedAccordionSummary).toHaveAttribute("aria-expanded", "true");
|
expect(delegatedAccordionSummary).toHaveAttribute("aria-expanded", "false");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { fireEvent, screen } from "@testing-library/react";
|
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
||||||
import EventDuplication from "../../src/components/Event/EventDuplicate";
|
import EventDuplication from "../../src/components/Event/EventDuplicate";
|
||||||
import EventDisplayModal from "../../src/features/Events/EventDisplay";
|
import EventDisplayModal from "../../src/features/Events/EventDisplay";
|
||||||
import EventPopover from "../../src/features/Events/EventModal";
|
import EventPopover from "../../src/features/Events/EventModal";
|
||||||
@@ -147,7 +147,7 @@ describe("EventPopover", () => {
|
|||||||
});
|
});
|
||||||
fireEvent.click(screen.getByRole("button", { name: /Save/i }));
|
fireEvent.click(screen.getByRole("button", { name: /Save/i }));
|
||||||
|
|
||||||
expect(onClose).toHaveBeenCalled();
|
waitFor(() => expect(onClose).toHaveBeenCalled());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { CalendarApi } from "@fullcalendar/core";
|
|||||||
import { jest } from "@jest/globals";
|
import { jest } from "@jest/globals";
|
||||||
import { ThunkDispatch } from "@reduxjs/toolkit";
|
import { ThunkDispatch } from "@reduxjs/toolkit";
|
||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
import { act, screen, within } from "@testing-library/react";
|
import { act, screen, waitFor, within } from "@testing-library/react";
|
||||||
import * as appHooks from "../../src/app/hooks";
|
import * as appHooks from "../../src/app/hooks";
|
||||||
import CalendarApp from "../../src/components/Calendar/Calendar";
|
import CalendarApp from "../../src/components/Calendar/Calendar";
|
||||||
import { renderWithProviders } from "../utils/Renderwithproviders";
|
import { renderWithProviders } from "../utils/Renderwithproviders";
|
||||||
@@ -97,7 +97,7 @@ describe("CalendarApp integration", () => {
|
|||||||
|
|
||||||
fcEvent?.setEnd(newEnd);
|
fcEvent?.setEnd(newEnd);
|
||||||
|
|
||||||
expect(dispatch).toHaveBeenCalled();
|
waitFor(() => expect(dispatch).toHaveBeenCalled());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { screen, fireEvent, waitFor } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import {
|
||||||
|
User,
|
||||||
|
PeopleSearch,
|
||||||
|
} from "../../src/components/Attendees/PeopleSearch";
|
||||||
|
import { renderWithProviders } from "../utils/Renderwithproviders";
|
||||||
|
import { searchUsers } from "../../src/features/User/userAPI";
|
||||||
|
|
||||||
|
jest.mock("../../src/features/User/userAPI");
|
||||||
|
const mockedSearchUsers = searchUsers as jest.MockedFunction<
|
||||||
|
typeof searchUsers
|
||||||
|
>;
|
||||||
|
|
||||||
|
describe("PeopleSearch", () => {
|
||||||
|
const baseUser: User = {
|
||||||
|
email: "test@example.com",
|
||||||
|
displayName: "Test User",
|
||||||
|
avatarUrl: "https://example.com/avatar.png",
|
||||||
|
openpaasId: "1234567890",
|
||||||
|
};
|
||||||
|
|
||||||
|
function setup(
|
||||||
|
selectedUsers: User[] = [],
|
||||||
|
props?: Partial<React.ComponentProps<typeof PeopleSearch>>
|
||||||
|
) {
|
||||||
|
const onChange = jest.fn();
|
||||||
|
renderWithProviders(
|
||||||
|
<PeopleSearch
|
||||||
|
selectedUsers={selectedUsers}
|
||||||
|
onChange={onChange}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
return { onChange };
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
mockedSearchUsers.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls searchUsers after debounce when typing", async () => {
|
||||||
|
mockedSearchUsers.mockResolvedValueOnce([baseUser]);
|
||||||
|
setup();
|
||||||
|
|
||||||
|
const input = screen.getByRole("combobox");
|
||||||
|
await userEvent.type(input, "Test");
|
||||||
|
jest.advanceTimersByTime(300);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockedSearchUsers).toHaveBeenCalledWith("Test", ["user"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders search results and allows selection", async () => {
|
||||||
|
mockedSearchUsers.mockResolvedValueOnce([baseUser]);
|
||||||
|
const { onChange } = setup();
|
||||||
|
|
||||||
|
const input = screen.getByRole("combobox");
|
||||||
|
await userEvent.type(input, "Test");
|
||||||
|
jest.advanceTimersByTime(300);
|
||||||
|
|
||||||
|
const option = await screen.findByText("Test User");
|
||||||
|
await userEvent.click(option);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(onChange).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not show already selected users in options", async () => {
|
||||||
|
mockedSearchUsers.mockResolvedValueOnce([baseUser]);
|
||||||
|
setup([baseUser]);
|
||||||
|
const input = screen.getByRole("combobox");
|
||||||
|
await userEvent.type(input, "Test");
|
||||||
|
jest.advanceTimersByTime(300);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText("test@example.com")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("triggers onToggleEventPreview on Enter key press", () => {
|
||||||
|
const onToggleEventPreview = jest.fn();
|
||||||
|
setup([], { onToggleEventPreview });
|
||||||
|
|
||||||
|
const input = screen.getByRole("combobox");
|
||||||
|
fireEvent.keyDown(input, { key: "Enter" });
|
||||||
|
|
||||||
|
expect(onToggleEventPreview).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("respects disabled state", () => {
|
||||||
|
setup([], { disabled: true });
|
||||||
|
expect(screen.getByRole("combobox")).toBeDisabled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -17,26 +17,33 @@ import TextField from "@mui/material/TextField";
|
|||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { searchUsers } from "../../features/User/userAPI";
|
import { searchUsers } from "../../features/User/userAPI";
|
||||||
|
import PeopleOutlineOutlinedIcon from "@mui/icons-material/PeopleOutlineOutlined";
|
||||||
|
import Chip from "@mui/material/Chip";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
email: string;
|
email: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
openpaasId: string;
|
openpaasId: string;
|
||||||
|
color?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PeopleSearch({
|
export function PeopleSearch({
|
||||||
selectedUsers,
|
selectedUsers,
|
||||||
onChange,
|
onChange,
|
||||||
disabled,
|
disabled,
|
||||||
|
onToggleEventPreview,
|
||||||
}: {
|
}: {
|
||||||
selectedUsers: User[];
|
selectedUsers: User[];
|
||||||
onChange: Function;
|
onChange: Function;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
onToggleEventPreview?: Function;
|
||||||
}) {
|
}) {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [options, setOptions] = useState<User[]>([]);
|
const [options, setOptions] = useState<User[]>([]);
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const delayDebounceFn = setTimeout(async () => {
|
const delayDebounceFn = setTimeout(async () => {
|
||||||
@@ -67,10 +74,25 @@ export function PeopleSearch({
|
|||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...params}
|
{...params}
|
||||||
|
placeholder="Search user"
|
||||||
label="Search user"
|
label="Search user"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" && onToggleEventPreview) {
|
||||||
|
e.preventDefault();
|
||||||
|
onToggleEventPreview();
|
||||||
|
}
|
||||||
|
}}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
input: {
|
input: {
|
||||||
...params.InputProps,
|
...params.InputProps,
|
||||||
|
startAdornment: (
|
||||||
|
<>
|
||||||
|
<PeopleOutlineOutlinedIcon
|
||||||
|
sx={{ mr: 1, color: "action.active" }}
|
||||||
|
/>
|
||||||
|
{params.InputProps.startAdornment}
|
||||||
|
</>
|
||||||
|
),
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
<>
|
<>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
@@ -101,6 +123,18 @@ export function PeopleSearch({
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
renderValue={(value, getTagProps) =>
|
||||||
|
value.map((option, index) => (
|
||||||
|
<Chip
|
||||||
|
{...getTagProps({ index })}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: option.color,
|
||||||
|
color: theme.palette.getContrastText(option.color ?? "#ffffffff"),
|
||||||
|
}}
|
||||||
|
label={option.displayName}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import interactionPlugin from "@fullcalendar/interaction";
|
|||||||
import { CalendarApi, DateSelectArg } from "@fullcalendar/core";
|
import { CalendarApi, DateSelectArg } from "@fullcalendar/core";
|
||||||
import ReactCalendar from "react-calendar";
|
import ReactCalendar from "react-calendar";
|
||||||
import "./Calendar.css";
|
import "./Calendar.css";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
||||||
import EventPopover from "../../features/Events/EventModal";
|
import EventPopover from "../../features/Events/EventModal";
|
||||||
import CalendarPopover from "../../features/Calendars/CalendarModal";
|
import CalendarPopover from "../../features/Calendars/CalendarModal";
|
||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
} from "../../features/Calendars/CalendarSlice";
|
} from "../../features/Calendars/CalendarSlice";
|
||||||
import ImportAlert from "../../features/Events/ImportAlert";
|
import ImportAlert from "../../features/Events/ImportAlert";
|
||||||
import {
|
import {
|
||||||
|
computeStartOfTheWeek,
|
||||||
formatDateToYYYYMMDDTHHMMSS,
|
formatDateToYYYYMMDDTHHMMSS,
|
||||||
getCalendarRange,
|
getCalendarRange,
|
||||||
getDeltaInMilliseconds,
|
getDeltaInMilliseconds,
|
||||||
@@ -35,15 +36,9 @@ import AddIcon from "@mui/icons-material/Add";
|
|||||||
import AccessTimeIcon from "@mui/icons-material/AccessTime";
|
import AccessTimeIcon from "@mui/icons-material/AccessTime";
|
||||||
import LockIcon from "@mui/icons-material/Lock";
|
import LockIcon from "@mui/icons-material/Lock";
|
||||||
import { userAttendee } from "../../features/User/userDataTypes";
|
import { userAttendee } from "../../features/User/userDataTypes";
|
||||||
|
import { TempCalendarsInput } from "./TempCalendarsInput";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
|
|
||||||
const computeStartOfTheWeek = (date: Date): Date => {
|
|
||||||
const startOfWeek = new Date(date);
|
|
||||||
startOfWeek.setDate(date.getDate() - ((date.getDay() + 6) % 7)); // Monday
|
|
||||||
startOfWeek.setHours(0, 0, 0, 0);
|
|
||||||
return startOfWeek;
|
|
||||||
};
|
|
||||||
|
|
||||||
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());
|
||||||
@@ -57,6 +52,8 @@ export default function CalendarApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const calendars = useAppSelector((state) => state.calendars.list);
|
const calendars = useAppSelector((state) => state.calendars.list);
|
||||||
|
const tempcalendars =
|
||||||
|
useAppSelector((state) => state.calendars.templist) ?? {};
|
||||||
const pending = useAppSelector((state) => state.calendars.pending);
|
const pending = useAppSelector((state) => state.calendars.pending);
|
||||||
const userId =
|
const userId =
|
||||||
useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
|
useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
|
||||||
@@ -106,35 +103,39 @@ export default function CalendarApp() {
|
|||||||
calendarRange.start
|
calendarRange.start
|
||||||
)}_${formatDateToYYYYMMDDTHHMMSS(calendarRange.end)}`;
|
)}_${formatDateToYYYYMMDDTHHMMSS(calendarRange.end)}`;
|
||||||
|
|
||||||
let filteredEvents: CalendarEvent[] = [];
|
let filteredEvents: CalendarEvent[] = extractEvents(
|
||||||
selectedCalendars.forEach((id) => {
|
selectedCalendars,
|
||||||
if (calendars[id].events) {
|
calendars
|
||||||
filteredEvents = filteredEvents
|
);
|
||||||
.concat(
|
|
||||||
Object.keys(calendars[id].events).map(
|
let filteredTempEvents: CalendarEvent[] = extractEvents(
|
||||||
(eventid) => calendars[id].events[eventid]
|
Object.keys(tempcalendars),
|
||||||
)
|
tempcalendars
|
||||||
)
|
);
|
||||||
.filter((event) => !(event.status === "CANCELLED"));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
selectedCalendars.forEach((id) => {
|
updateCalsDetails(
|
||||||
if (!pending && rangeKey) {
|
selectedCalendars,
|
||||||
dispatch(
|
pending,
|
||||||
getCalendarDetailAsync({
|
calendars,
|
||||||
calId: id,
|
rangeKey,
|
||||||
match: {
|
dispatch,
|
||||||
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
|
calendarRange
|
||||||
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
|
);
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [rangeKey, selectedCalendars]);
|
}, [rangeKey, selectedCalendars]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateCalsDetails(
|
||||||
|
Object.keys(tempcalendars),
|
||||||
|
pending,
|
||||||
|
tempcalendars,
|
||||||
|
rangeKey,
|
||||||
|
dispatch,
|
||||||
|
calendarRange,
|
||||||
|
"temp"
|
||||||
|
);
|
||||||
|
}, [rangeKey, Object.keys(tempcalendars).join(",")]);
|
||||||
|
|
||||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
||||||
const [anchorPosition, setAnchorPosition] = useState<{
|
const [anchorPosition, setAnchorPosition] = useState<{
|
||||||
top: number;
|
top: number;
|
||||||
@@ -142,11 +143,16 @@ export default function CalendarApp() {
|
|||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [openEventDisplay, setOpenEventDisplay] = useState(false);
|
const [openEventDisplay, setOpenEventDisplay] = useState(false);
|
||||||
const [eventDisplayedId, setEventDisplayedId] = useState("");
|
const [eventDisplayedId, setEventDisplayedId] = useState("");
|
||||||
|
const [eventDisplayedTemp, setEventDisplayedTemp] = useState(false);
|
||||||
const [eventDisplayedCalId, setEventDisplayedCalId] = useState("");
|
const [eventDisplayedCalId, setEventDisplayedCalId] = useState("");
|
||||||
const [selectedRange, setSelectedRange] = useState<DateSelectArg | null>(
|
const [selectedRange, setSelectedRange] = useState<DateSelectArg | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [tempEvent, setTempEvent] = useState<CalendarEvent>(
|
||||||
|
{} as CalendarEvent
|
||||||
|
);
|
||||||
|
|
||||||
const handleDateSelect = (selectInfo: DateSelectArg) => {
|
const handleDateSelect = (selectInfo: DateSelectArg) => {
|
||||||
setSelectedRange(selectInfo);
|
setSelectedRange(selectInfo);
|
||||||
setAnchorEl(document.body); // fallback: we could use selectInfo.jsEvent.target if from a click
|
setAnchorEl(document.body); // fallback: we could use selectInfo.jsEvent.target if from a click
|
||||||
@@ -156,6 +162,29 @@ export default function CalendarApp() {
|
|||||||
calendarRef.current?.unselect();
|
calendarRef.current?.unselect();
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
setSelectedRange(null);
|
setSelectedRange(null);
|
||||||
|
selectedCalendars.forEach((calId) =>
|
||||||
|
dispatch(
|
||||||
|
getCalendarDetailAsync({
|
||||||
|
calId,
|
||||||
|
match: {
|
||||||
|
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
|
||||||
|
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
Object.keys(tempcalendars).forEach((calId) =>
|
||||||
|
dispatch(
|
||||||
|
getCalendarDetailAsync({
|
||||||
|
calId,
|
||||||
|
match: {
|
||||||
|
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
|
||||||
|
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
|
||||||
|
},
|
||||||
|
calType: "temp",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
const handleCloseEventDisplay = () => {
|
const handleCloseEventDisplay = () => {
|
||||||
setAnchorPosition(null);
|
setAnchorPosition(null);
|
||||||
@@ -267,6 +296,12 @@ export default function CalendarApp() {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<TempCalendarsInput
|
||||||
|
setAnchorEl={setAnchorEl}
|
||||||
|
selectedCalendars={selectedCalendars}
|
||||||
|
setSelectedCalendars={setSelectedCalendars}
|
||||||
|
setTempEvent={setTempEvent}
|
||||||
|
/>
|
||||||
<CalendarSelection
|
<CalendarSelection
|
||||||
selectedCalendars={selectedCalendars}
|
selectedCalendars={selectedCalendars}
|
||||||
setSelectedCalendars={setSelectedCalendars}
|
setSelectedCalendars={setSelectedCalendars}
|
||||||
@@ -316,12 +351,11 @@ export default function CalendarApp() {
|
|||||||
timeGridWeek: { titleFormat: { month: "long", year: "numeric" } },
|
timeGridWeek: { titleFormat: { month: "long", year: "numeric" } },
|
||||||
}}
|
}}
|
||||||
dayMaxEvents={true}
|
dayMaxEvents={true}
|
||||||
events={filteredEvents.map((e) => {
|
events={eventToFullCalendarFormat(
|
||||||
if (e.calId.split("/")[0] === userId) {
|
filteredEvents,
|
||||||
return { ...e, editable: true };
|
filteredTempEvents,
|
||||||
}
|
userId
|
||||||
return { ...e, editable: false };
|
)}
|
||||||
})}
|
|
||||||
weekNumbers
|
weekNumbers
|
||||||
weekNumberFormat={{ week: "long" }}
|
weekNumberFormat={{ week: "long" }}
|
||||||
slotDuration={"00:30:00"}
|
slotDuration={"00:30:00"}
|
||||||
@@ -379,6 +413,7 @@ export default function CalendarApp() {
|
|||||||
});
|
});
|
||||||
setEventDisplayedId(info.event.extendedProps.uid);
|
setEventDisplayedId(info.event.extendedProps.uid);
|
||||||
setEventDisplayedCalId(info.event.extendedProps.calId);
|
setEventDisplayedCalId(info.event.extendedProps.calId);
|
||||||
|
setEventDisplayedTemp(info.event._def.extendedProps.temp);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
eventAllow={(dropInfo, draggedEvent) => {
|
eventAllow={(dropInfo, draggedEvent) => {
|
||||||
@@ -439,7 +474,7 @@ export default function CalendarApp() {
|
|||||||
start: computedNewStart,
|
start: computedNewStart,
|
||||||
end: computedNewEnd,
|
end: computedNewEnd,
|
||||||
} as CalendarEvent;
|
} as CalendarEvent;
|
||||||
console.log(event, newEvent);
|
|
||||||
dispatch(
|
dispatch(
|
||||||
putEventAsync({ cal: calendars[newEvent.calId], newEvent })
|
putEventAsync({ cal: calendars[newEvent.calId], newEvent })
|
||||||
);
|
);
|
||||||
@@ -451,7 +486,13 @@ export default function CalendarApp() {
|
|||||||
}}
|
}}
|
||||||
eventContent={(arg) => {
|
eventContent={(arg) => {
|
||||||
const event = arg.event;
|
const event = arg.event;
|
||||||
if (!calendars[arg.event._def.extendedProps.calId]) return;
|
if (
|
||||||
|
(!event._def.extendedProps.temp &&
|
||||||
|
!calendars[arg.event._def.extendedProps.calId]) ||
|
||||||
|
(event._def.extendedProps.temp &&
|
||||||
|
!tempcalendars[arg.event._def.extendedProps.calId])
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
const attendees = event._def.extendedProps.attendee || [];
|
const attendees = event._def.extendedProps.attendee || [];
|
||||||
const isPrivate =
|
const isPrivate =
|
||||||
@@ -460,15 +501,19 @@ export default function CalendarApp() {
|
|||||||
let Icon = null;
|
let Icon = null;
|
||||||
let titleStyle: React.CSSProperties = {};
|
let titleStyle: React.CSSProperties = {};
|
||||||
const ownerEmails = new Set(
|
const ownerEmails = new Set(
|
||||||
calendars[arg.event._def.extendedProps.calId].ownerEmails?.map(
|
(event._def.extendedProps.temp ? tempcalendars : calendars)[
|
||||||
(email) => email.toLowerCase()
|
arg.event._def.extendedProps.calId
|
||||||
)
|
].ownerEmails?.map((email) => email.toLowerCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const delegated = (
|
||||||
|
event._def.extendedProps.temp ? tempcalendars : calendars
|
||||||
|
)[arg.event._def.extendedProps.calId].delegated;
|
||||||
const showSpecialDisplay = attendees.filter((att: userAttendee) =>
|
const showSpecialDisplay = attendees.filter((att: userAttendee) =>
|
||||||
ownerEmails.has(att.cal_address.toLowerCase())
|
ownerEmails.has(att.cal_address.toLowerCase())
|
||||||
);
|
);
|
||||||
if (!showSpecialDisplay[0]) return;
|
if (!delegated && showSpecialDisplay.length === 0) return null;
|
||||||
switch (showSpecialDisplay[0].partstat) {
|
switch (showSpecialDisplay?.[0]?.partstat) {
|
||||||
case "DECLINED":
|
case "DECLINED":
|
||||||
Icon = null;
|
Icon = null;
|
||||||
titleStyle.textDecoration = "line-through";
|
titleStyle.textDecoration = "line-through";
|
||||||
@@ -545,11 +590,13 @@ export default function CalendarApp() {
|
|||||||
selectedRange={selectedRange}
|
selectedRange={selectedRange}
|
||||||
setSelectedRange={setSelectedRange}
|
setSelectedRange={setSelectedRange}
|
||||||
calendarRef={calendarRef}
|
calendarRef={calendarRef}
|
||||||
|
event={tempEvent}
|
||||||
/>
|
/>
|
||||||
{openEventDisplay && eventDisplayedId && eventDisplayedCalId && (
|
{openEventDisplay && eventDisplayedId && eventDisplayedCalId && (
|
||||||
<EventPreviewModal
|
<EventPreviewModal
|
||||||
eventId={eventDisplayedId}
|
eventId={eventDisplayedId}
|
||||||
calId={eventDisplayedCalId}
|
calId={eventDisplayedCalId}
|
||||||
|
tempEvent={eventDisplayedTemp}
|
||||||
anchorPosition={anchorPosition}
|
anchorPosition={anchorPosition}
|
||||||
open={openEventDisplay}
|
open={openEventDisplay}
|
||||||
onClose={handleCloseEventDisplay}
|
onClose={handleCloseEventDisplay}
|
||||||
@@ -559,3 +606,65 @@ export default function CalendarApp() {
|
|||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function eventToFullCalendarFormat(
|
||||||
|
filteredEvents: CalendarEvent[],
|
||||||
|
filteredTempEvents: CalendarEvent[],
|
||||||
|
userId: string | undefined
|
||||||
|
) {
|
||||||
|
return filteredEvents
|
||||||
|
.concat(filteredTempEvents.map((e) => ({ ...e, temp: true })))
|
||||||
|
.map((e) => {
|
||||||
|
if (e.calId.split("/")[0] === userId) {
|
||||||
|
return { ...e, editable: true };
|
||||||
|
}
|
||||||
|
return { ...e, editable: false };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractEvents(
|
||||||
|
selectedCalendars: string[],
|
||||||
|
calendars: Record<string, Calendars>
|
||||||
|
) {
|
||||||
|
let filteredEvents: CalendarEvent[] = [];
|
||||||
|
selectedCalendars.forEach((id) => {
|
||||||
|
if (calendars[id].events) {
|
||||||
|
filteredEvents = filteredEvents
|
||||||
|
.concat(
|
||||||
|
Object.keys(calendars[id].events).map(
|
||||||
|
(eventid) => calendars[id].events[eventid]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.filter((event) => !(event.status === "CANCELLED"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return filteredEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCalsDetails(
|
||||||
|
selectedCalendars: string[],
|
||||||
|
pending: boolean,
|
||||||
|
calendars: Record<string, Calendars>,
|
||||||
|
rangeKey: string,
|
||||||
|
dispatch: Function,
|
||||||
|
calendarRange: { start: Date; end: Date },
|
||||||
|
calType?: "temp"
|
||||||
|
) {
|
||||||
|
selectedCalendars.forEach((id) => {
|
||||||
|
if (Object.keys(calendars[id].events).length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!pending && rangeKey) {
|
||||||
|
dispatch(
|
||||||
|
getCalendarDetailAsync({
|
||||||
|
calId: id,
|
||||||
|
match: {
|
||||||
|
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
|
||||||
|
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
|
||||||
|
},
|
||||||
|
calType,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ function CalendarAccordion({
|
|||||||
const allCalendars = useAppSelector((state) => state.calendars.list);
|
const allCalendars = useAppSelector((state) => state.calendars.list);
|
||||||
|
|
||||||
const [expended, setExpended] = useState(defaultExpanded);
|
const [expended, setExpended] = useState(defaultExpanded);
|
||||||
if (calendars.length === 0) return null;
|
if (calendars.length === 0 && !defaultExpanded) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Accordion defaultExpanded={defaultExpanded} expanded={expended}>
|
<Accordion defaultExpanded={defaultExpanded} expanded={expended}>
|
||||||
@@ -129,7 +129,6 @@ export default function CalendarSelection({
|
|||||||
calendars={delegatedCalendars}
|
calendars={delegatedCalendars}
|
||||||
selectedCalendars={selectedCalendars}
|
selectedCalendars={selectedCalendars}
|
||||||
handleToggle={handleCalendarToggle}
|
handleToggle={handleCalendarToggle}
|
||||||
defaultExpanded
|
|
||||||
setOpen={(id: string) => {
|
setOpen={(id: string) => {
|
||||||
setAnchorElCal(document.body);
|
setAnchorElCal(document.body);
|
||||||
setSelectedCalId(id);
|
setSelectedCalId(id);
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
import { useState, useRef, useMemo } from "react";
|
||||||
|
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
||||||
|
import {
|
||||||
|
getTempCalendarsListAsync,
|
||||||
|
removeTempCal,
|
||||||
|
} from "../../features/Calendars/CalendarSlice";
|
||||||
|
import { Calendars } from "../../features/Calendars/CalendarTypes";
|
||||||
|
import { CalendarEvent } from "../../features/Events/EventsTypes";
|
||||||
|
import { User, PeopleSearch } from "../Attendees/PeopleSearch";
|
||||||
|
|
||||||
|
const requestControllers = new Map<string, AbortController>();
|
||||||
|
|
||||||
|
export function TempCalendarsInput({
|
||||||
|
setAnchorEl,
|
||||||
|
setTempEvent,
|
||||||
|
selectedCalendars,
|
||||||
|
setSelectedCalendars,
|
||||||
|
}: {
|
||||||
|
setAnchorEl: Function;
|
||||||
|
setTempEvent: Function;
|
||||||
|
selectedCalendars: string[];
|
||||||
|
setSelectedCalendars: Function;
|
||||||
|
}) {
|
||||||
|
const [tempUsers, setTempUsers] = useState<User[]>([]);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const tempcalendars =
|
||||||
|
useAppSelector((state) => state.calendars.templist) ?? {};
|
||||||
|
const calendars = useAppSelector((state) => state.calendars.list);
|
||||||
|
|
||||||
|
const prevUsersRef = useRef<User[]>([]);
|
||||||
|
const userColors = new Map<string, string>();
|
||||||
|
|
||||||
|
const handleUserChange = async (_: any, users: User[]) => {
|
||||||
|
setTempUsers(users);
|
||||||
|
|
||||||
|
const prevUsers = prevUsersRef.current;
|
||||||
|
|
||||||
|
const addedUsers = users.filter(
|
||||||
|
(u) => !prevUsers.some((p) => p.email === u.email)
|
||||||
|
);
|
||||||
|
const removedUsers = prevUsers.filter(
|
||||||
|
(p) => !users.some((u) => u.email === p.email)
|
||||||
|
);
|
||||||
|
|
||||||
|
prevUsersRef.current = users;
|
||||||
|
|
||||||
|
const { calendarsToImport, calendarsToToggle } = getCalendarsFromUsersDelta(
|
||||||
|
addedUsers,
|
||||||
|
buildEmailToCalendarMap(calendars),
|
||||||
|
selectedCalendars
|
||||||
|
);
|
||||||
|
|
||||||
|
if (calendarsToImport.length > 0) {
|
||||||
|
for (const user of calendarsToImport) {
|
||||||
|
const controller = new AbortController();
|
||||||
|
requestControllers.set(user.email, controller);
|
||||||
|
|
||||||
|
if (!userColors.has(user.email)) {
|
||||||
|
const existingColors = new Set(userColors.values());
|
||||||
|
userColors.set(user.email, generateUserColor(existingColors));
|
||||||
|
}
|
||||||
|
|
||||||
|
user.color = userColors.get(user.email)!;
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
getTempCalendarsListAsync(user, { signal: controller.signal })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (calendarsToToggle.length > 0) {
|
||||||
|
setSelectedCalendars((prev: string[]) => [
|
||||||
|
...new Set([...prev, ...calendarsToToggle]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const user of removedUsers) {
|
||||||
|
const controller = requestControllers.get(user.email);
|
||||||
|
if (controller) {
|
||||||
|
controller.abort();
|
||||||
|
requestControllers.delete(user.email);
|
||||||
|
}
|
||||||
|
|
||||||
|
const calIds = buildEmailToCalendarMap(tempcalendars).get(user.email);
|
||||||
|
calIds?.forEach((id) => dispatch(removeTempCal(id)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleEventPreview = () => {
|
||||||
|
const newEvent: CalendarEvent = {
|
||||||
|
title: "New Event",
|
||||||
|
attendee: tempUsers.map((u) => ({
|
||||||
|
cn: u.displayName,
|
||||||
|
cal_address: u.email,
|
||||||
|
partstat: "NEED-ACTION",
|
||||||
|
role: "REQ-PARTICIPANT",
|
||||||
|
rsvp: "TRUE",
|
||||||
|
cutype: "INDIVIDUAL",
|
||||||
|
})),
|
||||||
|
} as CalendarEvent;
|
||||||
|
|
||||||
|
setTempEvent(newEvent);
|
||||||
|
setAnchorEl(document.body);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PeopleSearch
|
||||||
|
selectedUsers={tempUsers}
|
||||||
|
onChange={handleUserChange}
|
||||||
|
onToggleEventPreview={handleToggleEventPreview}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCalendarsFromUsersDelta(
|
||||||
|
addedUsers: User[],
|
||||||
|
emailToCalendarId: Map<string, string[]>,
|
||||||
|
selectedCalendars: string[]
|
||||||
|
) {
|
||||||
|
const selectedSet = new Set(selectedCalendars);
|
||||||
|
|
||||||
|
const calendarsToImport: User[] = [];
|
||||||
|
const calendarsToToggle: string[] = [];
|
||||||
|
|
||||||
|
for (const user of addedUsers) {
|
||||||
|
const calIds = emailToCalendarId.get(user.email) ?? [];
|
||||||
|
|
||||||
|
if (!calIds || calIds.every((calId) => !selectedSet.has(calId))) {
|
||||||
|
calendarsToImport.push(user);
|
||||||
|
} else {
|
||||||
|
// calIds.forEach((calId) => calendarsToToggle.push(calId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { calendarsToImport, calendarsToToggle };
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildEmailToCalendarMap(calRecord: Record<string, Calendars>) {
|
||||||
|
const map = new Map<string, string[]>();
|
||||||
|
for (const [id, cal] of Object.entries(calRecord)) {
|
||||||
|
cal.ownerEmails?.forEach((email) => {
|
||||||
|
const existing = map.get(email);
|
||||||
|
if (existing) {
|
||||||
|
existing.push(id);
|
||||||
|
} else {
|
||||||
|
map.set(email, [id]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateUserColor(existingColors: Set<string>): string {
|
||||||
|
let color: string;
|
||||||
|
do {
|
||||||
|
const hue = Math.floor(Math.random() * 360);
|
||||||
|
color = `hsl(${hue}, 70%, 50%)`;
|
||||||
|
} while (existingColors.has(color));
|
||||||
|
return color;
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import { getOpenPaasUser, getUserDetails } from "../User/userAPI";
|
|||||||
import { parseCalendarEvent } from "../Events/eventUtils";
|
import { parseCalendarEvent } from "../Events/eventUtils";
|
||||||
import { deleteEvent, getEvent, moveEvent, putEvent } from "../Events/EventApi";
|
import { deleteEvent, getEvent, moveEvent, putEvent } from "../Events/EventApi";
|
||||||
import { formatDateToYYYYMMDDTHHMMSS } from "../../utils/dateUtils";
|
import { formatDateToYYYYMMDDTHHMMSS } from "../../utils/dateUtils";
|
||||||
|
import { User } from "../../components/Attendees/PeopleSearch";
|
||||||
|
|
||||||
export const getCalendarsListAsync = createAsyncThunk<
|
export const getCalendarsListAsync = createAsyncThunk<
|
||||||
Record<string, Calendars> // Return type
|
Record<string, Calendars> // Return type
|
||||||
@@ -55,10 +56,50 @@ export const getCalendarsListAsync = createAsyncThunk<
|
|||||||
return importedCalendars;
|
return importedCalendars;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getTempCalendarsListAsync = createAsyncThunk<
|
||||||
|
Record<string, Calendars>,
|
||||||
|
User
|
||||||
|
>("calendars/getTempCalendars", async (tempUser) => {
|
||||||
|
const importedCalendars: Record<string, Calendars> = {};
|
||||||
|
|
||||||
|
const calendars = (await getCalendars(
|
||||||
|
tempUser.openpaasId ?? "",
|
||||||
|
"sharedPublic=true&WithRights=true"
|
||||||
|
)) as Record<string, any>;
|
||||||
|
const rawCalendars = calendars._embedded["dav:calendar"];
|
||||||
|
|
||||||
|
for (const cal of rawCalendars) {
|
||||||
|
const name = cal["dav:name"];
|
||||||
|
const description = cal["caldav:description"];
|
||||||
|
const delegated = cal["calendarserver:delegatedsource"] ? true : false;
|
||||||
|
const source = cal["calendarserver:source"]
|
||||||
|
? cal["calendarserver:source"]._links.self.href
|
||||||
|
: cal._links.self.href;
|
||||||
|
const link = cal._links.self.href;
|
||||||
|
|
||||||
|
const id = source.replace("/calendars/", "").replace(".json", "");
|
||||||
|
const ownerData: any = await getUserDetails(id.split("/")[0]);
|
||||||
|
|
||||||
|
importedCalendars[id] = {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
link,
|
||||||
|
owner: `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${ownerData.lastname}`,
|
||||||
|
ownerEmails: ownerData.emails,
|
||||||
|
description,
|
||||||
|
delegated,
|
||||||
|
color: tempUser.color ?? "#a8a8a8ff",
|
||||||
|
events: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return importedCalendars;
|
||||||
|
});
|
||||||
|
|
||||||
export const getCalendarDetailAsync = createAsyncThunk<
|
export const getCalendarDetailAsync = createAsyncThunk<
|
||||||
{ calId: string; events: CalendarEvent[] }, // Return type
|
{ calId: string; events: CalendarEvent[]; calType?: string }, // Return type
|
||||||
{ calId: string; match: { start: string; end: string } } // Arg type
|
{ calId: string; match: { start: string; end: string }; calType?: string } // Arg type
|
||||||
>("calendars/getCalendarDetails", async ({ calId, match }) => {
|
>("calendars/getCalendarDetails", async ({ calId, match, calType }) => {
|
||||||
const calendar = (await getCalendar(calId, match)) as Record<string, any>;
|
const calendar = (await getCalendar(calId, match)) as Record<string, any>;
|
||||||
const color = calendar["apple:color"];
|
const color = calendar["apple:color"];
|
||||||
const events: CalendarEvent[] = calendar._embedded["dav:item"].flatMap(
|
const events: CalendarEvent[] = calendar._embedded["dav:item"].flatMap(
|
||||||
@@ -72,13 +113,13 @@ export const getCalendarDetailAsync = createAsyncThunk<
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return { calId, events };
|
return { calId, events, calType };
|
||||||
});
|
});
|
||||||
|
|
||||||
export const putEventAsync = createAsyncThunk<
|
export const putEventAsync = createAsyncThunk<
|
||||||
{ calId: string; events: CalendarEvent[] }, // Return type
|
{ calId: string; events: CalendarEvent[]; calType?: "temp" }, // Return type
|
||||||
{ cal: Calendars; newEvent: CalendarEvent } // Arg type
|
{ cal: Calendars; newEvent: CalendarEvent; calType?: "temp" } // Arg type
|
||||||
>("calendars/putEvent", async ({ cal, newEvent }) => {
|
>("calendars/putEvent", async ({ cal, newEvent, calType }) => {
|
||||||
const response = await putEvent(
|
const response = await putEvent(
|
||||||
newEvent,
|
newEvent,
|
||||||
cal.ownerEmails ? cal.ownerEmails[0] : undefined
|
cal.ownerEmails ? cal.ownerEmails[0] : undefined
|
||||||
@@ -116,6 +157,7 @@ export const putEventAsync = createAsyncThunk<
|
|||||||
return {
|
return {
|
||||||
calId: cal.id,
|
calId: cal.id,
|
||||||
events,
|
events,
|
||||||
|
calType,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -247,7 +289,11 @@ export const addSharedCalendarAsync = createAsyncThunk<
|
|||||||
|
|
||||||
const CalendarSlice = createSlice({
|
const CalendarSlice = createSlice({
|
||||||
name: "calendars",
|
name: "calendars",
|
||||||
initialState: { list: {} as Record<string, Calendars>, pending: false },
|
initialState: {
|
||||||
|
list: {} as Record<string, Calendars>,
|
||||||
|
templist: {} as Record<string, Calendars>,
|
||||||
|
pending: false,
|
||||||
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
createCalendar: (state, action: PayloadAction<Record<string, string>>) => {
|
createCalendar: (state, action: PayloadAction<Record<string, string>>) => {
|
||||||
const id = Date.now().toString(36);
|
const id = Date.now().toString(36);
|
||||||
@@ -280,6 +326,9 @@ const CalendarSlice = createSlice({
|
|||||||
action.payload.eventUid
|
action.payload.eventUid
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
removeTempCal: (state, action: PayloadAction<string>) => {
|
||||||
|
delete state.templist[action.payload];
|
||||||
|
},
|
||||||
updateEventLocal: (
|
updateEventLocal: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{ calId: string; event: CalendarEvent }>
|
action: PayloadAction<{ calId: string; event: CalendarEvent }>
|
||||||
@@ -297,56 +346,81 @@ const CalendarSlice = createSlice({
|
|||||||
state.list = action.payload;
|
state.list = action.payload;
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
.addCase(
|
||||||
|
getTempCalendarsListAsync.fulfilled,
|
||||||
|
(state, action: PayloadAction<Record<string, Calendars>>) => {
|
||||||
|
state.pending = false;
|
||||||
|
Object.keys(action.payload).forEach(
|
||||||
|
(id) => (state.templist[id] = action.payload[id])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
.addCase(
|
.addCase(
|
||||||
getCalendarDetailAsync.fulfilled,
|
getCalendarDetailAsync.fulfilled,
|
||||||
(
|
(
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{ calId: string; events: CalendarEvent[] }>
|
action: PayloadAction<{
|
||||||
|
calId: string;
|
||||||
|
events: CalendarEvent[];
|
||||||
|
calType?: string;
|
||||||
|
}>
|
||||||
) => {
|
) => {
|
||||||
state.pending = false;
|
state.pending = false;
|
||||||
if (!state.list[action.payload.calId]) {
|
const type = action.payload.calType === "temp" ? "templist" : "list";
|
||||||
state.list[action.payload.calId] = {
|
|
||||||
|
if (!state[type][action.payload.calId]) {
|
||||||
|
state[type][action.payload.calId] = {
|
||||||
id: action.payload.calId,
|
id: action.payload.calId,
|
||||||
events: {},
|
events: {},
|
||||||
} as Calendars;
|
} as Calendars;
|
||||||
}
|
}
|
||||||
action.payload.events.forEach((event) => {
|
action.payload.events.forEach((event) => {
|
||||||
state.list[action.payload.calId].events[event.uid] = event;
|
state[type][action.payload.calId].events[event.uid] = event;
|
||||||
});
|
|
||||||
Object.keys(state.list[action.payload.calId].events).forEach((id) => {
|
|
||||||
state.list[action.payload.calId].events[id].color =
|
|
||||||
state.list[action.payload.calId].color;
|
|
||||||
state.list[action.payload.calId].events[id].calId =
|
|
||||||
action.payload.calId;
|
|
||||||
state.list[action.payload.calId].events[id].timezone =
|
|
||||||
Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
||||||
});
|
});
|
||||||
|
Object.keys(state[type][action.payload.calId].events).forEach(
|
||||||
|
(id) => {
|
||||||
|
state[type][action.payload.calId].events[id].color =
|
||||||
|
state[type][action.payload.calId].color;
|
||||||
|
state[type][action.payload.calId].events[id].calId =
|
||||||
|
action.payload.calId;
|
||||||
|
state[type][action.payload.calId].events[id].timezone =
|
||||||
|
Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.addCase(
|
.addCase(
|
||||||
putEventAsync.fulfilled,
|
putEventAsync.fulfilled,
|
||||||
(
|
(
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{ calId: string; events: CalendarEvent[] }>
|
action: PayloadAction<{
|
||||||
|
calId: string;
|
||||||
|
events: CalendarEvent[];
|
||||||
|
calType?: "temp";
|
||||||
|
}>
|
||||||
) => {
|
) => {
|
||||||
state.pending = false;
|
state.pending = false;
|
||||||
if (!state.list[action.payload.calId]) {
|
const type = action.payload.calType === "temp" ? "templist" : "list";
|
||||||
state.list[action.payload.calId] = {
|
|
||||||
|
if (!state[type][action.payload.calId]) {
|
||||||
|
state[type][action.payload.calId] = {
|
||||||
id: action.payload.calId,
|
id: action.payload.calId,
|
||||||
events: {},
|
events: {},
|
||||||
} as Calendars;
|
} as Calendars;
|
||||||
}
|
}
|
||||||
action.payload.events.forEach((event) => {
|
action.payload.events.forEach((event) => {
|
||||||
state.list[action.payload.calId].events[event.uid] = event;
|
state[type][action.payload.calId].events[event.uid] = event;
|
||||||
});
|
|
||||||
Object.keys(state.list[action.payload.calId].events).forEach((id) => {
|
|
||||||
state.list[action.payload.calId].events[id].color =
|
|
||||||
state.list[action.payload.calId].color;
|
|
||||||
state.list[action.payload.calId].events[id].calId =
|
|
||||||
action.payload.calId;
|
|
||||||
state.list[action.payload.calId].events[id].timezone =
|
|
||||||
Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
||||||
});
|
});
|
||||||
|
Object.keys(state[type][action.payload.calId].events).forEach(
|
||||||
|
(id) => {
|
||||||
|
state[type][action.payload.calId].events[id].color =
|
||||||
|
state[type][action.payload.calId].color;
|
||||||
|
state[type][action.payload.calId].events[id].calId =
|
||||||
|
action.payload.calId;
|
||||||
|
state[type][action.payload.calId].events[id].timezone =
|
||||||
|
Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.addCase(
|
.addCase(
|
||||||
@@ -484,12 +558,20 @@ const CalendarSlice = createSlice({
|
|||||||
.addCase(createCalendarAsync.pending, (state) => {
|
.addCase(createCalendarAsync.pending, (state) => {
|
||||||
state.pending = true;
|
state.pending = true;
|
||||||
})
|
})
|
||||||
|
.addCase(getTempCalendarsListAsync.pending, (state) => {
|
||||||
|
state.pending = true;
|
||||||
|
})
|
||||||
.addCase(addSharedCalendarAsync.pending, (state) => {
|
.addCase(addSharedCalendarAsync.pending, (state) => {
|
||||||
state.pending = true;
|
state.pending = true;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { addEvent, removeEvent, createCalendar, updateEventLocal } =
|
export const {
|
||||||
CalendarSlice.actions;
|
addEvent,
|
||||||
|
removeEvent,
|
||||||
|
createCalendar,
|
||||||
|
updateEventLocal,
|
||||||
|
removeTempCal,
|
||||||
|
} = CalendarSlice.actions;
|
||||||
export default CalendarSlice.reducer;
|
export default CalendarSlice.reducer;
|
||||||
|
|||||||
@@ -45,22 +45,24 @@ import { getCalendar } from "../Calendars/CalendarApi";
|
|||||||
export default function EventPreviewModal({
|
export default function EventPreviewModal({
|
||||||
eventId,
|
eventId,
|
||||||
calId,
|
calId,
|
||||||
|
tempEvent,
|
||||||
anchorPosition,
|
anchorPosition,
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
}: {
|
}: {
|
||||||
eventId: string;
|
eventId: string;
|
||||||
calId: string;
|
calId: string;
|
||||||
|
tempEvent?: boolean;
|
||||||
anchorPosition: PopoverPosition | null;
|
anchorPosition: PopoverPosition | null;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void;
|
onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void;
|
||||||
}) {
|
}) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const calendars = useAppSelector((state) => state.calendars);
|
const calendars = useAppSelector((state) => state.calendars);
|
||||||
const calendar = calendars.list[calId];
|
const calendar = tempEvent
|
||||||
const event = useAppSelector(
|
? calendars.templist[calId]
|
||||||
(state) => state.calendars.list[calId]?.events[eventId]
|
: calendars.list[calId];
|
||||||
);
|
const event = calendar.events[eventId];
|
||||||
const user = useAppSelector((state) => state.user);
|
const user = useAppSelector((state) => state.user);
|
||||||
const [showAllAttendees, setShowAllAttendees] = useState(false);
|
const [showAllAttendees, setShowAllAttendees] = useState(false);
|
||||||
const [openFullDisplay, setOpenFullDisplay] = useState(false);
|
const [openFullDisplay, setOpenFullDisplay] = useState(false);
|
||||||
|
|||||||
@@ -67,13 +67,16 @@ function EventPopover({
|
|||||||
const [showMore, setShowMore] = useState(false);
|
const [showMore, setShowMore] = useState(false);
|
||||||
|
|
||||||
const [title, setTitle] = useState(event?.title ?? "");
|
const [title, setTitle] = useState(event?.title ?? "");
|
||||||
|
|
||||||
const [description, setDescription] = useState(event?.description ?? "");
|
const [description, setDescription] = useState(event?.description ?? "");
|
||||||
const [location, setLocation] = useState(event?.location ?? "");
|
const [location, setLocation] = useState(event?.location ?? "");
|
||||||
const [start, setStart] = useState(
|
const [start, setStart] = useState(
|
||||||
event?.start ? new Date(event.start).toISOString() : ""
|
event?.start
|
||||||
|
? new Date(event.start).toISOString()
|
||||||
|
: new Date().toISOString()
|
||||||
);
|
);
|
||||||
const [end, setEnd] = useState(
|
const [end, setEnd] = useState(
|
||||||
event?.end ? new Date(event.end)?.toISOString() : ""
|
event?.end ? new Date(event.end)?.toISOString() : new Date().toISOString()
|
||||||
);
|
);
|
||||||
const [calendarid, setCalendarid] = useState(
|
const [calendarid, setCalendarid] = useState(
|
||||||
event?.calId
|
event?.calId
|
||||||
@@ -104,6 +107,24 @@ function EventPopover({
|
|||||||
}
|
}
|
||||||
}, [selectedRange]);
|
}, [selectedRange]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTitle(event?.title ?? "");
|
||||||
|
setAttendees(
|
||||||
|
event?.attendee
|
||||||
|
? event.attendee.filter((a) => a.cal_address !== organizer?.cal_address)
|
||||||
|
: []
|
||||||
|
);
|
||||||
|
}, [event, organizer?.cal_address]);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
onClose({}, "backdropClick"); // Reset
|
||||||
|
setTitle("");
|
||||||
|
setDescription("");
|
||||||
|
setAttendees([]);
|
||||||
|
setLocation("");
|
||||||
|
setCalendarid(0);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
const newEventUID = crypto.randomUUID();
|
const newEventUID = crypto.randomUUID();
|
||||||
|
|
||||||
@@ -142,7 +163,7 @@ function EventPopover({
|
|||||||
newEvent.attendee = newEvent.attendee.concat(attendees);
|
newEvent.attendee = newEvent.attendee.concat(attendees);
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(
|
await dispatch(
|
||||||
putEventAsync({
|
putEventAsync({
|
||||||
cal: userPersonnalCalendars[calendarid],
|
cal: userPersonnalCalendars[calendarid],
|
||||||
newEvent,
|
newEvent,
|
||||||
@@ -153,6 +174,7 @@ function EventPopover({
|
|||||||
// Reset
|
// Reset
|
||||||
setTitle("");
|
setTitle("");
|
||||||
setDescription("");
|
setDescription("");
|
||||||
|
setAttendees([]);
|
||||||
setLocation("");
|
setLocation("");
|
||||||
setCalendarid(0);
|
setCalendarid(0);
|
||||||
};
|
};
|
||||||
@@ -161,7 +183,7 @@ function EventPopover({
|
|||||||
<Popover
|
<Popover
|
||||||
open={open}
|
open={open}
|
||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
onClose={onClose}
|
onClose={handleClose}
|
||||||
anchorOrigin={{
|
anchorOrigin={{
|
||||||
vertical: "center",
|
vertical: "center",
|
||||||
horizontal: "center",
|
horizontal: "center",
|
||||||
@@ -172,7 +194,7 @@ function EventPopover({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader title={event ? "Duplicate Event" : "Create Event"} />
|
<CardHeader title={event?.uid ? "Duplicate Event" : "Create Event"} />
|
||||||
<CardContent
|
<CardContent
|
||||||
sx={{ maxHeight: "85vh", maxWidth: "40vw", overflow: "auto" }}
|
sx={{ maxHeight: "85vh", maxWidth: "40vw", overflow: "auto" }}
|
||||||
>
|
>
|
||||||
@@ -359,10 +381,7 @@ function EventPopover({
|
|||||||
|
|
||||||
<CardActions>
|
<CardActions>
|
||||||
<Box mt={2} display="flex" justifyContent="flex-end" gap={1}>
|
<Box mt={2} display="flex" justifyContent="flex-end" gap={1}>
|
||||||
<Button
|
<Button variant="outlined" onClick={handleClose}>
|
||||||
variant="outlined"
|
|
||||||
onClick={() => onClose({}, "backdropClick")}
|
|
||||||
>
|
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="small" onClick={() => setShowMore(!showMore)}>
|
<Button size="small" onClick={() => setShowMore(!showMore)}>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { User } from "../../components/Attendees/PeopleSearch";
|
||||||
import { api } from "../../utils/apiUtils";
|
import { api } from "../../utils/apiUtils";
|
||||||
|
|
||||||
export async function getOpenPaasUser() {
|
export async function getOpenPaasUser() {
|
||||||
@@ -8,14 +9,7 @@ export async function getOpenPaasUser() {
|
|||||||
export async function searchUsers(
|
export async function searchUsers(
|
||||||
query: string,
|
query: string,
|
||||||
objectTypes: string[] = ["user", "contact"]
|
objectTypes: string[] = ["user", "contact"]
|
||||||
): Promise<
|
): Promise<User[]> {
|
||||||
{
|
|
||||||
email: string;
|
|
||||||
displayName: string;
|
|
||||||
avatarUrl: string;
|
|
||||||
openpaasId: string;
|
|
||||||
}[]
|
|
||||||
> {
|
|
||||||
const response: any[] = await api
|
const response: any[] = await api
|
||||||
.post(`api/people/search`, {
|
.post(`api/people/search`, {
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|||||||
@@ -45,3 +45,10 @@ export function getDeltaInMilliseconds(delta: {
|
|||||||
(delta.milliseconds || 0)
|
(delta.milliseconds || 0)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const computeStartOfTheWeek = (date: Date): Date => {
|
||||||
|
const startOfWeek = new Date(date);
|
||||||
|
startOfWeek.setDate(date.getDate() - ((date.getDay() + 6) % 7)); // Monday
|
||||||
|
startOfWeek.setHours(0, 0, 0, 0);
|
||||||
|
return startOfWeek;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user