Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
@@ -3,7 +3,7 @@ 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";
|
||||||
import { renderWithProviders } from "../utils/Renderwithproviders";
|
import { renderWithProviders } from "../utils/Renderwithproviders";
|
||||||
import EventPreviewModal from "../../src/features/Events/EventDisplayPreview";
|
import EventPreviewModal from "../../src/components/Event/EventDisplayPreview";
|
||||||
|
|
||||||
const day = new Date();
|
const day = new Date();
|
||||||
const preloadedState = {
|
const preloadedState = {
|
||||||
@@ -75,10 +75,9 @@ describe("EventDuplication", () => {
|
|||||||
preloadedState
|
preloadedState
|
||||||
);
|
);
|
||||||
|
|
||||||
const button = screen.getByRole("button");
|
fireEvent.click(screen.getByText("Duplicate event"));
|
||||||
fireEvent.click(button);
|
|
||||||
|
|
||||||
expect(screen.getByText(/Duplicate Event/i)).toBeInTheDocument();
|
expect(screen.getAllByText(/Duplicate Event/i)[1]).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls onClose when closing the popover", () => {
|
it("calls onClose when closing the popover", () => {
|
||||||
@@ -94,7 +93,7 @@ describe("EventDuplication", () => {
|
|||||||
preloadedState
|
preloadedState
|
||||||
);
|
);
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button"));
|
fireEvent.click(screen.getByText("Duplicate event"));
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: /Cancel/i }));
|
fireEvent.click(screen.getByRole("button", { name: /Cancel/i }));
|
||||||
|
|
||||||
@@ -159,13 +158,13 @@ describe("EventDisplayModal", () => {
|
|||||||
calId="667037022b752d0026472254/cal1"
|
calId="667037022b752d0026472254/cal1"
|
||||||
open={true}
|
open={true}
|
||||||
onClose={jest.fn()}
|
onClose={jest.fn()}
|
||||||
anchorPosition={{ top: 0, left: 0 }}
|
|
||||||
/>,
|
/>,
|
||||||
preloadedState
|
preloadedState
|
||||||
);
|
);
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("AddToPhotosIcon"));
|
fireEvent.click(screen.getByTestId("MoreVertIcon"));
|
||||||
expect(screen.getByText(/Duplicate Event/i)).toBeInTheDocument();
|
fireEvent.click(screen.getByText("Duplicate event"));
|
||||||
|
expect(screen.getAllByText(/Duplicate Event/i)[1]).toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
screen.getByDisplayValue(
|
screen.getByDisplayValue(
|
||||||
preloadedState.calendars.list["667037022b752d0026472254/cal1"].events
|
preloadedState.calendars.list["667037022b752d0026472254/cal1"].events
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
import { screen, fireEvent, waitFor, act } from "@testing-library/react";
|
import {
|
||||||
|
screen,
|
||||||
|
fireEvent,
|
||||||
|
waitFor,
|
||||||
|
act,
|
||||||
|
cleanup,
|
||||||
|
} from "@testing-library/react";
|
||||||
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 EventDisplayModal, {
|
import EventDisplayModal from "../../../src/features/Events/EventDisplay";
|
||||||
InfoRow,
|
import EventPreviewModal from "../../../src/components/Event/EventDisplayPreview";
|
||||||
stringAvatar,
|
import { InfoRow } from "../../../src/components/Event/InfoRow";
|
||||||
|
import {
|
||||||
stringToColor,
|
stringToColor,
|
||||||
} from "../../../src/features/Events/EventDisplay";
|
stringAvatar,
|
||||||
import EventPreviewModal from "../../../src/features/Events/EventDisplayPreview";
|
} from "../../../src/components/Event/utils/eventUtils";
|
||||||
|
|
||||||
describe("Event Preview Display", () => {
|
describe("Event Preview Display", () => {
|
||||||
const mockOnClose = jest.fn();
|
const mockOnClose = jest.fn();
|
||||||
@@ -104,7 +111,6 @@ describe("Event Preview Display", () => {
|
|||||||
});
|
});
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventPreviewModal
|
<EventPreviewModal
|
||||||
anchorPosition={{ top: 0, left: 0 }}
|
|
||||||
open={true}
|
open={true}
|
||||||
onClose={mockOnClose}
|
onClose={mockOnClose}
|
||||||
calId={"667037022b752d0026472254/cal1"}
|
calId={"667037022b752d0026472254/cal1"}
|
||||||
@@ -118,9 +124,8 @@ describe("Event Preview Display", () => {
|
|||||||
|
|
||||||
expect(screen.getByText("Test Event")).toBeInTheDocument();
|
expect(screen.getByText("Test Event")).toBeInTheDocument();
|
||||||
expect(screen.getByText(new RegExp(weekday, "i"))).toBeInTheDocument();
|
expect(screen.getByText(new RegExp(weekday, "i"))).toBeInTheDocument();
|
||||||
expect(screen.getByText(new RegExp(month, "i"))).toBeInTheDocument();
|
|
||||||
expect(
|
expect(
|
||||||
screen.getByText(new RegExp(`\\b${dayOfMonth}\\b`))
|
screen.getByText(new RegExp(`\\b${dayOfMonth}\\b ${month}`))
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
|
||||||
expect(screen.getByText(/\d{2}:\d{2} – \d{2}:\d{2}/)).toBeInTheDocument();
|
expect(screen.getByText(/\d{2}:\d{2} – \d{2}:\d{2}/)).toBeInTheDocument();
|
||||||
@@ -129,7 +134,6 @@ describe("Event Preview Display", () => {
|
|||||||
it("calls onClose when Cancel clicked", () => {
|
it("calls onClose when Cancel clicked", () => {
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventPreviewModal
|
<EventPreviewModal
|
||||||
anchorPosition={{ top: 0, left: 0 }}
|
|
||||||
open={true}
|
open={true}
|
||||||
onClose={mockOnClose}
|
onClose={mockOnClose}
|
||||||
calId={"667037022b752d0026472254/cal1"}
|
calId={"667037022b752d0026472254/cal1"}
|
||||||
@@ -145,7 +149,6 @@ describe("Event Preview Display", () => {
|
|||||||
// Renders the other cal event
|
// Renders the other cal event
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventPreviewModal
|
<EventPreviewModal
|
||||||
anchorPosition={{ top: 0, left: 0 }}
|
|
||||||
open={true}
|
open={true}
|
||||||
onClose={mockOnClose}
|
onClose={mockOnClose}
|
||||||
calId={"otherCal/cal"}
|
calId={"otherCal/cal"}
|
||||||
@@ -153,11 +156,12 @@ describe("Event Preview Display", () => {
|
|||||||
/>,
|
/>,
|
||||||
preloadedState
|
preloadedState
|
||||||
);
|
);
|
||||||
expect(screen.queryByTestId("DeleteIcon")).not.toBeInTheDocument();
|
fireEvent.click(screen.getByTestId("MoreVertIcon"));
|
||||||
|
expect(screen.queryByText("Delete event")).not.toBeInTheDocument();
|
||||||
|
cleanup();
|
||||||
// Renders the personnal cal event
|
// Renders the personnal cal event
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventPreviewModal
|
<EventPreviewModal
|
||||||
anchorPosition={{ top: 0, left: 0 }}
|
|
||||||
open={true}
|
open={true}
|
||||||
onClose={mockOnClose}
|
onClose={mockOnClose}
|
||||||
calId={"667037022b752d0026472254/cal1"}
|
calId={"667037022b752d0026472254/cal1"}
|
||||||
@@ -165,12 +169,12 @@ describe("Event Preview Display", () => {
|
|||||||
/>,
|
/>,
|
||||||
preloadedState
|
preloadedState
|
||||||
);
|
);
|
||||||
expect(screen.queryByTestId("DeleteIcon")).toBeInTheDocument();
|
fireEvent.click(screen.getByTestId("MoreVertIcon"));
|
||||||
|
expect(screen.queryByText("Delete event")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
it("calls delete when Delete clicked", async () => {
|
it("calls delete when Delete clicked", async () => {
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventPreviewModal
|
<EventPreviewModal
|
||||||
anchorPosition={{ top: 0, left: 0 }}
|
|
||||||
open={true}
|
open={true}
|
||||||
onClose={mockOnClose}
|
onClose={mockOnClose}
|
||||||
calId={"667037022b752d0026472254/cal1"}
|
calId={"667037022b752d0026472254/cal1"}
|
||||||
@@ -183,8 +187,8 @@ describe("Event Preview Display", () => {
|
|||||||
.mockImplementation((payload) => {
|
.mockImplementation((payload) => {
|
||||||
return () => Promise.resolve(payload) as any;
|
return () => Promise.resolve(payload) as any;
|
||||||
});
|
});
|
||||||
|
fireEvent.click(screen.getByTestId("MoreVertIcon"));
|
||||||
fireEvent.click(screen.getByTestId("DeleteIcon"));
|
fireEvent.click(screen.getByText("Delete event"));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(spy).toHaveBeenCalled();
|
expect(spy).toHaveBeenCalled();
|
||||||
@@ -237,7 +241,6 @@ describe("Event Preview Display", () => {
|
|||||||
|
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventPreviewModal
|
<EventPreviewModal
|
||||||
anchorPosition={{ top: 0, left: 0 }}
|
|
||||||
open={true}
|
open={true}
|
||||||
onClose={mockOnClose}
|
onClose={mockOnClose}
|
||||||
calId={"667037022b752d0026472254/cal1"}
|
calId={"667037022b752d0026472254/cal1"}
|
||||||
@@ -246,7 +249,7 @@ describe("Event Preview Display", () => {
|
|||||||
rsvpStateIsOrga
|
rsvpStateIsOrga
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText("Will you attend?")).toBeInTheDocument();
|
expect(screen.getByText("Attending?")).toBeInTheDocument();
|
||||||
expect(screen.getByRole("button", { name: "Accept" })).toBeInTheDocument();
|
expect(screen.getByRole("button", { name: "Accept" })).toBeInTheDocument();
|
||||||
expect(screen.getByRole("button", { name: "Maybe" })).toBeInTheDocument();
|
expect(screen.getByRole("button", { name: "Maybe" })).toBeInTheDocument();
|
||||||
expect(screen.getByRole("button", { name: "Decline" })).toBeInTheDocument();
|
expect(screen.getByRole("button", { name: "Decline" })).toBeInTheDocument();
|
||||||
@@ -284,7 +287,6 @@ describe("Event Preview Display", () => {
|
|||||||
|
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventPreviewModal
|
<EventPreviewModal
|
||||||
anchorPosition={{ top: 0, left: 0 }}
|
|
||||||
open={true}
|
open={true}
|
||||||
onClose={mockOnClose}
|
onClose={mockOnClose}
|
||||||
calId={"667037022b752d0026472254/cal1"}
|
calId={"667037022b752d0026472254/cal1"}
|
||||||
@@ -293,7 +295,7 @@ describe("Event Preview Display", () => {
|
|||||||
rsvpStateIsOrga
|
rsvpStateIsOrga
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.queryByText("Will you attend?")).not.toBeInTheDocument();
|
expect(screen.queryByText("Attending?")).not.toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
screen.queryByRole("button", { name: "Accept" })
|
screen.queryByRole("button", { name: "Accept" })
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
@@ -344,7 +346,6 @@ describe("Event Preview Display", () => {
|
|||||||
|
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventPreviewModal
|
<EventPreviewModal
|
||||||
anchorPosition={{ top: 0, left: 0 }}
|
|
||||||
open={true}
|
open={true}
|
||||||
onClose={mockOnClose}
|
onClose={mockOnClose}
|
||||||
calId={"667037022b752d0026472254/cal1"}
|
calId={"667037022b752d0026472254/cal1"}
|
||||||
@@ -403,7 +404,6 @@ describe("Event Preview Display", () => {
|
|||||||
|
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventPreviewModal
|
<EventPreviewModal
|
||||||
anchorPosition={{ top: 0, left: 0 }}
|
|
||||||
open={true}
|
open={true}
|
||||||
onClose={mockOnClose}
|
onClose={mockOnClose}
|
||||||
calId={"667037022b752d0026472254/cal1"}
|
calId={"667037022b752d0026472254/cal1"}
|
||||||
@@ -462,7 +462,6 @@ describe("Event Preview Display", () => {
|
|||||||
|
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventPreviewModal
|
<EventPreviewModal
|
||||||
anchorPosition={{ top: 0, left: 0 }}
|
|
||||||
open={true}
|
open={true}
|
||||||
onClose={mockOnClose}
|
onClose={mockOnClose}
|
||||||
calId={"667037022b752d0026472254/cal1"}
|
calId={"667037022b752d0026472254/cal1"}
|
||||||
@@ -496,7 +495,6 @@ describe("Event Preview Display", () => {
|
|||||||
|
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventPreviewModal
|
<EventPreviewModal
|
||||||
anchorPosition={{ top: 0, left: 0 }}
|
|
||||||
open={true}
|
open={true}
|
||||||
onClose={mockOnClose}
|
onClose={mockOnClose}
|
||||||
calId={"667037022b752d0026472254/cal1"}
|
calId={"667037022b752d0026472254/cal1"}
|
||||||
@@ -516,7 +514,6 @@ describe("Event Preview Display", () => {
|
|||||||
(window as any).MAIL_SPA_URL = "test";
|
(window as any).MAIL_SPA_URL = "test";
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventPreviewModal
|
<EventPreviewModal
|
||||||
anchorPosition={{ top: 0, left: 0 }}
|
|
||||||
open={true}
|
open={true}
|
||||||
onClose={mockOnClose}
|
onClose={mockOnClose}
|
||||||
calId={"667037022b752d0026472254/cal1"}
|
calId={"667037022b752d0026472254/cal1"}
|
||||||
@@ -524,13 +521,13 @@ describe("Event Preview Display", () => {
|
|||||||
/>,
|
/>,
|
||||||
preloadedState
|
preloadedState
|
||||||
);
|
);
|
||||||
expect(screen.getByTestId("EmailIcon")).toBeInTheDocument();
|
fireEvent.click(screen.getByTestId("MoreVertIcon"));
|
||||||
|
expect(screen.getByText("Email attendees")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
it("doesnt render message button when MAIL_SPA_URL is not null and event has no attendees", () => {
|
it("doesnt render message button when MAIL_SPA_URL is not null and event has no attendees", () => {
|
||||||
(window as any).MAIL_SPA_URL = "test";
|
(window as any).MAIL_SPA_URL = "test";
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventPreviewModal
|
<EventPreviewModal
|
||||||
anchorPosition={{ top: 0, left: 0 }}
|
|
||||||
open={true}
|
open={true}
|
||||||
onClose={mockOnClose}
|
onClose={mockOnClose}
|
||||||
calId={"667037022b752d0026472254/cal1"}
|
calId={"667037022b752d0026472254/cal1"}
|
||||||
@@ -543,7 +540,6 @@ describe("Event Preview Display", () => {
|
|||||||
it("doesnt render message button when MAIL_SPA_URL is null and event has attendees", () => {
|
it("doesnt render message button when MAIL_SPA_URL is null and event has attendees", () => {
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventPreviewModal
|
<EventPreviewModal
|
||||||
anchorPosition={{ top: 0, left: 0 }}
|
|
||||||
open={true}
|
open={true}
|
||||||
onClose={mockOnClose}
|
onClose={mockOnClose}
|
||||||
calId={"667037022b752d0026472254/cal1"}
|
calId={"667037022b752d0026472254/cal1"}
|
||||||
@@ -560,7 +556,6 @@ describe("Event Preview Display", () => {
|
|||||||
|
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventPreviewModal
|
<EventPreviewModal
|
||||||
anchorPosition={{ top: 0, left: 0 }}
|
|
||||||
open={true}
|
open={true}
|
||||||
onClose={mockOnClose}
|
onClose={mockOnClose}
|
||||||
calId={"667037022b752d0026472254/cal1"}
|
calId={"667037022b752d0026472254/cal1"}
|
||||||
@@ -569,10 +564,11 @@ describe("Event Preview Display", () => {
|
|||||||
preloadedState
|
preloadedState
|
||||||
);
|
);
|
||||||
|
|
||||||
const emailButton = screen.getByTestId("EmailIcon");
|
fireEvent.click(screen.getByTestId("MoreVertIcon"));
|
||||||
|
const emailButton = screen.getByText("Email attendees");
|
||||||
expect(emailButton).toBeInTheDocument();
|
expect(emailButton).toBeInTheDocument();
|
||||||
|
|
||||||
fireEvent.click(emailButton.closest("button")!);
|
fireEvent.click(emailButton);
|
||||||
|
|
||||||
const event =
|
const event =
|
||||||
preloadedState.calendars.list["667037022b752d0026472254/cal1"].events[
|
preloadedState.calendars.list["667037022b752d0026472254/cal1"].events[
|
||||||
@@ -1403,7 +1399,7 @@ describe("Event Full Display", () => {
|
|||||||
videoState
|
videoState
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText("Video conference available")).toBeInTheDocument();
|
expect(screen.getByText("Join the video conference")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1416,7 +1412,7 @@ describe("Helper functions", () => {
|
|||||||
it("stringAvatar returns correct props", () => {
|
it("stringAvatar returns correct props", () => {
|
||||||
const result = stringAvatar("Alice");
|
const result = stringAvatar("Alice");
|
||||||
expect(result.children).toBe("A");
|
expect(result.children).toBe("A");
|
||||||
expect(result.sx.bgcolor).toMatch(/^#/);
|
expect(result.style.backgroundColor).toMatch(/^#/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("InfoRow renders text and link if url is valid", () => {
|
it("InfoRow renders text and link if url is valid", () => {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
} from "../../utils/dateUtils";
|
} from "../../utils/dateUtils";
|
||||||
import { Calendars } from "../../features/Calendars/CalendarTypes";
|
import { Calendars } from "../../features/Calendars/CalendarTypes";
|
||||||
import { push } from "redux-first-history";
|
import { push } from "redux-first-history";
|
||||||
import EventPreviewModal from "../../features/Events/EventDisplayPreview";
|
import EventPreviewModal from "../Event/EventDisplayPreview";
|
||||||
import { createSelector } from "@reduxjs/toolkit";
|
import { createSelector } from "@reduxjs/toolkit";
|
||||||
import AddIcon from "@mui/icons-material/Add";
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
import { TempCalendarsInput } from "./TempCalendarsInput";
|
import { TempCalendarsInput } from "./TempCalendarsInput";
|
||||||
@@ -161,10 +161,7 @@ export default function CalendarApp({
|
|||||||
}, [rangeKey, Object.keys(tempcalendars).join(","), pending]);
|
}, [rangeKey, Object.keys(tempcalendars).join(","), pending]);
|
||||||
|
|
||||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
||||||
const [anchorPosition, setAnchorPosition] = useState<{
|
|
||||||
top: number;
|
|
||||||
left: number;
|
|
||||||
} | 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 [eventDisplayedTemp, setEventDisplayedTemp] = useState(false);
|
||||||
@@ -187,7 +184,6 @@ export default function CalendarApp({
|
|||||||
calendarRange,
|
calendarRange,
|
||||||
dispatch,
|
dispatch,
|
||||||
setOpenEventDisplay,
|
setOpenEventDisplay,
|
||||||
setAnchorPosition,
|
|
||||||
setEventDisplayedId,
|
setEventDisplayedId,
|
||||||
setEventDisplayedCalId,
|
setEventDisplayedCalId,
|
||||||
setEventDisplayedTemp,
|
setEventDisplayedTemp,
|
||||||
@@ -431,7 +427,6 @@ export default function CalendarApp({
|
|||||||
eventId={eventDisplayedId}
|
eventId={eventDisplayedId}
|
||||||
calId={eventDisplayedCalId}
|
calId={eventDisplayedCalId}
|
||||||
tempEvent={eventDisplayedTemp}
|
tempEvent={eventDisplayedTemp}
|
||||||
anchorPosition={anchorPosition}
|
|
||||||
open={openEventDisplay}
|
open={openEventDisplay}
|
||||||
onClose={eventHandlers.handleCloseEventDisplay}
|
onClose={eventHandlers.handleCloseEventDisplay}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ function CalendarSelector({
|
|||||||
<div>
|
<div>
|
||||||
<label>
|
<label>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
sx={{
|
style={{
|
||||||
color: calendars[id].color,
|
color: calendars[id].color,
|
||||||
"&.Mui-checked": { color: calendars[id].color },
|
"&.Mui-checked": { color: calendars[id].color },
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ export interface EventHandlersProps {
|
|||||||
calendarRange: { start: Date; end: Date };
|
calendarRange: { start: Date; end: Date };
|
||||||
dispatch: any;
|
dispatch: any;
|
||||||
setOpenEventDisplay: (open: boolean) => void;
|
setOpenEventDisplay: (open: boolean) => void;
|
||||||
setAnchorPosition: (position: { top: number; left: number } | null) => void;
|
|
||||||
setEventDisplayedId: (id: string) => void;
|
setEventDisplayedId: (id: string) => void;
|
||||||
setEventDisplayedCalId: (id: string) => void;
|
setEventDisplayedCalId: (id: string) => void;
|
||||||
setEventDisplayedTemp: (temp: boolean) => void;
|
setEventDisplayedTemp: (temp: boolean) => void;
|
||||||
@@ -37,7 +36,6 @@ export const createEventHandlers = (props: EventHandlersProps) => {
|
|||||||
calendarRange,
|
calendarRange,
|
||||||
dispatch,
|
dispatch,
|
||||||
setOpenEventDisplay,
|
setOpenEventDisplay,
|
||||||
setAnchorPosition,
|
|
||||||
setEventDisplayedId,
|
setEventDisplayedId,
|
||||||
setEventDisplayedCalId,
|
setEventDisplayedCalId,
|
||||||
setEventDisplayedTemp,
|
setEventDisplayedTemp,
|
||||||
@@ -79,7 +77,6 @@ export const createEventHandlers = (props: EventHandlersProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseEventDisplay = () => {
|
const handleCloseEventDisplay = () => {
|
||||||
setAnchorPosition(null);
|
|
||||||
setOpenEventDisplay(false);
|
setOpenEventDisplay(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -109,10 +106,6 @@ export const createEventHandlers = (props: EventHandlersProps) => {
|
|||||||
} else {
|
} else {
|
||||||
console.log(info.event);
|
console.log(info.event);
|
||||||
setOpenEventDisplay(true);
|
setOpenEventDisplay(true);
|
||||||
setAnchorPosition({
|
|
||||||
top: info.jsEvent.clientY,
|
|
||||||
left: info.jsEvent.clientX,
|
|
||||||
});
|
|
||||||
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);
|
setEventDisplayedTemp(info.event._def.extendedProps.temp);
|
||||||
|
|||||||
@@ -136,7 +136,12 @@ export const createViewHandlers = (props: ViewHandlersProps) => {
|
|||||||
const handleEventContent = (arg: any) => {
|
const handleEventContent = (arg: any) => {
|
||||||
const event = arg.event;
|
const event = arg.event;
|
||||||
const props = event._def.extendedProps;
|
const props = event._def.extendedProps;
|
||||||
const { calId, temp, attendees = [], class: classification } = props;
|
const {
|
||||||
|
calId,
|
||||||
|
temp,
|
||||||
|
attendee: attendees = [],
|
||||||
|
class: classification,
|
||||||
|
} = props;
|
||||||
|
|
||||||
const calendarsSource = temp ? tempcalendars : calendars;
|
const calendarsSource = temp ? tempcalendars : calendars;
|
||||||
const calendar = calendarsSource[calId];
|
const calendar = calendarsSource[calId];
|
||||||
|
|||||||
@@ -23,13 +23,12 @@ export const useCalendarEventHandlers = (props: EventHandlersProps) => {
|
|||||||
]),
|
]),
|
||||||
handleCloseEventDisplay: useCallback(
|
handleCloseEventDisplay: useCallback(
|
||||||
eventHandlers.handleCloseEventDisplay,
|
eventHandlers.handleCloseEventDisplay,
|
||||||
[props.setAnchorPosition, props.setOpenEventDisplay]
|
[props.setOpenEventDisplay]
|
||||||
),
|
),
|
||||||
handleMonthUp: useCallback(eventHandlers.handleMonthUp, []),
|
handleMonthUp: useCallback(eventHandlers.handleMonthUp, []),
|
||||||
handleMonthDown: useCallback(eventHandlers.handleMonthDown, []),
|
handleMonthDown: useCallback(eventHandlers.handleMonthDown, []),
|
||||||
handleEventClick: useCallback(eventHandlers.handleEventClick, [
|
handleEventClick: useCallback(eventHandlers.handleEventClick, [
|
||||||
props.setOpenEventDisplay,
|
props.setOpenEventDisplay,
|
||||||
props.setAnchorPosition,
|
|
||||||
props.setEventDisplayedId,
|
props.setEventDisplayedId,
|
||||||
props.setEventDisplayedCalId,
|
props.setEventDisplayedCalId,
|
||||||
props.setEventDisplayedTemp,
|
props.setEventDisplayedTemp,
|
||||||
|
|||||||
@@ -0,0 +1,452 @@
|
|||||||
|
import CalendarTodayIcon from "@mui/icons-material/CalendarToday";
|
||||||
|
import CircleIcon from "@mui/icons-material/Circle";
|
||||||
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
|
import EditIcon from "@mui/icons-material/Edit";
|
||||||
|
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
|
||||||
|
import FileDownloadOutlinedIcon from "@mui/icons-material/FileDownloadOutlined";
|
||||||
|
import LocationOnOutlinedIcon from "@mui/icons-material/LocationOnOutlined";
|
||||||
|
import MoreVertIcon from "@mui/icons-material/MoreVert";
|
||||||
|
import NotificationsNoneIcon from "@mui/icons-material/NotificationsNone";
|
||||||
|
import PeopleAltOutlinedIcon from "@mui/icons-material/PeopleAltOutlined";
|
||||||
|
import SubjectIcon from "@mui/icons-material/Subject";
|
||||||
|
import VideocamIcon from "@mui/icons-material/Videocam";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
|
DialogActions,
|
||||||
|
Divider,
|
||||||
|
IconButton,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
Typography,
|
||||||
|
} from "@mui/material";
|
||||||
|
import AvatarGroup from "@mui/material/AvatarGroup";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
||||||
|
import {
|
||||||
|
deleteEventAsync,
|
||||||
|
getEventAsync,
|
||||||
|
putEventAsync,
|
||||||
|
} from "../../features/Calendars/CalendarSlice";
|
||||||
|
import { dlEvent } from "../../features/Events/EventApi";
|
||||||
|
import EventDisplayModal from "../../features/Events/EventDisplay";
|
||||||
|
import { ResponsiveDialog } from "../Dialog";
|
||||||
|
import EventDuplication from "./EventDuplicate";
|
||||||
|
import { InfoRow } from "./InfoRow";
|
||||||
|
import { renderAttendeeBadge } from "./utils/eventUtils";
|
||||||
|
export default function EventPreviewModal({
|
||||||
|
eventId,
|
||||||
|
calId,
|
||||||
|
tempEvent,
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
eventId: string;
|
||||||
|
calId: string;
|
||||||
|
tempEvent?: boolean;
|
||||||
|
open: boolean;
|
||||||
|
onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void;
|
||||||
|
}) {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const calendars = useAppSelector((state) => state.calendars);
|
||||||
|
const calendar = tempEvent
|
||||||
|
? calendars.templist[calId]
|
||||||
|
: calendars.list[calId];
|
||||||
|
const event = calendar.events[eventId];
|
||||||
|
const user = useAppSelector((state) => state.user);
|
||||||
|
const [showAllAttendees, setShowAllAttendees] = useState(false);
|
||||||
|
const [openFullDisplay, setOpenFullDisplay] = useState(false);
|
||||||
|
const [toggleActionMenu, setToggleActionMenu] = useState<Element | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const mailSpaUrl = (window as any).MAIL_SPA_URL ?? null;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!event || !calendar) {
|
||||||
|
onClose({}, "backdropClick");
|
||||||
|
}
|
||||||
|
}, [event, calendar, onClose]);
|
||||||
|
|
||||||
|
if (!event || !calendar) return null;
|
||||||
|
|
||||||
|
const attendeeDisplayLimit = 3;
|
||||||
|
|
||||||
|
const attendees =
|
||||||
|
event.attendee?.filter(
|
||||||
|
(a) => a.cal_address !== event.organizer?.cal_address
|
||||||
|
) || [];
|
||||||
|
|
||||||
|
const visibleAttendees = showAllAttendees
|
||||||
|
? attendees
|
||||||
|
: attendees.slice(0, attendeeDisplayLimit);
|
||||||
|
|
||||||
|
const currentUserAttendee = event.attendee?.find(
|
||||||
|
(person) => person.cal_address === user.userData.email
|
||||||
|
);
|
||||||
|
|
||||||
|
const organizer = event.attendee?.find(
|
||||||
|
(a) => a.cal_address === event.organizer?.cal_address
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleRSVP(rsvp: string) {
|
||||||
|
const newEvent = {
|
||||||
|
...event,
|
||||||
|
attendee: event.attendee?.map((a) =>
|
||||||
|
a.cal_address === user.userData.email ? { ...a, partstat: rsvp } : a
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch(putEventAsync({ cal: calendar, newEvent }));
|
||||||
|
onClose({}, "backdropClick");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ResponsiveDialog
|
||||||
|
open={open}
|
||||||
|
onClose={() => onClose({}, "backdropClick")}
|
||||||
|
style={{ overflow: "auto" }}
|
||||||
|
title={
|
||||||
|
event.title && (
|
||||||
|
<>
|
||||||
|
<DialogActions>
|
||||||
|
<Box>
|
||||||
|
{(window as any).DEBUG && (
|
||||||
|
<IconButton
|
||||||
|
onClick={async () => {
|
||||||
|
const icsContent = await dlEvent(event);
|
||||||
|
const blob = new Blob([icsContent], {
|
||||||
|
type: "text/calendar",
|
||||||
|
});
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = url;
|
||||||
|
link.download = `${eventId}.ics`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FileDownloadOutlinedIcon />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
{user.userData.email === event.organizer?.cal_address && (
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={async () => {
|
||||||
|
setOpenFullDisplay(!openFullDisplay);
|
||||||
|
await dispatch(getEventAsync(event));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EditIcon />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={(e) => setToggleActionMenu(e.currentTarget)}
|
||||||
|
>
|
||||||
|
<MoreVertIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Menu
|
||||||
|
open={Boolean(toggleActionMenu)}
|
||||||
|
onClose={() => setToggleActionMenu(null)}
|
||||||
|
anchorEl={toggleActionMenu}
|
||||||
|
>
|
||||||
|
{mailSpaUrl && attendees.length > 0 && (
|
||||||
|
<MenuItem
|
||||||
|
onClick={() =>
|
||||||
|
window.open(
|
||||||
|
`${mailSpaUrl}/mailto/?uri=mailto:${event.attendee
|
||||||
|
.map((a) => a.cal_address)
|
||||||
|
.filter((mail) => mail !== user.userData.email)
|
||||||
|
.join(",")}?subject=${event.title}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Email attendees
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
<EventDuplication event={event} onClose={onClose} />
|
||||||
|
{user.userData.email === event.organizer?.cal_address && (
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
onClose({}, "backdropClick");
|
||||||
|
dispatch(
|
||||||
|
deleteEventAsync({
|
||||||
|
calId,
|
||||||
|
eventId,
|
||||||
|
eventURL: event.URL,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete event
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</Menu>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => onClose({}, "backdropClick")}
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</DialogActions>
|
||||||
|
<Typography
|
||||||
|
variant="inherit"
|
||||||
|
fontWeight="bold"
|
||||||
|
style={{
|
||||||
|
wordBreak: "break-word",
|
||||||
|
}}
|
||||||
|
gutterBottom
|
||||||
|
>
|
||||||
|
{event.title}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||||
|
{formatDate(event.start, event.allday)}
|
||||||
|
{event.end &&
|
||||||
|
formatEnd(event.start, event.end, event.allday) &&
|
||||||
|
` – ${formatEnd(event.start, event.end, event.allday)}`}
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
|
{currentUserAttendee &&
|
||||||
|
event.uid?.split("/")[1] &&
|
||||||
|
"event is a reccuring event, the interraction between acceptance and recurrence is broken and to avoid loosing reccuring events by accepting or refusing them this feature isn't available for now"}
|
||||||
|
{currentUserAttendee && !event.uid?.split("/")[1] && (
|
||||||
|
<Box style={{ display: "flex", flexDirection: "row" }}>
|
||||||
|
<Typography variant="body2">Attending?</Typography>
|
||||||
|
<ButtonGroup size="small" fullWidth>
|
||||||
|
<Button
|
||||||
|
color={
|
||||||
|
currentUserAttendee?.partstat === "ACCEPTED"
|
||||||
|
? "success"
|
||||||
|
: "primary"
|
||||||
|
}
|
||||||
|
onClick={() => handleRSVP("ACCEPTED")}
|
||||||
|
>
|
||||||
|
Accept
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color={
|
||||||
|
currentUserAttendee?.partstat === "TENTATIVE"
|
||||||
|
? "warning"
|
||||||
|
: "primary"
|
||||||
|
}
|
||||||
|
onClick={() => handleRSVP("TENTATIVE")}
|
||||||
|
>
|
||||||
|
Maybe
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color={
|
||||||
|
currentUserAttendee?.partstat === "DECLINED"
|
||||||
|
? "error"
|
||||||
|
: "primary"
|
||||||
|
}
|
||||||
|
onClick={() => handleRSVP("DECLINED")}
|
||||||
|
>
|
||||||
|
Decline
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{/* Video */}
|
||||||
|
{event.x_openpass_videoconference && (
|
||||||
|
<InfoRow
|
||||||
|
icon={<VideocamIcon style={{ fontSize: 18 }} />}
|
||||||
|
content={
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => window.open(event.x_openpass_videoconference)}
|
||||||
|
>
|
||||||
|
Join the video conference
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{/* Attendees */}
|
||||||
|
{attendees?.length > 0 && (
|
||||||
|
<>
|
||||||
|
<InfoRow
|
||||||
|
icon={<PeopleAltOutlinedIcon />}
|
||||||
|
content={
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
marginBottom: 1,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Typography>{attendees.length} guests</Typography>
|
||||||
|
<Typography>
|
||||||
|
{
|
||||||
|
attendees.filter((a) => a.partstat === "ACCEPTED")
|
||||||
|
.length
|
||||||
|
}{" "}
|
||||||
|
yes,{" "}
|
||||||
|
{
|
||||||
|
attendees.filter((a) => a.partstat === "DECLINED")
|
||||||
|
.length
|
||||||
|
}{" "}
|
||||||
|
no
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
{!showAllAttendees && (
|
||||||
|
<AvatarGroup max={attendeeDisplayLimit}>
|
||||||
|
{organizer &&
|
||||||
|
renderAttendeeBadge(
|
||||||
|
organizer,
|
||||||
|
"org",
|
||||||
|
showAllAttendees,
|
||||||
|
true
|
||||||
|
)}
|
||||||
|
{visibleAttendees.map((a, idx) =>
|
||||||
|
renderAttendeeBadge(a, idx.toString(), showAllAttendees)
|
||||||
|
)}
|
||||||
|
</AvatarGroup>
|
||||||
|
)}
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="primary"
|
||||||
|
style={{ cursor: "pointer", marginTop: 0.5 }}
|
||||||
|
onClick={() => setShowAllAttendees(!showAllAttendees)}
|
||||||
|
>
|
||||||
|
{showAllAttendees ? "Show less" : `Show more `}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{showAllAttendees &&
|
||||||
|
organizer &&
|
||||||
|
renderAttendeeBadge(organizer, "org", showAllAttendees, true)}
|
||||||
|
{showAllAttendees &&
|
||||||
|
visibleAttendees.map((a, idx) =>
|
||||||
|
renderAttendeeBadge(a, idx.toString(), showAllAttendees)
|
||||||
|
)}
|
||||||
|
{/* Location */}
|
||||||
|
{event.location && (
|
||||||
|
<InfoRow icon={<LocationOnOutlinedIcon />} text={event.location} />
|
||||||
|
)}
|
||||||
|
{/* Description */}
|
||||||
|
{event.description && (
|
||||||
|
<InfoRow icon={<SubjectIcon />} text={event.description} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
{event.alarm && (
|
||||||
|
<InfoRow
|
||||||
|
icon={<NotificationsNoneIcon />}
|
||||||
|
text={`${event.alarm.trigger} before by ${event.alarm.action}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Error */}
|
||||||
|
{event.error && (
|
||||||
|
<InfoRow
|
||||||
|
icon={<ErrorOutlineIcon color="error" style={{ fontSize: 18 }} />}
|
||||||
|
text={event.error}
|
||||||
|
error
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Calendar color dot */}
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 1,
|
||||||
|
marginBottom: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CalendarTodayIcon style={{ fontSize: 16 }} />
|
||||||
|
<CircleIcon
|
||||||
|
style={{
|
||||||
|
color: calendar.color ?? "#3788D8",
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography variant="body2">{calendar.name}</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{currentUserAttendee && !event.repetition && (
|
||||||
|
<>
|
||||||
|
<Divider variant="fullWidth" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ResponsiveDialog>
|
||||||
|
<EventDisplayModal
|
||||||
|
open={openFullDisplay}
|
||||||
|
onClose={() => setOpenFullDisplay(false)}
|
||||||
|
eventId={eventId}
|
||||||
|
calId={calId}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(date: Date | string, allday?: boolean) {
|
||||||
|
if (allday) {
|
||||||
|
return new Date(date).toLocaleDateString(undefined, {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
weekday: "long",
|
||||||
|
day: "numeric",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return new Date(date).toLocaleString(undefined, {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
weekday: "long",
|
||||||
|
day: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatEnd(start: Date | string, end: Date | string, allday?: boolean) {
|
||||||
|
const startDate = new Date(start);
|
||||||
|
const endDate = new Date(end);
|
||||||
|
|
||||||
|
const sameDay =
|
||||||
|
startDate.getFullYear() === endDate.getFullYear() &&
|
||||||
|
startDate.getMonth() === endDate.getMonth() &&
|
||||||
|
startDate.getDate() === endDate.getDate();
|
||||||
|
|
||||||
|
if (allday) {
|
||||||
|
return sameDay
|
||||||
|
? null
|
||||||
|
: endDate.toLocaleDateString(undefined, {
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (sameDay) {
|
||||||
|
return endDate.toLocaleTimeString(undefined, {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return endDate.toLocaleString(undefined, {
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IconButton } from "@mui/material";
|
import { IconButton, MenuItem } from "@mui/material";
|
||||||
import { CalendarEvent } from "../../features/Events/EventsTypes";
|
import { CalendarEvent } from "../../features/Events/EventsTypes";
|
||||||
import AddToPhotosIcon from "@mui/icons-material/AddToPhotos";
|
import AddToPhotosIcon from "@mui/icons-material/AddToPhotos";
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
@@ -30,14 +30,13 @@ export default function EventDuplication({
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<IconButton
|
<MenuItem
|
||||||
size="small"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setOpenModal(true);
|
setOpenModal(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AddToPhotosIcon fontSize="small" />
|
Duplicate event
|
||||||
</IconButton>
|
</MenuItem>
|
||||||
<EventPopover
|
<EventPopover
|
||||||
anchorEl={null}
|
anchorEl={null}
|
||||||
open={openModal}
|
open={openModal}
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Box, Typography, Link } from "@mui/material";
|
||||||
|
|
||||||
|
type InfoRowProps = {
|
||||||
|
icon: React.ReactNode;
|
||||||
|
text?: string;
|
||||||
|
error?: boolean;
|
||||||
|
data?: string; // optional link target
|
||||||
|
content?: React.ReactNode; // if provided, overrides text rendering
|
||||||
|
};
|
||||||
|
|
||||||
|
function detectUrls(text: string) {
|
||||||
|
// Simple regex that captures whole URLs without splitting them apart
|
||||||
|
const urlRegex = /(https?:\/\/[^\s]+|www\.[^\s]+)/gi;
|
||||||
|
|
||||||
|
const parts = [];
|
||||||
|
let lastIndex = 0;
|
||||||
|
|
||||||
|
text.replace(urlRegex, (match, _, offset) => {
|
||||||
|
// Push the text before the match
|
||||||
|
if (lastIndex < offset) {
|
||||||
|
parts.push(
|
||||||
|
<React.Fragment key={lastIndex}>
|
||||||
|
{text.slice(lastIndex, offset)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize href
|
||||||
|
const href = match.startsWith("http") ? match : `https://${match}`;
|
||||||
|
parts.push(
|
||||||
|
<Link
|
||||||
|
key={offset}
|
||||||
|
href={href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
underline="always"
|
||||||
|
>
|
||||||
|
{match}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
|
||||||
|
lastIndex = offset + match.length;
|
||||||
|
return match;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Push remaining text after last URL
|
||||||
|
if (lastIndex < text.length) {
|
||||||
|
parts.push(
|
||||||
|
<React.Fragment key={lastIndex}>{text.slice(lastIndex)}</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InfoRow({
|
||||||
|
icon,
|
||||||
|
text,
|
||||||
|
error = false,
|
||||||
|
data,
|
||||||
|
content,
|
||||||
|
}: InfoRowProps) {
|
||||||
|
return (
|
||||||
|
<Box style={{ display: "flex", alignItems: "center", gap: 1, mb: 1 }}>
|
||||||
|
{icon}
|
||||||
|
{content ? (
|
||||||
|
content
|
||||||
|
) : (
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color={error ? "error" : "textPrimary"}
|
||||||
|
style={{ wordBreak: "break-word" }}
|
||||||
|
>
|
||||||
|
{data ? (
|
||||||
|
<Link
|
||||||
|
href={data}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
underline="always"
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</Link>
|
||||||
|
) : text ? (
|
||||||
|
detectUrls(text)
|
||||||
|
) : null}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import CancelIcon from "@mui/icons-material/Cancel";
|
||||||
|
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
|
||||||
|
import Avatar from "@mui/material/Avatar";
|
||||||
|
import Badge from "@mui/material/Badge";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { userAttendee } from "../../../features/User/userDataTypes";
|
||||||
|
|
||||||
|
export function renderAttendeeBadge(
|
||||||
|
a: userAttendee,
|
||||||
|
key: string,
|
||||||
|
isFull?: boolean,
|
||||||
|
isOrganizer?: boolean
|
||||||
|
) {
|
||||||
|
const classIcon =
|
||||||
|
a.partstat === "ACCEPTED" ? (
|
||||||
|
<CheckCircleIcon fontSize="inherit" color="success" />
|
||||||
|
) : a.partstat === "DECLINED" ? (
|
||||||
|
<CancelIcon fontSize="inherit" color="error" />
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
if (!isFull) {
|
||||||
|
return <Avatar {...stringAvatar(a.cn || a.cal_address)} />;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={key}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 1.5,
|
||||||
|
marginBottom: 0.5,
|
||||||
|
padding: 0.5,
|
||||||
|
borderRadius: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Badge
|
||||||
|
overlap="circular"
|
||||||
|
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||||
|
badgeContent={
|
||||||
|
classIcon && (
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
fontSize: 14,
|
||||||
|
lineHeight: 0,
|
||||||
|
backgroundColor: "white",
|
||||||
|
borderRadius: "50%",
|
||||||
|
padding: "1px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{classIcon}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Avatar {...stringAvatar(a.cn || a.cal_address)} />
|
||||||
|
</Badge>
|
||||||
|
<Box style={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
noWrap
|
||||||
|
style={{
|
||||||
|
maxWidth: "180px",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{a.cn || a.cal_address}
|
||||||
|
</Typography>
|
||||||
|
{isOrganizer && (
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="text.secondary"
|
||||||
|
style={{ fontStyle: "italic" }}
|
||||||
|
>
|
||||||
|
Organizer
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stringToColor(string: string) {
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < string.length; i++) {
|
||||||
|
hash = string.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
let color = "#";
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const value = (hash >> (i * 8)) & 0xff;
|
||||||
|
color += `00${value.toString(16)}`.slice(-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stringAvatar(name: string) {
|
||||||
|
return {
|
||||||
|
style: { backgroundColor: stringToColor(name) },
|
||||||
|
children: name[0],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
|
|||||||
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
||||||
import "./Menubar.styl";
|
import "./Menubar.styl";
|
||||||
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
||||||
import { stringToColor } from "../../features/Events/EventDisplay";
|
import { stringToColor } from "../Event/utils/eventUtils";
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
IconButton,
|
IconButton,
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { useEffect, useState } from "react";
|
import CircleIcon from "@mui/icons-material/Circle";
|
||||||
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
|
||||||
|
import VideocamIcon from "@mui/icons-material/Videocam";
|
||||||
import {
|
import {
|
||||||
deleteEventAsync,
|
deleteEventAsync,
|
||||||
moveEventAsync,
|
moveEventAsync,
|
||||||
@@ -8,42 +12,34 @@ import {
|
|||||||
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
||||||
import AttendeeSelector from "../../components/Attendees/AttendeeSearch";
|
import AttendeeSelector from "../../components/Attendees/AttendeeSearch";
|
||||||
import {
|
import {
|
||||||
Button,
|
|
||||||
Box,
|
Box,
|
||||||
Typography,
|
Button,
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
Card,
|
Card,
|
||||||
|
CardActions,
|
||||||
CardContent,
|
CardContent,
|
||||||
Divider,
|
|
||||||
IconButton,
|
|
||||||
Avatar,
|
|
||||||
Badge,
|
|
||||||
Modal,
|
|
||||||
TextField,
|
|
||||||
CardHeader,
|
CardHeader,
|
||||||
|
Checkbox,
|
||||||
|
Divider,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
FormControlLabel,
|
||||||
|
IconButton,
|
||||||
InputLabel,
|
InputLabel,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
|
Modal,
|
||||||
Select,
|
Select,
|
||||||
SelectChangeEvent,
|
SelectChangeEvent,
|
||||||
Checkbox,
|
TextField,
|
||||||
FormControlLabel,
|
Typography,
|
||||||
CardActions,
|
|
||||||
Link,
|
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
import { useEffect, useState } from "react";
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
import RepeatEvent from "../../components/Event/EventRepeat";
|
||||||
import VideocamIcon from "@mui/icons-material/Videocam";
|
import { InfoRow } from "../../components/Event/InfoRow";
|
||||||
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
|
|
||||||
import CircleIcon from "@mui/icons-material/Circle";
|
|
||||||
import CancelIcon from "@mui/icons-material/Cancel";
|
|
||||||
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
|
|
||||||
import { userAttendee } from "../User/userDataTypes";
|
import { userAttendee } from "../User/userDataTypes";
|
||||||
import { Calendars } from "../Calendars/CalendarTypes";
|
import { Calendars } from "../Calendars/CalendarTypes";
|
||||||
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
|
|
||||||
import { isValidUrl } from "../../utils/apiUtils";
|
|
||||||
import { formatLocalDateTime } from "./EventModal";
|
import { formatLocalDateTime } from "./EventModal";
|
||||||
import RepeatEvent from "../../components/Event/EventRepeat";
|
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
|
||||||
|
import { renderAttendeeBadge } from "../../components/Event/utils/eventUtils";
|
||||||
|
|
||||||
export default function EventDisplayModal({
|
export default function EventDisplayModal({
|
||||||
eventId,
|
eventId,
|
||||||
@@ -406,8 +402,16 @@ export default function EventDisplayModal({
|
|||||||
{event.x_openpass_videoconference && (
|
{event.x_openpass_videoconference && (
|
||||||
<InfoRow
|
<InfoRow
|
||||||
icon={<VideocamIcon style={{ fontSize: 18 }} />}
|
icon={<VideocamIcon style={{ fontSize: 18 }} />}
|
||||||
text="Video conference available"
|
content={
|
||||||
data={event.x_openpass_videoconference}
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={() =>
|
||||||
|
window.open(event.x_openpass_videoconference)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Join the video conference
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -416,13 +420,13 @@ export default function EventDisplayModal({
|
|||||||
<Box style={{ marginBottom: 8 }}>
|
<Box style={{ marginBottom: 8 }}>
|
||||||
<Typography variant="subtitle2">Attendees:</Typography>
|
<Typography variant="subtitle2">Attendees:</Typography>
|
||||||
{organizer.cal_address &&
|
{organizer.cal_address &&
|
||||||
renderAttendeeBadge(organizer, "org", true)}
|
renderAttendeeBadge(organizer, "org", true, true)}
|
||||||
{(showAllAttendees
|
{(showAllAttendees
|
||||||
? attendees
|
? attendees
|
||||||
: attendees.slice(0, attendeeDisplayLimit)
|
: attendees.slice(0, attendeeDisplayLimit)
|
||||||
).map((a, idx) => (
|
).map((a, idx) => (
|
||||||
<Box key={a.cal_address}>
|
<Box key={a.cal_address}>
|
||||||
{renderAttendeeBadge(a, idx.toString())}
|
{renderAttendeeBadge(a, idx.toString(), true)}
|
||||||
{isOwn && (
|
{isOwn && (
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
@@ -568,119 +572,3 @@ export default function EventDisplayModal({
|
|||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InfoRow({
|
|
||||||
icon,
|
|
||||||
text,
|
|
||||||
error = false,
|
|
||||||
data,
|
|
||||||
}: {
|
|
||||||
icon: React.ReactNode;
|
|
||||||
text: string;
|
|
||||||
error?: boolean;
|
|
||||||
data?: string;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 8 }}
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
<Typography variant="body2" color={error ? "error" : "textPrimary"}>
|
|
||||||
{isValidUrl(data) ? <Link href={data}>{text}</Link> : text}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function renderAttendeeBadge(
|
|
||||||
a: userAttendee,
|
|
||||||
key: string,
|
|
||||||
isOrganizer?: boolean
|
|
||||||
) {
|
|
||||||
const classIcon =
|
|
||||||
a.partstat === "ACCEPTED" ? (
|
|
||||||
<CheckCircleIcon fontSize="inherit" color="success" />
|
|
||||||
) : a.partstat === "DECLINED" ? (
|
|
||||||
<CancelIcon fontSize="inherit" color="error" />
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
key={key}
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 12,
|
|
||||||
marginBottom: 4,
|
|
||||||
padding: 4,
|
|
||||||
borderRadius: 4,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Badge
|
|
||||||
overlap="circular"
|
|
||||||
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
|
|
||||||
badgeContent={
|
|
||||||
classIcon && (
|
|
||||||
<Box
|
|
||||||
style={{
|
|
||||||
fontSize: 14,
|
|
||||||
lineHeight: 0,
|
|
||||||
backgroundColor: "white",
|
|
||||||
borderRadius: "50%",
|
|
||||||
padding: "1px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{classIcon}
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Avatar {...stringAvatar(a.cn || a.cal_address)} />
|
|
||||||
</Badge>
|
|
||||||
<Box style={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
noWrap
|
|
||||||
style={{
|
|
||||||
maxWidth: "180px",
|
|
||||||
overflow: "hidden",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{a.cn || a.cal_address}
|
|
||||||
</Typography>
|
|
||||||
{isOrganizer && (
|
|
||||||
<Typography
|
|
||||||
variant="caption"
|
|
||||||
color="text.secondary"
|
|
||||||
style={{ fontStyle: "italic" }}
|
|
||||||
>
|
|
||||||
Organizer
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function stringToColor(string: string) {
|
|
||||||
let hash = 0;
|
|
||||||
for (let i = 0; i < string.length; i++) {
|
|
||||||
hash = string.charCodeAt(i) + ((hash << 5) - hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
let color = "#";
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
const value = (hash >> (i * 8)) & 0xff;
|
|
||||||
color += `00${value.toString(16)}`.slice(-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function stringAvatar(name: string) {
|
|
||||||
return {
|
|
||||||
sx: { width: 24, height: 24, fontSize: 18, bgcolor: stringToColor(name) },
|
|
||||||
children: name[0],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,410 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import {
|
|
||||||
deleteEventAsync,
|
|
||||||
getEventAsync,
|
|
||||||
putEventAsync,
|
|
||||||
} from "../Calendars/CalendarSlice";
|
|
||||||
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
|
||||||
import {
|
|
||||||
Popover,
|
|
||||||
Button,
|
|
||||||
Box,
|
|
||||||
Typography,
|
|
||||||
ButtonGroup,
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
Divider,
|
|
||||||
IconButton,
|
|
||||||
PopoverPosition,
|
|
||||||
} from "@mui/material";
|
|
||||||
import EditIcon from "@mui/icons-material/Edit";
|
|
||||||
import EmailIcon from "@mui/icons-material/Email";
|
|
||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
|
||||||
import VisibilityIcon from "@mui/icons-material/Visibility";
|
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
|
||||||
import CalendarTodayIcon from "@mui/icons-material/CalendarToday";
|
|
||||||
import LocationOnIcon from "@mui/icons-material/LocationOn";
|
|
||||||
import VideocamIcon from "@mui/icons-material/Videocam";
|
|
||||||
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
|
|
||||||
import CircleIcon from "@mui/icons-material/Circle";
|
|
||||||
import FileDownloadOutlinedIcon from "@mui/icons-material/FileDownloadOutlined";
|
|
||||||
import EventDisplayModal, {
|
|
||||||
InfoRow,
|
|
||||||
renderAttendeeBadge,
|
|
||||||
} from "./EventDisplay";
|
|
||||||
import { dlEvent } from "./EventApi";
|
|
||||||
import EventDuplication from "../../components/Event/EventDuplicate";
|
|
||||||
|
|
||||||
export default function EventPreviewModal({
|
|
||||||
eventId,
|
|
||||||
calId,
|
|
||||||
tempEvent,
|
|
||||||
anchorPosition,
|
|
||||||
open,
|
|
||||||
onClose,
|
|
||||||
}: {
|
|
||||||
eventId: string;
|
|
||||||
calId: string;
|
|
||||||
tempEvent?: boolean;
|
|
||||||
anchorPosition: PopoverPosition | null;
|
|
||||||
open: boolean;
|
|
||||||
onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void;
|
|
||||||
}) {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const calendars = useAppSelector((state) => state.calendars);
|
|
||||||
const calendar = tempEvent
|
|
||||||
? calendars.templist[calId]
|
|
||||||
: calendars.list[calId];
|
|
||||||
const event = calendar.events[eventId];
|
|
||||||
const user = useAppSelector((state) => state.user);
|
|
||||||
const [showAllAttendees, setShowAllAttendees] = useState(false);
|
|
||||||
const [openFullDisplay, setOpenFullDisplay] = useState(false);
|
|
||||||
const mailSpaUrl = (window as any).MAIL_SPA_URL ?? null;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!event || !calendar) {
|
|
||||||
onClose({}, "backdropClick");
|
|
||||||
}
|
|
||||||
}, [event, calendar, onClose]);
|
|
||||||
|
|
||||||
if (!event || !calendar) return null;
|
|
||||||
|
|
||||||
const attendeeDisplayLimit = 3;
|
|
||||||
|
|
||||||
const attendees =
|
|
||||||
event.attendee?.filter(
|
|
||||||
(a) => a.cal_address !== event.organizer?.cal_address
|
|
||||||
) || [];
|
|
||||||
|
|
||||||
const visibleAttendees = showAllAttendees
|
|
||||||
? attendees
|
|
||||||
: attendees.slice(0, attendeeDisplayLimit);
|
|
||||||
|
|
||||||
const currentUserAttendee = event.attendee?.find(
|
|
||||||
(person) => person.cal_address === user.userData.email
|
|
||||||
);
|
|
||||||
|
|
||||||
const organizer = event.attendee?.find(
|
|
||||||
(a) => a.cal_address === event.organizer?.cal_address
|
|
||||||
);
|
|
||||||
|
|
||||||
function handleRSVP(rsvp: string) {
|
|
||||||
const newEvent = {
|
|
||||||
...event,
|
|
||||||
attendee: event.attendee?.map((a) =>
|
|
||||||
a.cal_address === user.userData.email ? { ...a, partstat: rsvp } : a
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
dispatch(putEventAsync({ cal: calendar, newEvent }));
|
|
||||||
onClose({}, "backdropClick");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Popover
|
|
||||||
open={open}
|
|
||||||
anchorReference="anchorPosition"
|
|
||||||
anchorPosition={anchorPosition ?? undefined}
|
|
||||||
onClose={onClose}
|
|
||||||
>
|
|
||||||
<Card style={{ width: 300, padding: 16, position: "relative" }}>
|
|
||||||
{/* Top-right buttons */}
|
|
||||||
<Box
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: 8,
|
|
||||||
right: 8,
|
|
||||||
display: "flex",
|
|
||||||
gap: 8,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{(window as any).DEBUG && (
|
|
||||||
<IconButton
|
|
||||||
onClick={async () => {
|
|
||||||
const icsContent = await dlEvent(event);
|
|
||||||
const blob = new Blob([icsContent], {
|
|
||||||
type: "text/calendar",
|
|
||||||
});
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
const link = document.createElement("a");
|
|
||||||
link.href = url;
|
|
||||||
link.download = `${eventId}.ics`;
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FileDownloadOutlinedIcon />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
<EventDuplication event={event} onClose={onClose} />
|
|
||||||
{mailSpaUrl && attendees.length > 0 && (
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
onClick={() =>
|
|
||||||
window.open(
|
|
||||||
`${mailSpaUrl}/mailto/?uri=mailto:${event.attendee
|
|
||||||
.map((a) => a.cal_address)
|
|
||||||
.filter((mail) => mail !== user.userData.email)
|
|
||||||
.join(",")}?subject=${event.title}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<EmailIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
{user.userData.email !== event.organizer?.cal_address && (
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
onClick={() => {
|
|
||||||
setOpenFullDisplay(!openFullDisplay);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<VisibilityIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
{user.userData.email === event.organizer?.cal_address && (
|
|
||||||
<>
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
onClick={async () => {
|
|
||||||
setOpenFullDisplay(!openFullDisplay);
|
|
||||||
await dispatch(getEventAsync(event));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<EditIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
onClick={() => {
|
|
||||||
onClose({}, "backdropClick");
|
|
||||||
dispatch(
|
|
||||||
deleteEventAsync({ calId, eventId, eventURL: event.URL })
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DeleteIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
onClick={() => onClose({}, "backdropClick")}
|
|
||||||
>
|
|
||||||
<CloseIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<CardContent style={{ paddingTop: 12 }}>
|
|
||||||
{event.title && (
|
|
||||||
<Typography
|
|
||||||
variant="h6"
|
|
||||||
fontWeight="bold"
|
|
||||||
style={{
|
|
||||||
wordBreak: "break-word",
|
|
||||||
}}
|
|
||||||
gutterBottom
|
|
||||||
>
|
|
||||||
{event.title}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Time info*/}
|
|
||||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
|
||||||
{formatDate(event.start, event.allday)}
|
|
||||||
{event.end &&
|
|
||||||
formatEnd(event.start, event.end, event.allday) &&
|
|
||||||
` – ${formatEnd(event.start, event.end, event.allday)}`}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{/* Location */}
|
|
||||||
{event.location && (
|
|
||||||
<InfoRow
|
|
||||||
icon={<LocationOnIcon style={{ fontSize: 18 }} />}
|
|
||||||
text={event.location}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Video */}
|
|
||||||
{event.x_openpass_videoconference && (
|
|
||||||
<InfoRow
|
|
||||||
icon={<VideocamIcon style={{ fontSize: 18 }} />}
|
|
||||||
text="Video conference available"
|
|
||||||
data={event.x_openpass_videoconference}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Attendees */}
|
|
||||||
{event.attendee?.length > 0 && (
|
|
||||||
<Box style={{ marginBottom: 8 }}>
|
|
||||||
<Typography variant="subtitle2">Attendees:</Typography>
|
|
||||||
{organizer && renderAttendeeBadge(organizer, "org", true)}
|
|
||||||
{visibleAttendees.map((a, idx) =>
|
|
||||||
renderAttendeeBadge(a, idx.toString())
|
|
||||||
)}
|
|
||||||
{attendees.length > attendeeDisplayLimit && (
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
color="primary"
|
|
||||||
style={{ cursor: "pointer", marginTop: 4 }}
|
|
||||||
onClick={() => setShowAllAttendees(!showAllAttendees)}
|
|
||||||
>
|
|
||||||
{showAllAttendees
|
|
||||||
? "Show less"
|
|
||||||
: `Show more (${
|
|
||||||
attendees.length - attendeeDisplayLimit
|
|
||||||
} more)`}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Error */}
|
|
||||||
{event.error && (
|
|
||||||
<InfoRow
|
|
||||||
icon={
|
|
||||||
<ErrorOutlineIcon color="error" style={{ fontSize: 18 }} />
|
|
||||||
}
|
|
||||||
text={event.error}
|
|
||||||
error
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Calendar color dot */}
|
|
||||||
<Box
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 8,
|
|
||||||
marginBottom: 16,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CalendarTodayIcon style={{ fontSize: 16 }} />
|
|
||||||
<CircleIcon
|
|
||||||
style={{
|
|
||||||
color: calendar.color ?? "#3788D8",
|
|
||||||
width: 12,
|
|
||||||
height: 12,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Typography variant="body2">{calendar.name}</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Divider style={{ marginBottom: 8 }} />
|
|
||||||
|
|
||||||
{/* RSVP */}
|
|
||||||
{currentUserAttendee && (
|
|
||||||
<Box>
|
|
||||||
<Typography variant="body2" style={{ marginBottom: 8 }}>
|
|
||||||
Will you attend?
|
|
||||||
</Typography>
|
|
||||||
<ButtonGroup size="small" fullWidth>
|
|
||||||
<Button
|
|
||||||
color={
|
|
||||||
currentUserAttendee.partstat === "ACCEPTED"
|
|
||||||
? "success"
|
|
||||||
: "primary"
|
|
||||||
}
|
|
||||||
onClick={() => handleRSVP("ACCEPTED")}
|
|
||||||
>
|
|
||||||
Accept
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color={
|
|
||||||
currentUserAttendee.partstat === "TENTATIVE"
|
|
||||||
? "warning"
|
|
||||||
: "primary"
|
|
||||||
}
|
|
||||||
onClick={() => handleRSVP("TENTATIVE")}
|
|
||||||
>
|
|
||||||
Maybe
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color={
|
|
||||||
currentUserAttendee.partstat === "DECLINED"
|
|
||||||
? "error"
|
|
||||||
: "primary"
|
|
||||||
}
|
|
||||||
onClick={() => handleRSVP("DECLINED")}
|
|
||||||
>
|
|
||||||
Decline
|
|
||||||
</Button>
|
|
||||||
</ButtonGroup>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Description */}
|
|
||||||
{event.description && (
|
|
||||||
<Typography variant="body2" style={{ marginTop: 8 }}>
|
|
||||||
{event.description}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Popover>
|
|
||||||
<EventDisplayModal
|
|
||||||
open={openFullDisplay}
|
|
||||||
onClose={() => setOpenFullDisplay(false)}
|
|
||||||
eventId={eventId}
|
|
||||||
calId={calId}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDate(date: Date, allday?: boolean) {
|
|
||||||
if (allday) {
|
|
||||||
return new Date(date).toLocaleDateString(undefined, {
|
|
||||||
year: "numeric",
|
|
||||||
month: "long",
|
|
||||||
weekday: "long",
|
|
||||||
day: "numeric",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return new Date(date).toLocaleString(undefined, {
|
|
||||||
year: "numeric",
|
|
||||||
month: "long",
|
|
||||||
weekday: "long",
|
|
||||||
day: "numeric",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatEnd(start: Date, end: Date, allday?: boolean) {
|
|
||||||
const startDate = new Date(start);
|
|
||||||
const endDate = new Date(end);
|
|
||||||
|
|
||||||
const sameDay =
|
|
||||||
startDate.getFullYear() === endDate.getFullYear() &&
|
|
||||||
startDate.getMonth() === endDate.getMonth() &&
|
|
||||||
startDate.getDate() === endDate.getDate();
|
|
||||||
|
|
||||||
if (allday) {
|
|
||||||
return sameDay
|
|
||||||
? null
|
|
||||||
: endDate.toLocaleDateString(undefined, {
|
|
||||||
year: "numeric",
|
|
||||||
month: "short",
|
|
||||||
day: "numeric",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if (sameDay) {
|
|
||||||
return endDate.toLocaleTimeString(undefined, {
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return endDate.toLocaleString(undefined, {
|
|
||||||
year: "numeric",
|
|
||||||
month: "short",
|
|
||||||
day: "numeric",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user