[#77] added complex ui for repetition, fixed broken tests due to new feature
This commit is contained in:
@@ -2,7 +2,7 @@ import { CalendarApi } from "@fullcalendar/core";
|
|||||||
import { jest } from "@jest/globals";
|
import { jest } from "@jest/globals";
|
||||||
import { ThunkDispatch } from "@reduxjs/toolkit";
|
import { ThunkDispatch } from "@reduxjs/toolkit";
|
||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
import { screen } from "@testing-library/react";
|
import { act, screen } from "@testing-library/react";
|
||||||
import * as appHooks from "../../src/app/hooks";
|
import * as appHooks from "../../src/app/hooks";
|
||||||
import CalendarApp from "../../src/components/Calendar/Calendar";
|
import CalendarApp from "../../src/components/Calendar/Calendar";
|
||||||
import { renderWithProviders } from "../utils/Renderwithproviders";
|
import { renderWithProviders } from "../utils/Renderwithproviders";
|
||||||
@@ -88,16 +88,17 @@ describe("CalendarApp integration", () => {
|
|||||||
{ timeout: 3000 }
|
{ timeout: 3000 }
|
||||||
);
|
);
|
||||||
expect(eventEl).toBeInTheDocument();
|
expect(eventEl).toBeInTheDocument();
|
||||||
|
act(() => {
|
||||||
|
if (calendarApi) {
|
||||||
|
const fcEvent = calendarApi.getEventById("event1");
|
||||||
|
expect(fcEvent?.title).toBe("Test Event");
|
||||||
|
const oldEnd = new Date(today.getTime() + 3600000); // +1 hour
|
||||||
|
const newEnd = new Date(oldEnd.getTime() + 1800000); // +30 min
|
||||||
|
|
||||||
if (calendarApi) {
|
fcEvent?.setEnd(newEnd);
|
||||||
const fcEvent = calendarApi.getEventById("event1");
|
|
||||||
expect(fcEvent?.title).toBe("Test Event");
|
|
||||||
const oldEnd = new Date(today.getTime() + 3600000); // +1 hour
|
|
||||||
const newEnd = new Date(oldEnd.getTime() + 1800000); // +30 min
|
|
||||||
|
|
||||||
fcEvent?.setEnd(newEnd);
|
expect(dispatch).toHaveBeenCalled();
|
||||||
|
}
|
||||||
expect(dispatch).toHaveBeenCalled();
|
});
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { screen, fireEvent, waitFor } from "@testing-library/react";
|
import { screen, fireEvent, waitFor, act } 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, {
|
||||||
@@ -7,6 +7,7 @@ import EventDisplayModal, {
|
|||||||
stringToColor,
|
stringToColor,
|
||||||
} from "../../../src/features/Events/EventDisplay";
|
} from "../../../src/features/Events/EventDisplay";
|
||||||
import EventPreviewModal from "../../../src/features/Events/EventDisplayPreview";
|
import EventPreviewModal from "../../../src/features/Events/EventDisplayPreview";
|
||||||
|
|
||||||
describe("Event Preview Display", () => {
|
describe("Event Preview Display", () => {
|
||||||
const mockOnClose = jest.fn();
|
const mockOnClose = jest.fn();
|
||||||
const day = new Date();
|
const day = new Date();
|
||||||
@@ -915,7 +916,19 @@ describe("Event Full Display", () => {
|
|||||||
const updatedEvent = spy.mock.calls[0][0].newEvent;
|
const updatedEvent = spy.mock.calls[0][0].newEvent;
|
||||||
expect(updatedEvent.attendee[0].partstat).toBe("DECLINED");
|
expect(updatedEvent.attendee[0].partstat).toBe("DECLINED");
|
||||||
});
|
});
|
||||||
test("toggle Show More reveals extra fields", () => {
|
it("toggle Show More reveals extra fields", async () => {
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(eventThunks, "getEventAsync")
|
||||||
|
.mockImplementation((payload) => {
|
||||||
|
return () =>
|
||||||
|
Promise.resolve({
|
||||||
|
calId: payload.calId,
|
||||||
|
event:
|
||||||
|
preloadedState.calendars.list["667037022b752d0026472254/cal1"]
|
||||||
|
.events["event1"],
|
||||||
|
}) as any;
|
||||||
|
});
|
||||||
|
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventDisplayModal
|
<EventDisplayModal
|
||||||
open={true}
|
open={true}
|
||||||
@@ -925,14 +938,23 @@ describe("Event Full Display", () => {
|
|||||||
/>,
|
/>,
|
||||||
preloadedState
|
preloadedState
|
||||||
);
|
);
|
||||||
fireEvent.click(screen.getByText("Show More"));
|
act(() => {
|
||||||
expect(screen.getByLabelText("Alarm")).toBeInTheDocument();
|
fireEvent.click(screen.getByText("Show More"));
|
||||||
expect(screen.getByLabelText("Repetition")).toBeInTheDocument();
|
});
|
||||||
expect(screen.getByLabelText("Visibility")).toBeInTheDocument();
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(spy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
console.log(spy);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByLabelText(/Alarm/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByLabelText(/Repetition/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByLabelText(/Visibility/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
fireEvent.click(screen.getByText("Show Less"));
|
fireEvent.click(screen.getByText("Show Less"));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can edit title when user is organizer", () => {
|
it("can edit title when user is organizer", () => {
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventDisplayModal
|
<EventDisplayModal
|
||||||
open={true}
|
open={true}
|
||||||
@@ -946,7 +968,7 @@ describe("Event Full Display", () => {
|
|||||||
fireEvent.change(titleField, { target: { value: "New Title" } });
|
fireEvent.change(titleField, { target: { value: "New Title" } });
|
||||||
expect(screen.getByDisplayValue("New Title")).toBeInTheDocument();
|
expect(screen.getByDisplayValue("New Title")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
test("calendar select is disabled when not organizer", () => {
|
it("calendar select is disabled when not organizer", () => {
|
||||||
const rsvpState = {
|
const rsvpState = {
|
||||||
...preloadedState,
|
...preloadedState,
|
||||||
calendars: {
|
calendars: {
|
||||||
@@ -988,7 +1010,7 @@ describe("Event Full Display", () => {
|
|||||||
);
|
);
|
||||||
expect(screen.getByLabelText("Calendar")).toHaveClass("Mui-disabled");
|
expect(screen.getByLabelText("Calendar")).toHaveClass("Mui-disabled");
|
||||||
});
|
});
|
||||||
test("toggle all-day updates end date correctly", () => {
|
it("toggle all-day updates end date correctly", () => {
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventDisplayModal
|
<EventDisplayModal
|
||||||
open={true}
|
open={true}
|
||||||
@@ -1010,18 +1032,18 @@ describe("Event Full Display", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("Helper functions", () => {
|
describe("Helper functions", () => {
|
||||||
test("stringToColor generates consistent color", () => {
|
it("stringToColor generates consistent color", () => {
|
||||||
expect(stringToColor("Alice")).toMatch(/^#[0-9a-f]{6}$/);
|
expect(stringToColor("Alice")).toMatch(/^#[0-9a-f]{6}$/);
|
||||||
expect(stringToColor("Alice")).toBe(stringToColor("Alice"));
|
expect(stringToColor("Alice")).toBe(stringToColor("Alice"));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("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.sx.bgcolor).toMatch(/^#/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("InfoRow renders text and link if url is valid", () => {
|
it("InfoRow renders text and link if url is valid", () => {
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<InfoRow
|
<InfoRow
|
||||||
icon={<span>ico</span>}
|
icon={<span>ico</span>}
|
||||||
|
|||||||
@@ -247,7 +247,6 @@ describe("EventPopover", () => {
|
|||||||
uid: "6045c603-11ab-43c5-bc30-0641420bb3a8",
|
uid: "6045c603-11ab-43c5-bc30-0641420bb3a8",
|
||||||
description: "Discuss project",
|
description: "Discuss project",
|
||||||
location: "Zoom",
|
location: "Zoom",
|
||||||
repetition: "",
|
|
||||||
organizer: { cn: "test", cal_address: "test@test.com" },
|
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||||
timezone: "Europe/Paris",
|
timezone: "Europe/Paris",
|
||||||
transp: "OPAQUE",
|
transp: "OPAQUE",
|
||||||
@@ -302,7 +301,6 @@ describe("EventPopover", () => {
|
|||||||
).toBe(formatDateToYYYYMMDDTHHMMSS(new Date(newEvent.end)).split("T")[0]);
|
).toBe(formatDateToYYYYMMDDTHHMMSS(new Date(newEvent.end)).split("T")[0]);
|
||||||
expect(receivedPayload.newEvent.location).toBe(newEvent.location);
|
expect(receivedPayload.newEvent.location).toBe(newEvent.location);
|
||||||
expect(receivedPayload.newEvent.organizer).toEqual(newEvent.organizer);
|
expect(receivedPayload.newEvent.organizer).toEqual(newEvent.organizer);
|
||||||
expect(receivedPayload.newEvent.repetition).toEqual(newEvent.repetition);
|
|
||||||
expect(receivedPayload.newEvent.color).toEqual(
|
expect(receivedPayload.newEvent.color).toEqual(
|
||||||
preloadedState.calendars.list["667037022b752d0026472254/cal1"].color
|
preloadedState.calendars.list["667037022b752d0026472254/cal1"].color
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ describe("calendarEventToJCal", () => {
|
|||||||
allday: false,
|
allday: false,
|
||||||
location: "Room 101",
|
location: "Room 101",
|
||||||
description: "Discuss project roadmap.",
|
description: "Discuss project roadmap.",
|
||||||
repetition: "WEEKLY",
|
repetition: { freq: "WEEKLY" },
|
||||||
organizer: {
|
organizer: {
|
||||||
cn: "Alice",
|
cn: "Alice",
|
||||||
cal_address: "alice@example.com",
|
cal_address: "alice@example.com",
|
||||||
@@ -280,7 +280,7 @@ describe("calendarEventToJCal", () => {
|
|||||||
allday: true,
|
allday: true,
|
||||||
location: "Room 101",
|
location: "Room 101",
|
||||||
description: "Discuss project roadmap.",
|
description: "Discuss project roadmap.",
|
||||||
repetition: "WEEKLY",
|
repetition: { freq: "WEEKLY" },
|
||||||
organizer: {
|
organizer: {
|
||||||
cn: "Alice",
|
cn: "Alice",
|
||||||
cal_address: "alice@example.com",
|
cal_address: "alice@example.com",
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
Select,
|
||||||
|
SelectChangeEvent,
|
||||||
|
MenuItem,
|
||||||
|
Box,
|
||||||
|
Stack,
|
||||||
|
Paper,
|
||||||
|
Typography,
|
||||||
|
TextField,
|
||||||
|
Checkbox,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
FormControlLabel,
|
||||||
|
FormGroup,
|
||||||
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { RepetitionObject } from "../../features/Events/EventsTypes";
|
||||||
|
|
||||||
|
export default function RepeatEvent({
|
||||||
|
repetition,
|
||||||
|
setRepetition,
|
||||||
|
isOwn = true,
|
||||||
|
}: {
|
||||||
|
repetition: RepetitionObject;
|
||||||
|
setRepetition: Function;
|
||||||
|
isOwn?: boolean;
|
||||||
|
}) {
|
||||||
|
console.log(JSON.stringify(repetition));
|
||||||
|
|
||||||
|
const repetitionValues = ["day", "week", "month", "year"];
|
||||||
|
const [interval, setInterval] = useState(repetition.interval ?? 0);
|
||||||
|
const [selectedDays, setSelectedDays] = useState<string[]>(
|
||||||
|
repetition.selectedDays ?? []
|
||||||
|
);
|
||||||
|
const [endOption, setEndOption] = useState("");
|
||||||
|
const [occurrences, setOccurrences] = useState(repetition.occurrences) ?? 0;
|
||||||
|
const [endDate, setEndDate] = useState(repetition.endDate ?? "");
|
||||||
|
const days = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"];
|
||||||
|
|
||||||
|
const handleDayChange = (day: string) => {
|
||||||
|
setSelectedDays((prev: string[]) =>
|
||||||
|
prev.includes(day) ? prev.filter((d) => d !== day) : [...prev, day]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<FormControl fullWidth margin="dense" size="small">
|
||||||
|
<InputLabel id="repeat">Repetition</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="repeat"
|
||||||
|
value={repetition.freq ?? ""}
|
||||||
|
disabled={!isOwn}
|
||||||
|
label="Repetition"
|
||||||
|
onChange={(e: SelectChangeEvent) =>
|
||||||
|
setRepetition({ ...repetition, freq: e.target.value })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MenuItem value={""}>No Repetition</MenuItem>
|
||||||
|
<MenuItem value={"daily"}>Repeat daily</MenuItem>
|
||||||
|
<MenuItem value={"weekly"}>Repeat weekly</MenuItem>
|
||||||
|
<MenuItem value={"monthly"}>Repeat monthly</MenuItem>
|
||||||
|
<MenuItem value={"yearly"}>Repeat yearly</MenuItem>
|
||||||
|
</Select>
|
||||||
|
{repetition.freq && (
|
||||||
|
<Stack>
|
||||||
|
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
||||||
|
<Typography>Interval:</Typography>
|
||||||
|
<TextField
|
||||||
|
type="number"
|
||||||
|
value={interval}
|
||||||
|
onChange={(e) => setInterval(Number(e.target.value))}
|
||||||
|
size="small"
|
||||||
|
sx={{ width: 80 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Typography>
|
||||||
|
{
|
||||||
|
repetitionValues[
|
||||||
|
repetitionValues.findIndex((el) => el === repetition.freq)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
{repetition.freq === "weekly" && (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="body2" gutterBottom>
|
||||||
|
On days:
|
||||||
|
</Typography>
|
||||||
|
<FormGroup row>
|
||||||
|
{days.map((day) => (
|
||||||
|
<FormControlLabel
|
||||||
|
key={day}
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedDays.includes(day)}
|
||||||
|
onChange={() => handleDayChange(day)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={day}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</FormGroup>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Box>
|
||||||
|
<Typography variant="body2" gutterBottom sx={{ mt: 2 }}>
|
||||||
|
End:
|
||||||
|
</Typography>
|
||||||
|
<RadioGroup
|
||||||
|
value={endOption}
|
||||||
|
onChange={(e) => setEndOption(e.target.value)}
|
||||||
|
>
|
||||||
|
<FormControlLabel
|
||||||
|
value="never"
|
||||||
|
control={<Radio />}
|
||||||
|
label="Never"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormControlLabel
|
||||||
|
value="after"
|
||||||
|
control={<Radio />}
|
||||||
|
label={
|
||||||
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
|
After
|
||||||
|
<TextField
|
||||||
|
type="number"
|
||||||
|
size="small"
|
||||||
|
value={occurrences}
|
||||||
|
onChange={(e) => setOccurrences(Number(e.target.value))}
|
||||||
|
sx={{ width: 100 }}
|
||||||
|
inputProps={{ min: 1 }}
|
||||||
|
disabled={endOption !== "after"}
|
||||||
|
/>
|
||||||
|
occurrences
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormControlLabel
|
||||||
|
value="on"
|
||||||
|
control={<Radio />}
|
||||||
|
label={
|
||||||
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
|
On
|
||||||
|
<TextField
|
||||||
|
type="date"
|
||||||
|
size="small"
|
||||||
|
value={endDate}
|
||||||
|
onChange={(e) => setEndDate(e.target.value)}
|
||||||
|
disabled={endOption !== "on"}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</RadioGroup>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -74,7 +74,7 @@ export function Menubar() {
|
|||||||
>
|
>
|
||||||
<div className="app-grid">
|
<div className="app-grid">
|
||||||
{applist.map((prop: AppIconProps) => (
|
{applist.map((prop: AppIconProps) => (
|
||||||
<AppIcon prop={prop} />
|
<AppIcon key={prop.name} prop={prop} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Popover>
|
</Popover>
|
||||||
@@ -96,7 +96,6 @@ export function MainTitle() {
|
|||||||
function AppIcon({ prop }: { prop: AppIconProps }) {
|
function AppIcon({ prop }: { prop: AppIconProps }) {
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
key={prop.name}
|
|
||||||
href={prop.link}
|
href={prop.link}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
style={{ textDecoration: "none", color: "inherit" }}
|
style={{ textDecoration: "none", color: "inherit" }}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { CalendarEvent } from "../Events/EventsTypes";
|
|||||||
import { getCalendar, getCalendars } from "./CalendarApi";
|
import { getCalendar, getCalendars } from "./CalendarApi";
|
||||||
import { getOpenPaasUser, getUserDetails } from "../User/userAPI";
|
import { getOpenPaasUser, getUserDetails } from "../User/userAPI";
|
||||||
import { parseCalendarEvent } from "../Events/eventUtils";
|
import { parseCalendarEvent } from "../Events/eventUtils";
|
||||||
import { deleteEvent, moveEvent, putEvent } from "../Events/EventApi";
|
import { deleteEvent, getEvent, moveEvent, putEvent } from "../Events/EventApi";
|
||||||
import { formatDateToYYYYMMDDTHHMMSS } from "../../utils/dateUtils";
|
import { formatDateToYYYYMMDDTHHMMSS } from "../../utils/dateUtils";
|
||||||
|
|
||||||
export const getCalendarsListAsync = createAsyncThunk<
|
export const getCalendarsListAsync = createAsyncThunk<
|
||||||
@@ -92,6 +92,18 @@ export const putEventAsync = createAsyncThunk<
|
|||||||
events,
|
events,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getEventAsync = createAsyncThunk<
|
||||||
|
{ calId: string; event: CalendarEvent }, // Return type
|
||||||
|
CalendarEvent // Arg type
|
||||||
|
>("calendars/getEvent", async (event) => {
|
||||||
|
const response: CalendarEvent = await getEvent(event);
|
||||||
|
return {
|
||||||
|
calId: event.calId,
|
||||||
|
event: response,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
export const moveEventAsync = createAsyncThunk<
|
export const moveEventAsync = createAsyncThunk<
|
||||||
{ calId: string; events: CalendarEvent[] }, // Return type
|
{ calId: string; events: CalendarEvent[] }, // Return type
|
||||||
{ cal: Calendars; newEvent: CalendarEvent; newURL: string } // Arg type
|
{ cal: Calendars; newEvent: CalendarEvent; newURL: string } // Arg type
|
||||||
@@ -233,6 +245,24 @@ const CalendarSlice = createSlice({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
.addCase(
|
||||||
|
getEventAsync.fulfilled,
|
||||||
|
(
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{ calId: string; event: CalendarEvent }>
|
||||||
|
) => {
|
||||||
|
state.pending = false;
|
||||||
|
if (!state.list[action.payload.calId]) {
|
||||||
|
state.list[action.payload.calId] = {
|
||||||
|
id: action.payload.calId,
|
||||||
|
events: {},
|
||||||
|
} as Calendars;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.list[action.payload.calId].events[action.payload.event.uid] =
|
||||||
|
action.payload.event;
|
||||||
|
}
|
||||||
|
)
|
||||||
.addCase(
|
.addCase(
|
||||||
moveEventAsync.fulfilled,
|
moveEventAsync.fulfilled,
|
||||||
(
|
(
|
||||||
@@ -266,6 +296,9 @@ const CalendarSlice = createSlice({
|
|||||||
.addCase(getCalendarDetailAsync.pending, (state) => {
|
.addCase(getCalendarDetailAsync.pending, (state) => {
|
||||||
state.pending = true;
|
state.pending = true;
|
||||||
})
|
})
|
||||||
|
.addCase(getEventAsync.pending, (state) => {
|
||||||
|
state.pending = true;
|
||||||
|
})
|
||||||
.addCase(getCalendarsListAsync.pending, (state) => {
|
.addCase(getCalendarsListAsync.pending, (state) => {
|
||||||
state.pending = true;
|
state.pending = true;
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,20 @@
|
|||||||
import { api } from "../../utils/apiUtils";
|
import { api } from "../../utils/apiUtils";
|
||||||
import { CalendarEvent } from "./EventsTypes";
|
import { CalendarEvent } from "./EventsTypes";
|
||||||
import { calendarEventToJCal } from "./eventUtils";
|
import { calendarEventToJCal, parseCalendarEvent } from "./eventUtils";
|
||||||
|
import ICAL from "ical.js";
|
||||||
|
|
||||||
|
export async function getEvent(event: CalendarEvent) {
|
||||||
|
const response = await api.get(`dav${event.URL}`);
|
||||||
|
const eventData = await response.text();
|
||||||
|
const eventical = ICAL.parse(eventData);
|
||||||
|
const eventjson = parseCalendarEvent(
|
||||||
|
eventical[2][1][1],
|
||||||
|
event.color ?? "",
|
||||||
|
event.calId,
|
||||||
|
event.URL
|
||||||
|
);
|
||||||
|
return { ...eventjson, ...event };
|
||||||
|
}
|
||||||
|
|
||||||
export async function putEvent(event: CalendarEvent) {
|
export async function putEvent(event: CalendarEvent) {
|
||||||
const response = await api(`dav${event.URL}`, {
|
const response = await api(`dav${event.URL}`, {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
deleteEventAsync,
|
deleteEventAsync,
|
||||||
|
getEventAsync,
|
||||||
moveEventAsync,
|
moveEventAsync,
|
||||||
putEventAsync,
|
putEventAsync,
|
||||||
removeEvent,
|
removeEvent,
|
||||||
@@ -42,10 +43,10 @@ import CheckCircleIcon from "@mui/icons-material/CheckCircle";
|
|||||||
import { userAttendee } from "../User/userDataTypes";
|
import { userAttendee } from "../User/userDataTypes";
|
||||||
import { TIMEZONES } from "../../utils/timezone-data";
|
import { TIMEZONES } from "../../utils/timezone-data";
|
||||||
import { Calendars } from "../Calendars/CalendarTypes";
|
import { Calendars } from "../Calendars/CalendarTypes";
|
||||||
import { CalendarEvent } from "./EventsTypes";
|
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
|
||||||
import { isValidUrl } from "../../utils/apiUtils";
|
import { isValidUrl } from "../../utils/apiUtils";
|
||||||
import { formatLocalDateTime } from "./EventModal";
|
import { formatLocalDateTime } from "./EventModal";
|
||||||
import RepeatEvent from "./EventRepeat";
|
import RepeatEvent from "../../components/Event/EventRepeat";
|
||||||
|
|
||||||
export default function EventDisplayModal({
|
export default function EventDisplayModal({
|
||||||
eventId,
|
eventId,
|
||||||
@@ -68,8 +69,8 @@ export default function EventDisplayModal({
|
|||||||
const [showAllAttendees, setShowAllAttendees] = useState(false);
|
const [showAllAttendees, setShowAllAttendees] = useState(false);
|
||||||
const [showMore, setShowMore] = useState(false);
|
const [showMore, setShowMore] = useState(false);
|
||||||
|
|
||||||
const calendars = useAppSelector((state) =>
|
const calendars = Object.values(
|
||||||
Object.values(state.calendars.list)
|
useAppSelector((state) => state.calendars.list)
|
||||||
);
|
);
|
||||||
|
|
||||||
const userPersonnalCalendars: Calendars[] = calendars.filter(
|
const userPersonnalCalendars: Calendars[] = calendars.filter(
|
||||||
@@ -87,13 +88,15 @@ export default function EventDisplayModal({
|
|||||||
formatLocalDateTime(new Date(event?.end ?? Date.now()))
|
formatLocalDateTime(new Date(event?.end ?? Date.now()))
|
||||||
);
|
);
|
||||||
const [allday, setAllDay] = useState(event?.allday);
|
const [allday, setAllDay] = useState(event?.allday);
|
||||||
const [repetition, setRepetition] = useState(event?.repetition ?? "");
|
const [repetition, setRepetition] = useState<RepetitionObject>(
|
||||||
|
event.repetition ?? ({} as RepetitionObject)
|
||||||
|
);
|
||||||
const [alarm, setAlarm] = useState("");
|
const [alarm, setAlarm] = useState("");
|
||||||
const [eventClass, setEventClass] = useState(event?.class ?? "PUBLIC");
|
const [eventClass, setEventClass] = useState(event?.class ?? "PUBLIC");
|
||||||
const [timezone, setTimezone] = useState(event?.timezone ?? "UTC");
|
const [timezone, setTimezone] = useState(event?.timezone ?? "UTC");
|
||||||
const [newCalId, setNewCalId] = useState(event.calId);
|
const [newCalId, setNewCalId] = useState(event.calId);
|
||||||
const [calendarid, setCalendarid] = useState(
|
const [calendarid, setCalendarid] = useState(
|
||||||
event?.calId.split("/")[0] === user.userData.openpaasId
|
calId.split("/")[0] === user.userData.openpaasId
|
||||||
? userPersonnalCalendars.findIndex((cal) => cal.id === calId)
|
? userPersonnalCalendars.findIndex((cal) => cal.id === calId)
|
||||||
: calendars.findIndex((cal) => cal.id === calId)
|
: calendars.findIndex((cal) => cal.id === calId)
|
||||||
);
|
);
|
||||||
@@ -103,7 +106,6 @@ export default function EventDisplayModal({
|
|||||||
(a) => a.cal_address !== event?.organizer?.cal_address
|
(a) => a.cal_address !== event?.organizer?.cal_address
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const currentUserAttendee = event?.attendee?.find(
|
const currentUserAttendee = event?.attendee?.find(
|
||||||
(person) => person.cal_address === user.userData.email
|
(person) => person.cal_address === user.userData.email
|
||||||
);
|
);
|
||||||
@@ -120,7 +122,7 @@ export default function EventDisplayModal({
|
|||||||
if (!event || !calendar) {
|
if (!event || !calendar) {
|
||||||
onClose({}, "backdropClick");
|
onClose({}, "backdropClick");
|
||||||
}
|
}
|
||||||
}, [event, calendar, onClose]);
|
}, [open, eventId, dispatch, onClose]);
|
||||||
|
|
||||||
if (!event || !calendar) return null;
|
if (!event || !calendar) return null;
|
||||||
|
|
||||||
@@ -187,8 +189,18 @@ export default function EventDisplayModal({
|
|||||||
onClose({}, "backdropClick");
|
onClose({}, "backdropClick");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [detailsLoaded, setDetailsLoaded] = useState(false);
|
||||||
|
|
||||||
|
const handleToggleShowMore = async () => {
|
||||||
|
if (!detailsLoaded) {
|
||||||
|
await dispatch(getEventAsync(event));
|
||||||
|
setDetailsLoaded(true);
|
||||||
|
}
|
||||||
|
setShowMore(!showMore);
|
||||||
|
};
|
||||||
|
|
||||||
const calList =
|
const calList =
|
||||||
event.calId.split("/")[0] === user.userData.openpaasId
|
calId.split("/")[0] === user.userData.openpaasId
|
||||||
? Object.keys(userPersonnalCalendars).map((calendar, index) => (
|
? Object.keys(userPersonnalCalendars).map((calendar, index) => (
|
||||||
<MenuItem key={index} value={index}>
|
<MenuItem key={index} value={index}>
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
@@ -230,7 +242,7 @@ export default function EventDisplayModal({
|
|||||||
|
|
||||||
<CardHeader title={isOwn ? "Edit Event" : "Event Details"} />
|
<CardHeader title={isOwn ? "Edit Event" : "Event Details"} />
|
||||||
|
|
||||||
<CardContent sx={{ maxHeight: "85vh", overflow: "auto" }}>
|
<CardContent sx={{ overflow: "auto" }}>
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -403,7 +415,7 @@ export default function EventDisplayModal({
|
|||||||
? attendees
|
? attendees
|
||||||
: attendees.slice(0, attendeeDisplayLimit)
|
: attendees.slice(0, attendeeDisplayLimit)
|
||||||
).map((a, idx) => (
|
).map((a, idx) => (
|
||||||
<Box>
|
<Box key={a.cal_address}>
|
||||||
{renderAttendeeBadge(a, idx.toString())}
|
{renderAttendeeBadge(a, idx.toString())}
|
||||||
{isOwn && (
|
{isOwn && (
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -491,7 +503,7 @@ export default function EventDisplayModal({
|
|||||||
<InputLabel id="busy">is Busy</InputLabel>
|
<InputLabel id="busy">is Busy</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
labelId="busy"
|
labelId="busy"
|
||||||
value={eventClass}
|
value={""}
|
||||||
disabled={!isOwn}
|
disabled={!isOwn}
|
||||||
label="is busy"
|
label="is busy"
|
||||||
onChange={(e: SelectChangeEvent) =>
|
onChange={(e: SelectChangeEvent) =>
|
||||||
@@ -531,7 +543,7 @@ export default function EventDisplayModal({
|
|||||||
<DeleteIcon fontSize="small" />
|
<DeleteIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
<Button size="small" onClick={() => setShowMore(!showMore)}>
|
<Button size="small" onClick={handleToggleShowMore}>
|
||||||
{showMore ? "Show Less" : "Show More"}
|
{showMore ? "Show Less" : "Show More"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { deleteEventAsync, putEventAsync } from "../Calendars/CalendarSlice";
|
import {
|
||||||
|
deleteEventAsync,
|
||||||
|
getEventAsync,
|
||||||
|
putEventAsync,
|
||||||
|
} from "../Calendars/CalendarSlice";
|
||||||
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
@@ -32,6 +36,8 @@ import EventDisplayModal, {
|
|||||||
renderAttendeeBadge,
|
renderAttendeeBadge,
|
||||||
stringAvatar,
|
stringAvatar,
|
||||||
} from "./EventDisplay";
|
} from "./EventDisplay";
|
||||||
|
import { getEvent } from "./EventApi";
|
||||||
|
import { CalendarEvent } from "./EventsTypes";
|
||||||
|
|
||||||
export default function EventPreviewModal({
|
export default function EventPreviewModal({
|
||||||
eventId,
|
eventId,
|
||||||
@@ -47,13 +53,15 @@ export default function EventPreviewModal({
|
|||||||
onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void;
|
onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void;
|
||||||
}) {
|
}) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const calendar = useAppSelector((state) => state.calendars.list[calId]);
|
const calendars = useAppSelector((state) => state.calendars);
|
||||||
|
const calendar = calendars.list[calId];
|
||||||
const event = useAppSelector(
|
const event = useAppSelector(
|
||||||
(state) => state.calendars.list[calId]?.events[eventId]
|
(state) => state.calendars.list[calId]?.events[eventId]
|
||||||
);
|
);
|
||||||
const user = useAppSelector((state) => state.user);
|
const user = useAppSelector((state) => state.user);
|
||||||
const [showAllAttendees, setShowAllAttendees] = useState(false);
|
const [showAllAttendees, setShowAllAttendees] = useState(false);
|
||||||
const [openFullDisplay, setOpenFullDisplay] = useState(false);
|
const [openFullDisplay, setOpenFullDisplay] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!event || !calendar) {
|
if (!event || !calendar) {
|
||||||
onClose({}, "backdropClick");
|
onClose({}, "backdropClick");
|
||||||
@@ -126,7 +134,7 @@ export default function EventPreviewModal({
|
|||||||
<>
|
<>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
setOpenFullDisplay(!openFullDisplay);
|
setOpenFullDisplay(!openFullDisplay);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ import { TIMEZONES } from "../../utils/timezone-data";
|
|||||||
import { putEventAsync } from "../Calendars/CalendarSlice";
|
import { putEventAsync } from "../Calendars/CalendarSlice";
|
||||||
import { Calendars } from "../Calendars/CalendarTypes";
|
import { Calendars } from "../Calendars/CalendarTypes";
|
||||||
import { userAttendee } from "../User/userDataTypes";
|
import { userAttendee } from "../User/userDataTypes";
|
||||||
import { CalendarEvent } from "./EventsTypes";
|
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
|
||||||
import { createSelector } from "@reduxjs/toolkit";
|
import { createSelector } from "@reduxjs/toolkit";
|
||||||
import RepeatEvent from "./EventRepeat";
|
import RepeatEvent from "../../components/Event/EventRepeat";
|
||||||
|
|
||||||
function EventPopover({
|
function EventPopover({
|
||||||
anchorEl,
|
anchorEl,
|
||||||
@@ -70,7 +70,9 @@ function EventPopover({
|
|||||||
const [end, setEnd] = useState("");
|
const [end, setEnd] = useState("");
|
||||||
const [calendarid, setCalendarid] = useState(0);
|
const [calendarid, setCalendarid] = useState(0);
|
||||||
const [allday, setAllDay] = useState(false);
|
const [allday, setAllDay] = useState(false);
|
||||||
const [repetition, setRepetition] = useState("");
|
const [repetition, setRepetition] = useState<RepetitionObject>(
|
||||||
|
{} as RepetitionObject
|
||||||
|
);
|
||||||
const [attendees, setAttendees] = useState<userAttendee[]>([]);
|
const [attendees, setAttendees] = useState<userAttendee[]>([]);
|
||||||
const [alarm, setAlarm] = useState("");
|
const [alarm, setAlarm] = useState("");
|
||||||
const [eventClass, setEventClass] = useState("PUBLIC");
|
const [eventClass, setEventClass] = useState("PUBLIC");
|
||||||
@@ -343,7 +345,7 @@ function EventPopover({
|
|||||||
<InputLabel id="busy">is Busy</InputLabel>
|
<InputLabel id="busy">is Busy</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
labelId="busy"
|
labelId="busy"
|
||||||
value={eventClass}
|
value={""}
|
||||||
label="is busy"
|
label="is busy"
|
||||||
onChange={(e: SelectChangeEvent) =>
|
onChange={(e: SelectChangeEvent) =>
|
||||||
console.log(e.target.value)
|
console.log(e.target.value)
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import {
|
|
||||||
FormControl,
|
|
||||||
InputLabel,
|
|
||||||
Select,
|
|
||||||
SelectChangeEvent,
|
|
||||||
MenuItem,
|
|
||||||
} from "@mui/material";
|
|
||||||
|
|
||||||
export default function RepeatEvent({
|
|
||||||
repetition,
|
|
||||||
setRepetition,
|
|
||||||
isOwn = true,
|
|
||||||
}: {
|
|
||||||
repetition: string;
|
|
||||||
setRepetition: Function;
|
|
||||||
isOwn?: boolean;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<FormControl fullWidth margin="dense" size="small">
|
|
||||||
<InputLabel id="repeat">Repetition</InputLabel>
|
|
||||||
<Select
|
|
||||||
labelId="repeat"
|
|
||||||
value={repetition}
|
|
||||||
disabled={!isOwn}
|
|
||||||
label="Repetition"
|
|
||||||
onChange={(e: SelectChangeEvent) => setRepetition(e.target.value)}
|
|
||||||
>
|
|
||||||
<MenuItem value={""}>No Repetition</MenuItem>
|
|
||||||
<MenuItem value={"daily"}>Repeat daily</MenuItem>
|
|
||||||
<MenuItem value={"weekly"}>Repeat weekly</MenuItem>
|
|
||||||
<MenuItem value={"monthly"}>Repeat monthly</MenuItem>
|
|
||||||
<MenuItem value={"yearly"}>Repeat yearly</MenuItem>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -21,5 +21,13 @@ export interface CalendarEvent {
|
|||||||
error?: string;
|
error?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
timezone: string;
|
timezone: string;
|
||||||
repetition?: string;
|
repetition?: RepetitionObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RepetitionObject {
|
||||||
|
freq: string;
|
||||||
|
interval?: number;
|
||||||
|
selectedDays?: string[];
|
||||||
|
occurrences?: number;
|
||||||
|
endDate?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,22 @@ export function parseCalendarEvent(
|
|||||||
break;
|
break;
|
||||||
case "status":
|
case "status":
|
||||||
event.status = String(value);
|
event.status = String(value);
|
||||||
|
break;
|
||||||
|
case "rrule":
|
||||||
|
event.repetition = { freq: value.freq.toLowerCase() };
|
||||||
|
if (value.byday) {
|
||||||
|
event.repetition.selectedDays = value.byday;
|
||||||
|
}
|
||||||
|
if (value.until) {
|
||||||
|
event.repetition.selectedDays = value.endDate;
|
||||||
|
}
|
||||||
|
if (value.count) {
|
||||||
|
event.repetition.selectedDays = value.occurrences;
|
||||||
|
}
|
||||||
|
if (value.interval) {
|
||||||
|
event.repetition.interval = value.interval;
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (recurrenceId && event.uid) {
|
if (recurrenceId && event.uid) {
|
||||||
@@ -105,7 +121,7 @@ export function calendarEventToJCal(event: CalendarEvent): any[] {
|
|||||||
const vevent: any[] = [
|
const vevent: any[] = [
|
||||||
"vevent",
|
"vevent",
|
||||||
[
|
[
|
||||||
["uid", {}, "text", event.uid],
|
["uid", {}, "text", event.uid.split("/")[0]],
|
||||||
["transp", {}, "text", event.transp ?? "OPAQUE"],
|
["transp", {}, "text", event.transp ?? "OPAQUE"],
|
||||||
[
|
[
|
||||||
"dtstart",
|
"dtstart",
|
||||||
@@ -150,8 +166,21 @@ export function calendarEventToJCal(event: CalendarEvent): any[] {
|
|||||||
if (event.description) {
|
if (event.description) {
|
||||||
vevent[1].push(["description", {}, "text", event.description]);
|
vevent[1].push(["description", {}, "text", event.description]);
|
||||||
}
|
}
|
||||||
if (event.repetition) {
|
if (event.repetition?.freq) {
|
||||||
vevent[1].push(["rrule", {}, "recur", { freq: event.repetition }]);
|
const repetitionRule: Record<string, any> = { freq: event.repetition.freq };
|
||||||
|
if (event.repetition.interval) {
|
||||||
|
repetitionRule["interval"] = event.repetition.interval;
|
||||||
|
}
|
||||||
|
if (event.repetition.occurrences) {
|
||||||
|
repetitionRule["count"] = event.repetition.occurrences;
|
||||||
|
}
|
||||||
|
if (event.repetition.endDate) {
|
||||||
|
repetitionRule["until"] = event.repetition.endDate;
|
||||||
|
}
|
||||||
|
if (event.repetition.selectedDays) {
|
||||||
|
repetitionRule["byday"] = event.repetition.selectedDays;
|
||||||
|
}
|
||||||
|
vevent[1].push(["rrule", {}, "recur", repetitionRule]);
|
||||||
}
|
}
|
||||||
|
|
||||||
event.attendee.forEach((att) => {
|
event.attendee.forEach((att) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user