ability to register a shared calendar (#118)
Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { screen, fireEvent } from "@testing-library/react";
|
import { screen, fireEvent, waitFor } from "@testing-library/react";
|
||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
import CalendarSelection from "../../src/components/Calendar/CalendarSelection";
|
import CalendarSelection from "../../src/components/Calendar/CalendarSelection";
|
||||||
import { renderWithProviders } from "../utils/Renderwithproviders";
|
import { renderWithProviders } from "../utils/Renderwithproviders";
|
||||||
@@ -16,7 +16,7 @@ describe("CalendarSelection", () => {
|
|||||||
|
|
||||||
const calendarsMock = {
|
const calendarsMock = {
|
||||||
"user1/cal1": {
|
"user1/cal1": {
|
||||||
name: "Calendar personnal",
|
name: "Calendar personal",
|
||||||
id: "user1/cal1",
|
id: "user1/cal1",
|
||||||
color: "#FF0000",
|
color: "#FF0000",
|
||||||
ownerEmails: ["alice@example.com"],
|
ownerEmails: ["alice@example.com"],
|
||||||
@@ -36,7 +36,7 @@ describe("CalendarSelection", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
it("renders personal, delegated and shared calendars", () => {
|
it("renders personal, delegated and other calendars", () => {
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<CalendarSelection
|
<CalendarSelection
|
||||||
selectedCalendars={["user1/cal1"]}
|
selectedCalendars={["user1/cal1"]}
|
||||||
@@ -52,7 +52,7 @@ describe("CalendarSelection", () => {
|
|||||||
expect(screen.getByText("Delegated Calendars")).toBeInTheDocument();
|
expect(screen.getByText("Delegated Calendars")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Other Calendars")).toBeInTheDocument();
|
expect(screen.getByText("Other Calendars")).toBeInTheDocument();
|
||||||
|
|
||||||
expect(screen.getByLabelText("Calendar personnal")).toBeChecked();
|
expect(screen.getByLabelText("Calendar personal")).toBeChecked();
|
||||||
expect(screen.getByLabelText("Calendar delegated")).not.toBeChecked();
|
expect(screen.getByLabelText("Calendar delegated")).not.toBeChecked();
|
||||||
expect(screen.getByLabelText("Calendar shared")).not.toBeChecked();
|
expect(screen.getByLabelText("Calendar shared")).not.toBeChecked();
|
||||||
});
|
});
|
||||||
@@ -71,7 +71,7 @@ describe("CalendarSelection", () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const checkbox = screen.getByLabelText("Calendar personnal");
|
const checkbox = screen.getByLabelText("Calendar personal");
|
||||||
fireEvent.click(checkbox);
|
fireEvent.click(checkbox);
|
||||||
|
|
||||||
expect(setSelectedCalendars).toHaveBeenCalledWith(expect.any(Function));
|
expect(setSelectedCalendars).toHaveBeenCalledWith(expect.any(Function));
|
||||||
@@ -94,14 +94,14 @@ describe("CalendarSelection", () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const checkbox = screen.getByLabelText("Calendar personnal");
|
const checkbox = screen.getByLabelText("Calendar personal");
|
||||||
fireEvent.click(checkbox);
|
fireEvent.click(checkbox);
|
||||||
|
|
||||||
const updater = setSelectedCalendars.mock.calls[0][0];
|
const updater = setSelectedCalendars.mock.calls[0][0];
|
||||||
expect(updater(["user1/cal1"])).toEqual([]);
|
expect(updater(["user1/cal1"])).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("opens CalendarPopover when Add button is clicked", () => {
|
it("opens CalendarPopover modal when personal Add button is clicked", () => {
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<CalendarSelection
|
<CalendarSelection
|
||||||
selectedCalendars={[]}
|
selectedCalendars={[]}
|
||||||
@@ -113,13 +113,31 @@ describe("CalendarSelection", () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const addButton = screen.getAllByTestId("AddIcon")[0];
|
const addButtons = screen.getAllByRole("button");
|
||||||
fireEvent.click(addButton);
|
fireEvent.click(addButtons[0]);
|
||||||
|
|
||||||
|
waitFor(() => expect(screen.getByRole("presentation")).toBeInTheDocument());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opens CalendarSearch modal when Other Add button is clicked", () => {
|
||||||
|
renderWithProviders(
|
||||||
|
<CalendarSelection
|
||||||
|
selectedCalendars={[]}
|
||||||
|
setSelectedCalendars={jest.fn()}
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
user: baseUser,
|
||||||
|
calendars: { list: calendarsMock, pending: false },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const addButtons = screen.getAllByRole("button");
|
||||||
|
fireEvent.click(addButtons[1]); // seccond Add button (other)
|
||||||
|
|
||||||
expect(screen.getByRole("presentation")).toBeInTheDocument();
|
expect(screen.getByRole("presentation")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders only personal calendars if no delegated/shared exist", () => {
|
it("renders only personal calendars if no delegated/other exist", () => {
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<CalendarSelection
|
<CalendarSelection
|
||||||
selectedCalendars={[]}
|
selectedCalendars={[]}
|
||||||
@@ -138,7 +156,7 @@ describe("CalendarSelection", () => {
|
|||||||
|
|
||||||
expect(screen.getByText("Personnal Calendars")).toBeInTheDocument();
|
expect(screen.getByText("Personnal Calendars")).toBeInTheDocument();
|
||||||
expect(screen.queryByText("Delegated Calendars")).not.toBeInTheDocument();
|
expect(screen.queryByText("Delegated Calendars")).not.toBeInTheDocument();
|
||||||
expect(screen.queryByText("Shared Calendars")).not.toBeInTheDocument();
|
expect(screen.queryByText("Other Calendars")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders nothing when no calendars are present", () => {
|
it("renders nothing when no calendars are present", () => {
|
||||||
@@ -155,4 +173,26 @@ describe("CalendarSelection", () => {
|
|||||||
|
|
||||||
expect(screen.queryByLabelText(/Calendar/)).not.toBeInTheDocument();
|
expect(screen.queryByLabelText(/Calendar/)).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("expands and collapses accordions when clicked", () => {
|
||||||
|
renderWithProviders(
|
||||||
|
<CalendarSelection
|
||||||
|
selectedCalendars={[]}
|
||||||
|
setSelectedCalendars={jest.fn()}
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
user: baseUser,
|
||||||
|
calendars: { list: calendarsMock, pending: false },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const delegatedAccordionSummary = screen
|
||||||
|
.getByText("Delegated Calendars")
|
||||||
|
.closest(".MuiAccordionSummary-root");
|
||||||
|
|
||||||
|
fireEvent.click(delegatedAccordionSummary!);
|
||||||
|
expect(delegatedAccordionSummary).toHaveAttribute("aria-expanded", "false");
|
||||||
|
|
||||||
|
fireEvent.click(delegatedAccordionSummary!);
|
||||||
|
expect(delegatedAccordionSummary).toHaveAttribute("aria-expanded", "true");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -56,15 +56,15 @@ describe("CalendarPopover", () => {
|
|||||||
renderPopover();
|
renderPopover();
|
||||||
|
|
||||||
// There are multiple color buttons; pick the first
|
// There are multiple color buttons; pick the first
|
||||||
const colorButtons = screen
|
const colorButtons = screen.getAllByRole("button", {
|
||||||
.getAllByRole("button")
|
name: /select color/i,
|
||||||
.filter((btn) => btn.style.backgroundColor !== "");
|
});
|
||||||
fireEvent.click(colorButtons[0]);
|
fireEvent.click(colorButtons[0]);
|
||||||
|
|
||||||
// The header background should update (check via inline style)
|
// The header background should update (check via inline style)
|
||||||
expect(
|
expect(
|
||||||
screen.getByText("Calendar configuration").style.backgroundColor
|
screen.getByText("Calendar configuration").style.backgroundColor
|
||||||
).toBe(colorButtons[0].style.backgroundColor);
|
).toBe("rgb(52, 211, 153)");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("dispatches createCalendar and calls onClose when Save clicked", () => {
|
it("dispatches createCalendar and calls onClose when Save clicked", () => {
|
||||||
@@ -82,12 +82,9 @@ describe("CalendarPopover", () => {
|
|||||||
target: { value: "Test Description" },
|
target: { value: "Test Description" },
|
||||||
});
|
});
|
||||||
|
|
||||||
const expectedColor = "#D50000";
|
const colorButtons = screen.getAllByRole("button", {
|
||||||
|
name: /select color/i,
|
||||||
const colorButtons = screen
|
});
|
||||||
.getAllByRole("button")
|
|
||||||
.filter((btn) => btn.style.backgroundColor !== "");
|
|
||||||
|
|
||||||
fireEvent.click(colorButtons[0]);
|
fireEvent.click(colorButtons[0]);
|
||||||
|
|
||||||
fireEvent.click(screen.getByText(/Save/i));
|
fireEvent.click(screen.getByText(/Save/i));
|
||||||
|
|||||||
@@ -1,21 +1,7 @@
|
|||||||
import {
|
|
||||||
Autocomplete,
|
|
||||||
Avatar,
|
|
||||||
CircularProgress,
|
|
||||||
ListItem,
|
|
||||||
ListItemAvatar,
|
|
||||||
ListItemText,
|
|
||||||
TextField,
|
|
||||||
} from "@mui/material";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { searchUsers } from "../../features/User/userAPI";
|
import { searchUsers } from "../../features/User/userAPI";
|
||||||
import { userAttendee } from "../../features/User/userDataTypes";
|
import { userAttendee } from "../../features/User/userDataTypes";
|
||||||
|
import { PeopleSearch, User } from "./PeopleSearch";
|
||||||
interface User {
|
|
||||||
email: string;
|
|
||||||
displayName: string;
|
|
||||||
avatarUrl: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function UserSearch({
|
export default function UserSearch({
|
||||||
attendees,
|
attendees,
|
||||||
@@ -26,77 +12,43 @@ export default function UserSearch({
|
|||||||
setAttendees: Function;
|
setAttendees: Function;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const [query, setQuery] = useState("");
|
const [selectedUsers, setSelectedUsers] = useState(
|
||||||
const [options, setOptions] = useState<User[]>([]);
|
attendees.map((a) => ({
|
||||||
const [loading, setLoading] = useState(false);
|
email: a.cal_address,
|
||||||
|
displayName: a.cn ?? "",
|
||||||
|
avatarUrl: "",
|
||||||
|
openpaasId: "",
|
||||||
|
}))
|
||||||
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const delayDebounceFn = setTimeout(async () => {
|
setSelectedUsers(
|
||||||
if (query) {
|
attendees.map((a) => ({
|
||||||
setLoading(true);
|
email: a.cal_address,
|
||||||
const res = await searchUsers(query);
|
displayName: a.cn ?? "",
|
||||||
setOptions(res);
|
avatarUrl: "",
|
||||||
setLoading(false);
|
openpaasId: "",
|
||||||
}
|
}))
|
||||||
}, 300);
|
);
|
||||||
|
}, [attendees]);
|
||||||
return () => clearTimeout(delayDebounceFn);
|
|
||||||
}, [query]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Autocomplete
|
<>
|
||||||
multiple
|
<PeopleSearch
|
||||||
options={options}
|
selectedUsers={selectedUsers}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
loading={loading}
|
onChange={(event: any, value: User[]) => {
|
||||||
filterOptions={(x) => x}
|
setAttendees(
|
||||||
fullWidth
|
value.map((a: User) => ({
|
||||||
getOptionLabel={(option) => option.displayName || option.email}
|
cn: a.displayName,
|
||||||
filterSelectedOptions
|
cal_address: a.email,
|
||||||
onInputChange={(event, value) => setQuery(value)}
|
partstat: "NEED_ACTION",
|
||||||
onChange={(event, value) =>
|
rsvp: "FALSE",
|
||||||
setAttendees(
|
role: "CHAIR",
|
||||||
value.map((a) => ({
|
cutype: "INDIVIDUAL",
|
||||||
cn: a.displayName,
|
}))
|
||||||
cal_address: a.email,
|
);
|
||||||
partstat: "NEED_ACTION",
|
setSelectedUsers(value);
|
||||||
rsvp: "FALSE",
|
}}
|
||||||
role: "CHAIR",
|
/>
|
||||||
cutype: "INDIVIDUAL",
|
</>
|
||||||
}))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
renderInput={(params) => (
|
|
||||||
<TextField
|
|
||||||
{...params}
|
|
||||||
label="Search user"
|
|
||||||
InputProps={{
|
|
||||||
...params.InputProps,
|
|
||||||
endAdornment: (
|
|
||||||
<>
|
|
||||||
{loading ? (
|
|
||||||
<CircularProgress color="inherit" size={20} />
|
|
||||||
) : null}
|
|
||||||
{params.InputProps.endAdornment}
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
renderOption={(props, option) => {
|
|
||||||
if (attendees.find((a) => a.cal_address === option.email)) return;
|
|
||||||
return (
|
|
||||||
<ListItem {...props} key={option.email} disableGutters>
|
|
||||||
<ListItemAvatar>
|
|
||||||
<Avatar src={option.avatarUrl} alt={option.displayName} />
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText
|
|
||||||
primary={option.displayName}
|
|
||||||
secondary={option.email}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
|
import Autocomplete from "@mui/material/Autocomplete";
|
||||||
|
import Avatar from "@mui/material/Avatar";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import Card from "@mui/material/Card";
|
||||||
|
import CardActions from "@mui/material/CardActions";
|
||||||
|
import CardContent from "@mui/material/CardContent";
|
||||||
|
import CardHeader from "@mui/material/CardHeader";
|
||||||
|
import CircularProgress from "@mui/material/CircularProgress";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import ListItem from "@mui/material/ListItem";
|
||||||
|
import ListItemAvatar from "@mui/material/ListItemAvatar";
|
||||||
|
import ListItemText from "@mui/material/ListItemText";
|
||||||
|
import Modal from "@mui/material/Modal";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { searchUsers } from "../../features/User/userAPI";
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
email: string;
|
||||||
|
displayName: string;
|
||||||
|
avatarUrl: string;
|
||||||
|
openpaasId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PeopleSearch({
|
||||||
|
selectedUsers,
|
||||||
|
onChange,
|
||||||
|
disabled,
|
||||||
|
}: {
|
||||||
|
selectedUsers: User[];
|
||||||
|
onChange: Function;
|
||||||
|
disabled?: boolean;
|
||||||
|
}) {
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [options, setOptions] = useState<User[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const delayDebounceFn = setTimeout(async () => {
|
||||||
|
if (query) {
|
||||||
|
setLoading(true);
|
||||||
|
const res = await searchUsers(query, ["user"]);
|
||||||
|
setOptions(res);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
return () => clearTimeout(delayDebounceFn);
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Autocomplete
|
||||||
|
multiple
|
||||||
|
options={options}
|
||||||
|
disabled={disabled}
|
||||||
|
loading={loading}
|
||||||
|
filterOptions={(x) => x}
|
||||||
|
fullWidth
|
||||||
|
getOptionLabel={(option) => option.displayName || option.email}
|
||||||
|
filterSelectedOptions
|
||||||
|
value={selectedUsers}
|
||||||
|
onInputChange={(event, value) => setQuery(value)}
|
||||||
|
onChange={(event, value) => onChange(event, value)}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
label="Search user"
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
...params.InputProps,
|
||||||
|
endAdornment: (
|
||||||
|
<>
|
||||||
|
{loading ? (
|
||||||
|
<CircularProgress color="inherit" size={20} />
|
||||||
|
) : null}
|
||||||
|
{params.InputProps.endAdornment}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
renderOption={(props, option) => {
|
||||||
|
if (selectedUsers.find((u) => u.email === option.email)) return;
|
||||||
|
return (
|
||||||
|
<ListItem
|
||||||
|
{...props}
|
||||||
|
key={option.email + option.displayName}
|
||||||
|
disableGutters
|
||||||
|
>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar src={option.avatarUrl} alt={option.displayName} />
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText
|
||||||
|
primary={option.displayName}
|
||||||
|
secondary={option.email}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Box } from "@mui/material";
|
||||||
|
|
||||||
|
const defaultColors = ["#34d399", "#fbbf24", "#f87171", "#60a5fa", "#f472b6"];
|
||||||
|
|
||||||
|
export function ColorPicker({
|
||||||
|
selectedColor,
|
||||||
|
colors = defaultColors,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
selectedColor?: string;
|
||||||
|
colors?: string[];
|
||||||
|
onChange: (color: string) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
|
{colors.map((c) => (
|
||||||
|
<Box
|
||||||
|
key={c}
|
||||||
|
role="button"
|
||||||
|
aria-label={`select color ${c}`}
|
||||||
|
onClick={() => onChange(c)}
|
||||||
|
sx={{
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
borderRadius: "50%",
|
||||||
|
bgcolor: c,
|
||||||
|
cursor: "pointer",
|
||||||
|
border:
|
||||||
|
selectedColor === c ? "2px solid black" : "2px solid transparent",
|
||||||
|
transition: "all 0.2s",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,336 @@
|
|||||||
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
|
import Autocomplete from "@mui/material/Autocomplete";
|
||||||
|
import Avatar from "@mui/material/Avatar";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import Card from "@mui/material/Card";
|
||||||
|
import CardActions from "@mui/material/CardActions";
|
||||||
|
import CardContent from "@mui/material/CardContent";
|
||||||
|
import CardHeader from "@mui/material/CardHeader";
|
||||||
|
import CircularProgress from "@mui/material/CircularProgress";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import ListItem from "@mui/material/ListItem";
|
||||||
|
import ListItemAvatar from "@mui/material/ListItemAvatar";
|
||||||
|
import ListItemText from "@mui/material/ListItemText";
|
||||||
|
import Modal from "@mui/material/Modal";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
||||||
|
import { searchUsers } from "../../features/User/userAPI";
|
||||||
|
import { getCalendars } from "../../features/Calendars/CalendarApi";
|
||||||
|
import { addSharedCalendarAsync } from "../../features/Calendars/CalendarSlice";
|
||||||
|
import { ColorPicker } from "./CalendarColorPicker";
|
||||||
|
import { Calendars } from "../../features/Calendars/CalendarTypes";
|
||||||
|
import { User, PeopleSearch } from "../Attendees/PeopleSearch";
|
||||||
|
|
||||||
|
interface CalendarWithOwner {
|
||||||
|
cal: Record<string, any>;
|
||||||
|
owner: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CalendarItem({
|
||||||
|
cal,
|
||||||
|
onRemove,
|
||||||
|
onColorChange,
|
||||||
|
}: {
|
||||||
|
cal: CalendarWithOwner;
|
||||||
|
onRemove: () => void;
|
||||||
|
onColorChange: (color: string) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={cal.owner.email + cal.cal["dav:name"]}
|
||||||
|
display="flex"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="space-between"
|
||||||
|
sx={{
|
||||||
|
borderRadius: 2,
|
||||||
|
border: "1px solid #e5e7eb",
|
||||||
|
p: 1,
|
||||||
|
mb: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box display="flex" alignItems="center" gap={2}>
|
||||||
|
<Avatar
|
||||||
|
src={cal.owner.avatarUrl}
|
||||||
|
alt={cal.owner.email}
|
||||||
|
sx={{
|
||||||
|
border: `2px solid ${cal.cal["apple:color"] ?? "transparent"}`,
|
||||||
|
boxShadow: cal.cal["apple:color"]
|
||||||
|
? `0 0 0 2px ${cal.cal["apple:color"]}`
|
||||||
|
: "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="body1">{cal.cal["dav:name"]}</Typography>
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
{cal.owner.email}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
|
<ColorPicker
|
||||||
|
selectedColor={cal.cal["apple:color"]}
|
||||||
|
onChange={onColorChange}
|
||||||
|
/>
|
||||||
|
<IconButton size="small" onClick={onRemove}>
|
||||||
|
<CloseIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectedCalendarsList({
|
||||||
|
calendars,
|
||||||
|
selectedCal,
|
||||||
|
onRemove,
|
||||||
|
onColorChange,
|
||||||
|
}: {
|
||||||
|
calendars: Record<string, Calendars>;
|
||||||
|
selectedCal: CalendarWithOwner[];
|
||||||
|
onRemove: (cal: CalendarWithOwner) => void;
|
||||||
|
onColorChange: (cal: CalendarWithOwner, color: string) => void;
|
||||||
|
}) {
|
||||||
|
if (selectedCal.length === 0) return null;
|
||||||
|
|
||||||
|
const groupedByOwner = selectedCal.reduce<
|
||||||
|
Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
owner: User;
|
||||||
|
visibleCals: CalendarWithOwner[];
|
||||||
|
alreadyExisting: boolean;
|
||||||
|
}
|
||||||
|
>
|
||||||
|
>((acc, cal) => {
|
||||||
|
const exists = Object.values(calendars).some(
|
||||||
|
(existing: any) =>
|
||||||
|
existing.id ===
|
||||||
|
cal.cal?._links?.self?.href
|
||||||
|
.replace("/calendars/", "")
|
||||||
|
.replace(".json", "")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!acc[cal.owner.email]) {
|
||||||
|
acc[cal.owner.email] = {
|
||||||
|
owner: cal.owner,
|
||||||
|
visibleCals: [],
|
||||||
|
alreadyExisting: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
acc[cal.owner.email].alreadyExisting = true;
|
||||||
|
} else {
|
||||||
|
acc[cal.owner.email].visibleCals.push(cal);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box mt={2}>
|
||||||
|
<Typography variant="subtitle1" gutterBottom>
|
||||||
|
Name
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{Object.values(groupedByOwner).map(
|
||||||
|
({ owner, visibleCals, alreadyExisting }) => (
|
||||||
|
<Box key={owner.email} mb={2}>
|
||||||
|
{visibleCals.length > 0 ? (
|
||||||
|
visibleCals.map((cal) =>
|
||||||
|
cal.cal ? (
|
||||||
|
<CalendarItem
|
||||||
|
key={cal.owner.email + cal.cal["dav:name"]}
|
||||||
|
cal={cal}
|
||||||
|
onRemove={() => onRemove(cal)}
|
||||||
|
onColorChange={(color) => onColorChange(cal, color)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Typography color="textSecondary">
|
||||||
|
No publicly available calendars for {owner.displayName}
|
||||||
|
</Typography>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) : alreadyExisting ? (
|
||||||
|
<Typography color="textSecondary">
|
||||||
|
No more Calendar for {owner.displayName}
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CalendarSearch({
|
||||||
|
anchorEl,
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
anchorEl: HTMLElement | null;
|
||||||
|
open: boolean;
|
||||||
|
onClose: Function;
|
||||||
|
}) {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const openpaasId =
|
||||||
|
useAppSelector((state) => state.user.userData.openpaasId) ?? "";
|
||||||
|
const calendars = useAppSelector((state) => state.calendars.list);
|
||||||
|
|
||||||
|
const [selectedCal, setSelectedCalendars] = useState<CalendarWithOwner[]>([]);
|
||||||
|
const [selectedUsers, setSelectedUsers] = useState<User[]>([]);
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (selectedCal.length > 0) {
|
||||||
|
const idList = await Promise.all(
|
||||||
|
selectedCal.map(async (cal) => {
|
||||||
|
const calId = crypto.randomUUID();
|
||||||
|
const exists = Object.values(calendars).some(
|
||||||
|
(existing: any) =>
|
||||||
|
existing.id ===
|
||||||
|
cal.cal?._links?.self?.href
|
||||||
|
.replace("/calendars/", "")
|
||||||
|
.replace(".json", "")
|
||||||
|
);
|
||||||
|
if (!exists && cal.cal) {
|
||||||
|
await dispatch(
|
||||||
|
addSharedCalendarAsync({
|
||||||
|
userId: openpaasId,
|
||||||
|
calId,
|
||||||
|
cal: { ...cal, color: cal.cal["apple:color"] },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return cal.cal._links.self.href
|
||||||
|
.replace("/calendars/", "")
|
||||||
|
.replace(".json", "");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
onClose(idList.filter(Boolean));
|
||||||
|
setSelectedCalendars([]);
|
||||||
|
setSelectedUsers([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onClose={() => {
|
||||||
|
onClose({}, "backdropClick");
|
||||||
|
setSelectedCalendars([]);
|
||||||
|
setSelectedUsers([]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
left: "50%",
|
||||||
|
top: "20%",
|
||||||
|
transform: "translate( -50%)",
|
||||||
|
width: "50vw",
|
||||||
|
maxHeight: "80vh",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
p: 2,
|
||||||
|
borderRadius: 3,
|
||||||
|
boxShadow: 6,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
maxHeight: "80vh",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ position: "absolute", top: 8, right: 8 }}>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => onClose({}, "backdropClick")}
|
||||||
|
>
|
||||||
|
<CloseIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<CardHeader title="Browse other calendars" sx={{ pb: 0 }} />
|
||||||
|
<CardContent sx={{ flex: 1, overflow: "auto" }}>
|
||||||
|
<PeopleSearch
|
||||||
|
selectedUsers={selectedUsers}
|
||||||
|
onChange={async (event: any, value: User[]) => {
|
||||||
|
setSelectedUsers(value);
|
||||||
|
|
||||||
|
const cals = await Promise.all(
|
||||||
|
value.map(async (user: User) => {
|
||||||
|
const cals = (await getCalendars(
|
||||||
|
user.openpaasId,
|
||||||
|
"sharedPublic=true&withRights=true"
|
||||||
|
)) as Record<string, any>;
|
||||||
|
return cals._embedded?.["dav:calendar"]
|
||||||
|
? cals._embedded["dav:calendar"].map(
|
||||||
|
(cal: Record<string, any>) => ({ cal, owner: user })
|
||||||
|
)
|
||||||
|
: { cal: undefined, owner: user };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
setSelectedCalendars(cals.flat());
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectedCalendarsList
|
||||||
|
calendars={calendars}
|
||||||
|
selectedCal={selectedCal}
|
||||||
|
onRemove={(cal) => {
|
||||||
|
setSelectedCalendars((prev) =>
|
||||||
|
prev.filter(
|
||||||
|
(c) => c.cal._links.self.href !== cal.cal._links.self.href
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
!selectedCal.find(
|
||||||
|
(c) =>
|
||||||
|
cal.owner.email === c.owner.email &&
|
||||||
|
c.cal._links.self.href !== cal.cal._links.self.href
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
setSelectedUsers((prev) =>
|
||||||
|
prev.filter((u) => u.email !== cal.owner.email)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onColorChange={(cal, color) =>
|
||||||
|
setSelectedCalendars((prev) =>
|
||||||
|
prev.map((prevcal) =>
|
||||||
|
prevcal.owner.email === cal.owner.email &&
|
||||||
|
prevcal.cal._links.self.href === cal.cal._links.self.href
|
||||||
|
? {
|
||||||
|
...prevcal,
|
||||||
|
cal: { ...prevcal.cal, "apple:color": color },
|
||||||
|
}
|
||||||
|
: prevcal
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
<CardActions sx={{ justifyContent: "flex-end", gap: 1 }}>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => onClose({}, "backdropClick")}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant="contained" onClick={handleSave}>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</CardActions>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -72,6 +72,8 @@ function CalendarAccordion({
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
|
||||||
|
import CalendarSearch from "./CalendarSearch";
|
||||||
|
|
||||||
export default function CalendarSelection({
|
export default function CalendarSelection({
|
||||||
selectedCalendars,
|
selectedCalendars,
|
||||||
@@ -102,6 +104,9 @@ export default function CalendarSelection({
|
|||||||
const [selectedCalId, setSelectedCalId] = useState("");
|
const [selectedCalId, setSelectedCalId] = useState("");
|
||||||
|
|
||||||
const [anchorElCal, setAnchorElCal] = useState<HTMLElement | null>(null);
|
const [anchorElCal, setAnchorElCal] = useState<HTMLElement | null>(null);
|
||||||
|
const [anchorElCalOthers, setAnchorElCalOthers] =
|
||||||
|
useState<HTMLElement | null>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
@@ -137,7 +142,7 @@ export default function CalendarSelection({
|
|||||||
selectedCalendars={selectedCalendars}
|
selectedCalendars={selectedCalendars}
|
||||||
showAddButton
|
showAddButton
|
||||||
onAddClick={() => {
|
onAddClick={() => {
|
||||||
setAnchorElCal(document.body);
|
setAnchorElCalOthers(document.body);
|
||||||
}}
|
}}
|
||||||
handleToggle={handleCalendarToggle}
|
handleToggle={handleCalendarToggle}
|
||||||
setOpen={(id: string) => {
|
setOpen={(id: string) => {
|
||||||
@@ -146,15 +151,24 @@ export default function CalendarSelection({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CalendarPopover
|
<CalendarPopover
|
||||||
anchorEl={anchorElCal}
|
anchorEl={anchorElCal}
|
||||||
open={Boolean(anchorElCal)}
|
open={Boolean(anchorElCal)}
|
||||||
|
calendar={calendars[selectedCalId] ?? undefined}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setSelectedCalId("");
|
setSelectedCalId("");
|
||||||
setAnchorElCal(null);
|
setAnchorElCal(null);
|
||||||
}}
|
}}
|
||||||
calendar={calendars[selectedCalId] ?? undefined}
|
/>
|
||||||
|
<CalendarSearch
|
||||||
|
anchorEl={anchorElCalOthers}
|
||||||
|
open={Boolean(anchorElCalOthers)}
|
||||||
|
onClose={(newCalIds?: string[]) => {
|
||||||
|
setAnchorElCalOthers(null);
|
||||||
|
if (newCalIds?.length) {
|
||||||
|
newCalIds.forEach((id) => handleCalendarToggle(id));
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { api } from "../../utils/apiUtils";
|
import { api } from "../../utils/apiUtils";
|
||||||
|
|
||||||
export async function getCalendars(userId: string) {
|
export async function getCalendars(
|
||||||
|
userId: string,
|
||||||
|
scope: string = "personal=true&sharedDelegationStatus=accepted&sharedPublicSubscription=true&withRights=true"
|
||||||
|
) {
|
||||||
const calendars = await api
|
const calendars = await api
|
||||||
.get(
|
.get(`dav/calendars/${userId}.json?${scope}`, {
|
||||||
`dav/calendars/${userId}.json?personal=true&sharedDelegationStatus=accepted&sharedPublicSubscription=true&withRights=true`,
|
headers: {
|
||||||
{
|
Accept: "application/calendar+json",
|
||||||
headers: {
|
},
|
||||||
Accept: "application/calendar+json",
|
})
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.json();
|
.json();
|
||||||
return calendars;
|
return calendars;
|
||||||
}
|
}
|
||||||
@@ -52,6 +52,33 @@ export async function postCalendar(
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function addSharedCalendar(
|
||||||
|
userId: string,
|
||||||
|
calId: string,
|
||||||
|
cal: Record<string, any>
|
||||||
|
) {
|
||||||
|
const response = await api.post(`dav/calendars/${userId}.json`, {
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json, text/plain, */*",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: calId,
|
||||||
|
...cal.cal,
|
||||||
|
"calendarserver:source": {
|
||||||
|
acl: cal.cal.acl,
|
||||||
|
calendarHomeId: cal.cal.id,
|
||||||
|
color: cal.cal["apple:color"],
|
||||||
|
description: cal.cal["caldav:description"],
|
||||||
|
href: cal.cal._links.self.href,
|
||||||
|
id: cal.cal.id,
|
||||||
|
invite: cal.cal.invite,
|
||||||
|
name: cal.cal["dav:name"],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
export async function proppatchCalendar(
|
export async function proppatchCalendar(
|
||||||
calLink: string,
|
calLink: string,
|
||||||
patch: { name: string; desc: string; color: string }
|
patch: { name: string; desc: string; color: string }
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
import { ColorPicker } from "../../components/Calendar/CalendarColorPicker";
|
||||||
import { Calendars } from "./CalendarTypes";
|
import { Calendars } from "./CalendarTypes";
|
||||||
|
|
||||||
function CalendarPopover({
|
function CalendarPopover({
|
||||||
@@ -128,15 +129,11 @@ function CalendarPopover({
|
|||||||
multiline
|
multiline
|
||||||
rows={2}
|
rows={2}
|
||||||
/>
|
/>
|
||||||
<ButtonGroup>
|
<ColorPicker
|
||||||
{palette.map((c) => (
|
onChange={(color) => setColor(color)}
|
||||||
<Button
|
selectedColor={color}
|
||||||
key={c}
|
/>
|
||||||
style={{ backgroundColor: c }}
|
|
||||||
onClick={() => setColor(c)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ButtonGroup>
|
|
||||||
<Box mt={2} display="flex" justifyContent="flex-end" gap={1}>
|
<Box mt={2} display="flex" justifyContent="flex-end" gap={1}>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit";
|
|||||||
import { Calendars } from "./CalendarTypes";
|
import { Calendars } from "./CalendarTypes";
|
||||||
import { CalendarEvent } from "../Events/EventsTypes";
|
import { CalendarEvent } from "../Events/EventsTypes";
|
||||||
import {
|
import {
|
||||||
|
addSharedCalendar,
|
||||||
getCalendar,
|
getCalendar,
|
||||||
getCalendars,
|
getCalendars,
|
||||||
postCalendar,
|
postCalendar,
|
||||||
@@ -184,11 +185,64 @@ export const deleteEventAsync = createAsyncThunk<
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const createCalendarAsync = createAsyncThunk<
|
export const createCalendarAsync = createAsyncThunk<
|
||||||
{ userId: string; calId: string; color: string; name: string; desc: string }, // Return type
|
{
|
||||||
|
userId: string;
|
||||||
|
calId: string;
|
||||||
|
color: string;
|
||||||
|
name: string;
|
||||||
|
desc: string;
|
||||||
|
owner: string;
|
||||||
|
ownerEmails: string[];
|
||||||
|
}, // Return type
|
||||||
{ userId: string; calId: string; color: string; name: string; desc: string } // Arg type
|
{ userId: string; calId: string; color: string; name: string; desc: string } // Arg type
|
||||||
>("calendars/createCalendar", async ({ userId, calId, color, name, desc }) => {
|
>("calendars/createCalendar", async ({ userId, calId, color, name, desc }) => {
|
||||||
const response = await postCalendar(userId, calId, color, name, desc);
|
const response = await postCalendar(userId, calId, color, name, desc);
|
||||||
return { userId, calId, color, name, desc };
|
const ownerData: any = await getUserDetails(userId.split("/")[0]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId,
|
||||||
|
calId,
|
||||||
|
color,
|
||||||
|
name,
|
||||||
|
desc,
|
||||||
|
owner: `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${
|
||||||
|
ownerData.lastname
|
||||||
|
}`,
|
||||||
|
ownerEmails: ownerData.emails,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export const addSharedCalendarAsync = createAsyncThunk<
|
||||||
|
{
|
||||||
|
calId: string;
|
||||||
|
color: string;
|
||||||
|
name: string;
|
||||||
|
desc: string;
|
||||||
|
owner: string;
|
||||||
|
ownerEmails: string[];
|
||||||
|
}, // Return type
|
||||||
|
{ userId: string; calId: string; cal: Record<string, any> } // Arg type
|
||||||
|
>("calendars/addSharedCalendar", async ({ userId, calId, cal }) => {
|
||||||
|
const response = await addSharedCalendar(userId, calId, cal);
|
||||||
|
const ownerData: any = await getUserDetails(
|
||||||
|
cal.cal._links.self.href
|
||||||
|
.replace("/calendars/", "")
|
||||||
|
.replace(".json", "")
|
||||||
|
.split("/")[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
calId: cal.cal._links.self.href
|
||||||
|
.replace("/calendars/", "")
|
||||||
|
.replace(".json", ""),
|
||||||
|
color: cal.cal["apple:color"],
|
||||||
|
desc: cal.cal["caldav:description"],
|
||||||
|
name: cal.cal["dav:name"],
|
||||||
|
owner: `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${
|
||||||
|
ownerData.lastname
|
||||||
|
}`,
|
||||||
|
ownerEmails: ownerData.emails,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const CalendarSlice = createSlice({
|
const CalendarSlice = createSlice({
|
||||||
@@ -363,7 +417,10 @@ const CalendarSlice = createSlice({
|
|||||||
id: `${action.payload.userId}/${action.payload.calId}`,
|
id: `${action.payload.userId}/${action.payload.calId}`,
|
||||||
description: action.payload.desc,
|
description: action.payload.desc,
|
||||||
name: action.payload.name,
|
name: action.payload.name,
|
||||||
} as unknown as Calendars;
|
owner: action.payload.owner,
|
||||||
|
ownerEmails: action.payload.ownerEmails,
|
||||||
|
events: {},
|
||||||
|
} as Calendars;
|
||||||
})
|
})
|
||||||
.addCase(patchCalendarAsync.fulfilled, (state, action) => {
|
.addCase(patchCalendarAsync.fulfilled, (state, action) => {
|
||||||
state.pending = false;
|
state.pending = false;
|
||||||
@@ -391,6 +448,18 @@ const CalendarSlice = createSlice({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.addCase(addSharedCalendarAsync.fulfilled, (state, action) => {
|
||||||
|
state.pending = false;
|
||||||
|
state.list[action.payload.calId] = {
|
||||||
|
color: action.payload.color,
|
||||||
|
id: action.payload.calId,
|
||||||
|
description: action.payload.desc,
|
||||||
|
name: action.payload.name,
|
||||||
|
events: {},
|
||||||
|
owner: action.payload.owner,
|
||||||
|
ownerEmails: action.payload.ownerEmails,
|
||||||
|
} as Calendars;
|
||||||
|
})
|
||||||
.addCase(getCalendarDetailAsync.pending, (state) => {
|
.addCase(getCalendarDetailAsync.pending, (state) => {
|
||||||
state.pending = true;
|
state.pending = true;
|
||||||
})
|
})
|
||||||
@@ -414,6 +483,9 @@ const CalendarSlice = createSlice({
|
|||||||
})
|
})
|
||||||
.addCase(createCalendarAsync.pending, (state) => {
|
.addCase(createCalendarAsync.pending, (state) => {
|
||||||
state.pending = true;
|
state.pending = true;
|
||||||
|
})
|
||||||
|
.addCase(addSharedCalendarAsync.pending, (state) => {
|
||||||
|
state.pending = true;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -391,10 +391,7 @@ export default function EventDisplayModal({
|
|||||||
{isOwn && (
|
{isOwn && (
|
||||||
<AttendeeSelector
|
<AttendeeSelector
|
||||||
attendees={attendees}
|
attendees={attendees}
|
||||||
setAttendees={(value: userAttendee[]) => {
|
setAttendees={setAttendees}
|
||||||
const newAttendeeList = attendees.concat(value);
|
|
||||||
setAttendees(newAttendeeList);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,22 @@ export async function getOpenPaasUser() {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function searchUsers(query: string) {
|
export async function searchUsers(
|
||||||
|
query: string,
|
||||||
|
objectTypes: string[] = ["user", "contact"]
|
||||||
|
): Promise<
|
||||||
|
{
|
||||||
|
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({
|
||||||
limit: 10,
|
limit: 10,
|
||||||
objectTypes: ["user", "group", "contact", "ldap"],
|
objectTypes,
|
||||||
q: query,
|
q: query,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
@@ -21,6 +31,7 @@ export async function searchUsers(query: string) {
|
|||||||
displayName:
|
displayName:
|
||||||
user.names?.[0]?.displayName || user.emailAddresses?.[0]?.value,
|
user.names?.[0]?.displayName || user.emailAddresses?.[0]?.value,
|
||||||
avatarUrl: user.photos?.[0]?.url || "",
|
avatarUrl: user.photos?.[0]?.url || "",
|
||||||
|
openpaasId: user.id || "",
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user