diff --git a/__test__/features/Events/EventRepetition.test.tsx b/__test__/features/Events/EventRepetition.test.tsx index 03203a2..80ed9b9 100644 --- a/__test__/features/Events/EventRepetition.test.tsx +++ b/__test__/features/Events/EventRepetition.test.tsx @@ -8,7 +8,7 @@ import { createEventHandlers, EventHandlersProps, } from "../../../src/components/Calendar/handlers/eventHandlers"; -import EventPreviewModal from "../../../src/components/Event/EventDisplayPreview"; +import EventPreviewModal from "../../../src/features/Events/EventDisplayPreview"; describe("Recurrence Event Behavior Tests", () => { const mockOnClose = jest.fn(); diff --git a/src/features/Events/EventDisplay.tsx b/src/features/Events/EventDisplay.tsx index 5fdbf56..b94a79f 100644 --- a/src/features/Events/EventDisplay.tsx +++ b/src/features/Events/EventDisplay.tsx @@ -1,64 +1,74 @@ -import { 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 { - deleteEventAsync, - putEventAsync, - removeEvent, -} from "../Calendars/CalendarSlice"; + Box, + Button, + ButtonGroup, + Card, + CardActions, + CardContent, + CardHeader, + Checkbox, + Divider, + FormControl, + FormControlLabel, + IconButton, + InputLabel, + MenuItem, + Modal, + Select, + SelectChangeEvent, + TextField, + Typography, +} from "@mui/material"; +import { useEffect, useState } from "react"; import { useAppDispatch, useAppSelector } from "../../app/hooks"; import AttendeeSelector from "../../components/Attendees/AttendeeSearch"; import { - Button, - Box, - Typography, - ButtonGroup, - Card, - CardContent, - Divider, - IconButton, - Avatar, - Badge, - Modal, - CardHeader, - FormControl, - InputLabel, - MenuItem, - Select, - SelectChangeEvent, - CardActions, -} from "@mui/material"; -import DeleteIcon from "@mui/icons-material/Delete"; -import CloseIcon from "@mui/icons-material/Close"; -import VideocamIcon from "@mui/icons-material/Videocam"; -import CircleIcon from "@mui/icons-material/Circle"; -import { userAttendee } from "../User/userDataTypes"; -import { Calendars } from "../Calendars/CalendarTypes"; -import { CalendarEvent, RepetitionObject } from "./EventsTypes"; -import { formatLocalDateTime } from "../../components/Event/EventFormFields"; + handleDelete, + handleRSVP, +} from "../../components/Event/eventHandlers/eventHandlers"; import RepeatEvent from "../../components/Event/EventRepeat"; +import { InfoRow } from "../../components/Event/InfoRow"; +import { refreshCalendars } from "../../components/Event/utils/eventUtils"; +import { renderAttendeeBadge } from "../../components/Event/utils/eventUtils"; +import { getCalendarRange } from "../../utils/dateUtils"; +import { + moveEventAsync, + putEventAsync, + removeEvent, + updateEventInstanceAsync, + updateSeriesAsync, +} from "../Calendars/CalendarSlice"; +import { Calendars } from "../Calendars/CalendarTypes"; +import { userAttendee } from "../User/userDataTypes"; +import { getEvent } from "./EventApi"; +import { formatLocalDateTime } from "./EventModal"; +import { CalendarEvent, RepetitionObject } from "./EventsTypes"; export default function EventDisplayModal({ eventId, calId, open, onClose, - eventData, + typeOfAction, }: { eventId: string; calId: string; open: boolean; onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void; - eventData?: CalendarEvent | null; + typeOfAction?: "solo" | "all"; }) { const dispatch = useAppDispatch(); const calendar = useAppSelector((state) => state.calendars.list[calId]); - const cachedEvent = useAppSelector( + const event = useAppSelector( (state) => state.calendars.list[calId]?.events[eventId] ); const user = useAppSelector((state) => state.user); - // Use eventData from props if available, otherwise use cached data - const event = eventData || cachedEvent; - const [showAllAttendees, setShowAllAttendees] = useState(false); const [showMore, setShowMore] = useState(false); @@ -71,91 +81,135 @@ export default function EventDisplayModal({ ); // Form state + const [title, setTitle] = useState(event?.title ?? ""); + const [description, setDescription] = useState(event?.description ?? ""); + const [location, setLocation] = useState(event?.location ?? ""); + const [start, setStart] = useState( + formatLocalDateTime(new Date(event?.start ?? Date.now())) + ); + const [end, setEnd] = useState( + formatLocalDateTime(new Date(event?.end ?? Date.now())) + ); + const [allday, setAllDay] = useState(event?.allday); const [repetition, setRepetition] = useState( event?.repetition ?? ({} as RepetitionObject) ); const [alarm, setAlarm] = useState(event?.alarm?.trigger ?? ""); const [busy, setBusy] = useState(event?.transp ?? "OPAQUE"); - const [timezone] = useState(event?.timezone ?? "UTC"); + const [eventClass, setEventClass] = useState(event?.class ?? "PUBLIC"); + const [timezone, setTimezone] = useState(event?.timezone ?? "UTC"); + const [newCalId, setNewCalId] = useState(event?.calId); const [calendarid, setCalendarid] = useState( calId.split("/")[0] === user.userData?.openpaasId ? userPersonnalCalendars.findIndex((cal) => cal.id === calId) : calendars.findIndex((cal) => cal.id === calId) ); - const [attendees, setAttendees] = useState( - event?.attendee - ? event.attendee.filter( - (a) => a.cal_address !== event.organizer?.cal_address - ) - : [] + const [attendees, setAttendees] = useState( + (event?.attendee || []).filter( + (a) => a.cal_address !== event?.organizer?.cal_address + ) ); - - const [showRepeat] = useState(event?.repetition?.freq ? true : false); - - const isOwn = calId.split("/")[0] === user.userData?.openpaasId; - const isOwnCal = calendar?.id?.split("/")[0] === user.userData?.openpaasId; - const currentUserAttendee = event?.attendee?.find( - (a) => a.cal_address === user.userData?.email + (person) => person.cal_address === user.userData.email ); - const calList = - calId.split("/")[0] === user.userData?.openpaasId - ? userPersonnalCalendars - : calendars; + const organizer = + event?.attendee?.find( + (a) => a.cal_address === event?.organizer?.cal_address + ) ?? ({} as userAttendee); - function handleRSVP(rsvp: string) { - if (!event) return; - const newEvent = { ...event }; - newEvent.attendee = newEvent.attendee.map((a) => - a.cal_address === user.userData?.email ? { ...a, partstat: rsvp } : a - ); - dispatch(putEventAsync({ cal: calendar, newEvent })); - } + const isOwn = organizer?.cal_address === user.userData.email; + const isOwnCal = userPersonnalCalendars.find((cal) => cal.id === calId); + const attendeeDisplayLimit = 3; + + useEffect(() => { + if (!event || !calendar) { + onClose({}, "backdropClick"); + } + setRepetition(event?.repetition ?? ({} as RepetitionObject)); + }, [open, eventId, dispatch, onClose, event]); + useEffect(() => { + const fetchMasterEvent = async () => { + const masterEvent = await getEvent(event); + + setTitle(masterEvent.title ?? ""); + setDescription(masterEvent.description ?? ""); + setLocation(masterEvent.location ?? ""); + setStart(formatLocalDateTime(new Date(masterEvent?.start ?? Date.now()))); + setEnd(formatLocalDateTime(new Date(masterEvent?.end ?? Date.now()))); + setAllDay(masterEvent.allday ?? false); + setRepetition(masterEvent?.repetition ?? ({} as RepetitionObject)); + setAlarm(masterEvent?.alarm?.trigger ?? ""); + setBusy(masterEvent?.transp ?? "OPAQUE"); + setEventClass(masterEvent?.class ?? "PUBLIC"); + setTimezone(masterEvent.timezone ?? "UTC"); + }; + if (typeOfAction === "all") { + fetchMasterEvent(); + } + }, [typeOfAction, event]); + + if (!event || !calendar) return null; + const isRecurring = event.uid?.includes("/"); const handleSave = async () => { - if (!event) return; + const newEventUID = crypto.randomUUID(); const newEvent: CalendarEvent = { calId, - title: event.title, - URL: event.URL ?? `/calendars/${calId}/${event.uid}.ics`, - start: event.start, - end: event.end, - allday: event.allday, - uid: event.uid, - description: event.description, - location: event.location, + title, + URL: event.URL ?? `/calendars/${calId}/${newEventUID}.ics`, + start: new Date(start), + end: new Date(end), + allday, + uid: event.uid ?? newEventUID, + description, + location, repetition, - class: event.class, + class: eventClass, organizer: event.organizer, timezone, - attendee: event.organizer - ? [event.organizer as userAttendee, ...attendees] - : attendees, + attendee: [organizer, ...attendees], transp: busy, color: userPersonnalCalendars[calendarid]?.color, alarm: { trigger: alarm, action: "EMAIL" }, }; const [baseId, recurrenceId] = event.uid.split("/"); - if (recurrenceId) { - Object.keys(userPersonnalCalendars[calendarid].events).forEach( - (element) => { - if (element.split("/")[0] === baseId) { - dispatch(removeEvent({ calendarUid: calId, eventUid: element })); - } - } + const calendarRange = getCalendarRange(new Date(start)); + + if (typeOfAction === "solo") { + dispatch( + updateEventInstanceAsync({ + cal: userPersonnalCalendars[calendarid], + event: { ...newEvent, recurrenceId: recurrenceId }, + }) + ); + } else if (typeOfAction === "all") { + dispatch( + updateSeriesAsync({ + cal: userPersonnalCalendars[calendarid], + event: { ...newEvent, recurrenceId: recurrenceId }, + }) + ); + await refreshCalendars(dispatch, calendars, calendarRange); + } else { + dispatch( + putEventAsync({ cal: userPersonnalCalendars[calendarid], newEvent }) ); } - await dispatch( - putEventAsync({ - cal: userPersonnalCalendars[calendarid], - newEvent, - }) - ); + if (newCalId !== calId) { + dispatch( + moveEventAsync({ + cal: userPersonnalCalendars[calendarid], + newEvent, + newURL: `/calendars/${newCalId}/${baseId}.ics`, + }) + ); + dispatch(removeEvent({ calendarUid: calId, eventUid: event.uid })); + } onClose({}, "backdropClick"); }; @@ -163,416 +217,423 @@ export default function EventDisplayModal({ setShowMore(!showMore); }; - function getEventColor() { - if (!event) return "#1976d2"; - const color = event.color || calendar?.color || "#1976d2"; - return color; - } - - if (!event) return null; + const calList = + calId.split("/")[0] === user.userData?.openpaasId + ? Object.keys(userPersonnalCalendars).map((calendar, index) => ( + + + + {userPersonnalCalendars[index].name} + + + )) + : Object.keys(calendars).map((calendar, index) => ( + + + + {calendars[index].name} - {calendars[index].owner} + + + )); return ( - <> - - - - onClose({}, "backdropClick")}> - - - } - /> - - {/* Event details */} - } - text={event?.title || "No title"} - /> - } - text={`${formatLocalDateTime(new Date(event?.start || Date.now()))} - ${formatLocalDateTime(new Date(event?.end || Date.now()))}`} - /> - } - text={event?.location || "No location"} - /> - } - text={event?.description || "No description"} - /> + + + + {/* Close button */} + + onClose({}, "backdropClick")} + > + + + - {/* Attendees */} - {event?.attendee && event.attendee.length > 0 && ( - - - Attendees - - {event.attendee - .slice(0, showAllAttendees ? event.attendee.length : 3) - .map((attendee, index) => ( - + + + {/* Title */} + setTitle(e.target.value)} + size="small" + margin="dense" + /> + + {/* RSVP */} + {currentUserAttendee && isOwnCal && ( + + + + + + + + + )} + + {/* Calendar selector */} + + Calendar + + + + {/* Dates */} + + setStart(formatLocalDateTime(new Date(e.target.value))) + } + size="small" + margin="dense" + InputLabelProps={{ shrink: true }} + /> + + + setEnd(formatLocalDateTime(new Date(e.target.value))) + } + size="small" + margin="dense" + InputLabelProps={{ shrink: true }} + /> + + { + const endDate = new Date(end); + const startDate = new Date(start); + setAllDay(!allday); + if (endDate.getDate() === startDate.getDate()) { + endDate.setDate(startDate.getDate() + 1); + setEnd(formatLocalDateTime(endDate)); + } + }} + /> + } + label="All day" + /> + + {/* Description & Location */} + setDescription(e.target.value)} + size="small" + margin="dense" + multiline + rows={2} + /> + + {isOwn && ( + + )} + + setLocation(e.target.value)} + size="small" + margin="dense" + /> + + {/* Video */} + {event.x_openpass_videoconference && ( + } + content={ + + } + /> + )} + + {/* Attendees */} + {event.attendee?.length > 0 && ( + + Attendees: + {organizer.cal_address && + renderAttendeeBadge(organizer, "org", true, true)} + {(showAllAttendees + ? attendees + : attendees.slice(0, attendeeDisplayLimit) + ).map((a, idx) => ( + + {renderAttendeeBadge(a, idx.toString(), true)} + {isOwn && ( + { + const newAttendeesList = [...attendees]; + newAttendeesList.splice(idx, 1); + setAttendees(newAttendeesList); }} > - - - {attendee.cn || attendee.cal_address} - - - - {attendee.partstat} - - - - ))} - {event.attendee.length > 3 && ( - - )} - - )} + + + )} + + ))} + {attendees.length > attendeeDisplayLimit && ( + setShowAllAttendees(!showAllAttendees)} + > + {showAllAttendees + ? "Show less" + : `Show more (${ + attendees.length - attendeeDisplayLimit + } more)`} + + )} + + )} - {/* RSVP */} - {currentUserAttendee && isOwnCal && ( - - - - - - - - - )} + - {/* Calendar selector */} - - Calendar - + setAlarm(e.target.value) + } + > + No Notification + 1 minute + 2 minutes + 10 minutes + 15 minutes + 30 minutes + 1 hours + 2 hours + 5 hours + 12 hours + 1 day + 2 days + 1 week + + + + Visibility + + + + is Busy + + + {/* Error */} + {event.error && ( + + } + text={event.error} + error + /> + )} + + )} + + + + + {isOwn && ( + + handleDelete( + isRecurring, + typeOfAction, + onClose, + dispatch, + calendar, + event, + calId, + eventId + ) } > - {calList.map((calendar, index) => ( - - {calendar.name} - - ))} - - - - {/* Video conference */} - {event?.x_openpass_videoconference && ( - - - Video Conference - - - + + )} + - {/* Extended options */} - {showMore && ( - <> - - - Extended Options - - - {/* Notification */} - - Notification - - - - {/* Show me as */} - - Show me as - - - - {/* Repeat */} - {showRepeat && ( - - - Repeat - - - - )} - - {/* Attendees */} - - - Participants - - - - - )} - - - - - {isOwn && ( - { - onClose({}, "backdropClick"); - dispatch( - deleteEventAsync({ - calId, - eventId, - eventURL: event.URL, - }) - ); - }} - > - - - )} - - - {isOwn && ( - - )} - - - - - - - ); -} - -export function InfoRow({ - icon, - text, - error = false, - data, -}: { - icon: React.ReactNode; - text: string; - error?: boolean; - data?: string; -}) { - return ( - - {icon} - - {text} - - {data && ( - - {data} - - )} - - ); -} - -export function AttendeeRow({ - attendee, - key, -}: { - attendee: userAttendee; - key: number; -}) { - return ( - - - - {attendee.cn || attendee.cal_address} - - - {attendee.partstat} - - - ); -} - -export function stringToColor(string: string) { - let hash = 0; - let i; - - /* eslint-disable no-bitwise */ - for (i = 0; i < string.length; i += 1) { - hash = string.charCodeAt(i) + ((hash << 5) - hash); - } - - let color = "#"; - - for (i = 0; i < 3; i += 1) { - const value = (hash >> (i * 8)) & 0xff; - color += `00${value.toString(16)}`.substr(-2); - } - /* eslint-enable no-bitwise */ - - return color; -} - -export function stringAvatar(name: string) { - return { - sx: { width: 24, height: 24, fontSize: 18, bgcolor: stringToColor(name) }, - children: name[0], - }; -} - -export function renderAttendeeBadge( - attendee: userAttendee, - key: string, - isOrganizer = false -) { - return ( - - - - {attendee.cn || attendee.cal_address} - {isOrganizer && " (Organizer)"} - - + )} + + + + + ); }