diff --git a/__test__/features/Events/EventDisplay.test.tsx b/__test__/features/Events/EventDisplay.test.tsx index 6e3a999..ff1aa09 100644 --- a/__test__/features/Events/EventDisplay.test.tsx +++ b/__test__/features/Events/EventDisplay.test.tsx @@ -155,8 +155,10 @@ describe("Event Preview Display", () => { />, preloadedState ); - fireEvent.click(screen.getByTestId("MoreVertIcon")); - expect(screen.queryByText("Delete event")).not.toBeInTheDocument(); + const moreButton = screen.getByTestId("MoreVertIcon"); + if (moreButton) { + expect(screen.queryByText("Delete event")).not.toBeInTheDocument(); + } cleanup(); // Renders the personnal cal event renderWithProviders( diff --git a/src/components/Calendar/Calendar.tsx b/src/components/Calendar/Calendar.tsx index 67d88fc..bc4b9ac 100644 --- a/src/components/Calendar/Calendar.tsx +++ b/src/components/Calendar/Calendar.tsx @@ -3,7 +3,6 @@ import dayGridPlugin from "@fullcalendar/daygrid"; import timeGridPlugin from "@fullcalendar/timegrid"; import interactionPlugin from "@fullcalendar/interaction"; import { CalendarApi, DateSelectArg } from "@fullcalendar/core"; -import ReactCalendar from "react-calendar"; import "./Calendar.styl"; import "./CustomCalendar.styl"; import { useEffect, useRef, useState } from "react"; @@ -330,25 +329,39 @@ export default function CalendarApp({ filteredTempEvents, userId )} - weekNumbers + weekNumbers={ + currentView === "timeGridWeek" || currentView === "timeGridDay" + } weekNumberFormat={{ week: "long" }} weekNumberContent={(arg) => { - const showSelector = - currentView === "timeGridWeek" || currentView === "timeGridDay"; return (
{arg.text}
- {showSelector && ( - - dispatch(setTimeZone(newTimezone)) - } - /> - )} + + dispatch(setTimeZone(newTimezone)) + } + />
); }} + dayCellContent={(arg) => { + const month = arg.date.toLocaleDateString("en-US", { + month: "short", + }); + if (arg.view.type === "dayGridMonth") { + return ( + + {arg.dayNumberText === "1" ? month : ""} {arg.dayNumberText} + + ); + } + }} slotDuration={"00:30:00"} slotLabelInterval={"01:00:00"} scrollTime="12:00:00" diff --git a/src/components/Calendar/handlers/eventHandlers.ts b/src/components/Calendar/handlers/eventHandlers.ts index 0b6c1d0..b6fac58 100644 --- a/src/components/Calendar/handlers/eventHandlers.ts +++ b/src/components/Calendar/handlers/eventHandlers.ts @@ -5,6 +5,7 @@ import { Calendars } from "../../../features/Calendars/CalendarTypes"; import { getDeltaInMilliseconds } from "../../../utils/dateUtils"; import { getCalendarDetailAsync, + getEventAsync, putEventAsync, updateEventInstanceAsync, updateEventLocal, @@ -97,6 +98,21 @@ export const createEventHandlers = (props: EventHandlersProps) => { window.open(info.event.url); } else { setOpenEventDisplay(true); + if ( + calendars[info.event.extendedProps.calId] && + calendars[info.event.extendedProps.calId].events[ + info.event.extendedProps.uid + ] + ) { + dispatch( + getEventAsync( + calendars[info.event.extendedProps.calId].events[ + info.event.extendedProps.uid + ] + ) + ); + } + setEventDisplayedId(info.event.extendedProps.uid); setEventDisplayedCalId(info.event.extendedProps.calId); setEventDisplayedTemp(info.event._def.extendedProps.temp); diff --git a/src/components/Calendar/hooks/useCalendarEventHandlers.ts b/src/components/Calendar/hooks/useCalendarEventHandlers.ts index c7f3773..dccc16c 100644 --- a/src/components/Calendar/hooks/useCalendarEventHandlers.ts +++ b/src/components/Calendar/hooks/useCalendarEventHandlers.ts @@ -30,6 +30,7 @@ export const useCalendarEventHandlers = (props: EventHandlersProps) => { props.setEventDisplayedId, props.setEventDisplayedCalId, props.setEventDisplayedTemp, + props.calendars, ]), handleEventAllow: useCallback(eventHandlers.handleEventAllow, []), handleEventDrop: useCallback(eventHandlers.handleEventDrop, [ diff --git a/src/features/Events/EventDisplayPreview.tsx b/src/features/Events/EventDisplayPreview.tsx index 4cf16c0..41c33b4 100644 --- a/src/features/Events/EventDisplayPreview.tsx +++ b/src/features/Events/EventDisplayPreview.tsx @@ -10,15 +10,19 @@ 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 RepeatIcon from "@mui/icons-material/Repeat"; +import LockOutlineIcon from "@mui/icons-material/LockOutline"; import { Box, Button, ButtonGroup, + Chip, DialogActions, Divider, IconButton, Menu, MenuItem, + Tooltip, Typography, } from "@mui/material"; import AvatarGroup from "@mui/material/AvatarGroup"; @@ -40,6 +44,7 @@ import { renderAttendeeBadge } from "../../components/Event/utils/eventUtils"; import { getTimezoneOffset } from "../../components/Calendar/TimezoneSelector"; import { getCalendarRange } from "../../utils/dateUtils"; import { updateTempCalendar } from "../../components/Calendar/utils/calendarUtils"; +import { CalendarEvent } from "./EventsTypes"; export default function EventPreviewModal({ eventId, calId, @@ -68,6 +73,7 @@ export default function EventPreviewModal({ const user = useAppSelector((state) => state.user); const isRecurring = event?.uid?.includes("/"); + const isOwn = calendar.ownerEmails?.includes(user.userData.email); const [showAllAttendees, setShowAllAttendees] = useState(false); const [openFullDisplay, setOpenFullDisplay] = useState(false); const [openUpdateModal, setOpenUpdateModal] = useState(false); @@ -177,12 +183,14 @@ export default function EventPreviewModal({ )} - setToggleActionMenu(e.currentTarget)} - > - - + {((event.class !== "PRIVATE" && !isOwn) || isOwn) && ( + setToggleActionMenu(e.currentTarget)} + > + + + )} setToggleActionMenu(null)} @@ -247,16 +255,44 @@ export default function EventPreviewModal({ - - {event.title} - + + {event.class === "PRIVATE" && + (isOwn ? ( + + + + ) : ( + + ))} + + {event.title} + + {event.transp === "TRANSPARENT" && ( + + } + label="Free" + /> + + )} + {formatDate(event.start, event.allday)} {event.end && @@ -387,99 +423,140 @@ export default function EventPreviewModal({ } > - {/* Video */} - {event.x_openpass_videoconference && ( - } - content={ - - } - /> - )} - {/* Attendees */} - {attendees?.length > 0 && ( + {((event.class !== "PRIVATE" && !isOwn) || isOwn) && ( <> - } - content={ - - - {attendees.length} guests - - { - attendees.filter((a) => a.partstat === "ACCEPTED") - .length - }{" "} - yes,{" "} - { - attendees.filter((a) => a.partstat === "DECLINED") - .length - }{" "} - no - - - {!showAllAttendees && ( - - {organizer && - renderAttendeeBadge( - organizer, - "org", - showAllAttendees, - true - )} - {visibleAttendees.map((a, idx) => - renderAttendeeBadge(a, idx.toString(), showAllAttendees) - )} - - )} - setShowAllAttendees(!showAllAttendees)} + {/* Video */} + {event.x_openpass_videoconference && ( + } + content={ + + } + /> + )} + {/* Attendees */} + {attendees?.length > 0 && ( + <> + } + content={ + + + {attendees.length} guests + + { + attendees.filter((a) => a.partstat === "ACCEPTED") + .length + }{" "} + yes,{" "} + { + attendees.filter((a) => a.partstat === "DECLINED") + .length + }{" "} + no + + + {!showAllAttendees && ( + + {organizer && + renderAttendeeBadge( + organizer, + "org", + showAllAttendees, + true + )} + {visibleAttendees.map((a, idx) => + renderAttendeeBadge( + a, + idx.toString(), + showAllAttendees + ) + )} + + )} + setShowAllAttendees(!showAllAttendees)} + > + {showAllAttendees ? "Show less" : `Show more `} + + + } + /> + + )} + {showAllAttendees && + organizer && + renderAttendeeBadge(organizer, "org", showAllAttendees, true)} + {showAllAttendees && + visibleAttendees.map((a, idx) => + renderAttendeeBadge(a, idx.toString(), showAllAttendees) + )} + {/* Location */} + {event.location && ( + } + text={event.location} + /> + )} + {/* Description */} + {event.description && ( + } text={event.description} /> + )} + {/* ALARM */} + {event.alarm && ( + } + text={`${event.alarm.trigger} before by ${event.alarm.action}`} + /> + )} + {/* Repetition */} + {event.repetition && ( + } + text={makeRecurrenceString(event)} + /> + )} )} - {showAllAttendees && - organizer && - renderAttendeeBadge(organizer, "org", showAllAttendees, true)} - {showAllAttendees && - visibleAttendees.map((a, idx) => - renderAttendeeBadge(a, idx.toString(), showAllAttendees) - )} - {/* Location */} - {event.location && ( - } text={event.location} /> + {event.class === "PRIVATE" && !isOwn && ( + + + This is a private event. Details are hidden. + + )} - {/* Description */} - {event.description && ( - } text={event.description} /> - )} - - {/* Description */} - {event.alarm && ( - } - text={`${event.alarm.trigger} before by ${event.alarm.action}`} - /> - )} - {/* Error */} {event.error && ( )} - {/* Calendar color dot */} {calendar.name} - {currentUserAttendee && !event.repetition && ( <> @@ -549,6 +624,33 @@ export default function EventPreviewModal({ ); } +function makeRecurrenceString(event: CalendarEvent): string | undefined { + if (!event.repetition) { + return; + } + const recur = [`Reccurent Event ยท ${event.repetition.freq}`]; + const recurType: Record = { + daily: "days", + monthly: "months", + yearly: "years", + }; + if (event.repetition.byday) { + recur.push(`on ${event.repetition.byday.join(", ")}`); + } + if (event.repetition.interval && event.repetition.interval > 1) { + recur.push( + `every ${event.repetition.interval} ${recurType[event.repetition.freq]}` + ); + } + if (event.repetition.occurrences) { + recur.push(`for ${event.repetition.occurrences} occurences`); + } + if (event.repetition.endDate) { + recur.push(`until ${event.repetition.endDate}`); + } + return recur.join(", "); +} + function formatDate(date: Date | string, allday?: boolean) { if (allday) { return new Date(date).toLocaleDateString(undefined, { diff --git a/src/features/Events/eventUtils.ts b/src/features/Events/eventUtils.ts index 8ac4bd8..bce2723 100644 --- a/src/features/Events/eventUtils.ts +++ b/src/features/Events/eventUtils.ts @@ -94,7 +94,11 @@ export function parseCalendarEvent( case "rrule": event.repetition = { freq: value.freq.toLowerCase() }; if (value.byday) { - event.repetition.byday = value.byday; + if (typeof value.byday === "string") { + event.repetition.byday = [value.byday]; + } else { + event.repetition.byday = value.byday; + } } if (value.until) { event.repetition.endDate = value.until;