[#197] added missing infos

This commit is contained in:
Camille Moussu
2025-10-16 11:48:50 +02:00
committed by Benoit TELLIER
parent ed8bb41ca9
commit 6411ed636b
6 changed files with 259 additions and 121 deletions
@@ -155,8 +155,10 @@ describe("Event Preview Display", () => {
/>, />,
preloadedState preloadedState
); );
fireEvent.click(screen.getByTestId("MoreVertIcon")); const moreButton = screen.getByTestId("MoreVertIcon");
expect(screen.queryByText("Delete event")).not.toBeInTheDocument(); if (moreButton) {
expect(screen.queryByText("Delete event")).not.toBeInTheDocument();
}
cleanup(); cleanup();
// Renders the personnal cal event // Renders the personnal cal event
renderWithProviders( renderWithProviders(
+25 -12
View File
@@ -3,7 +3,6 @@ import dayGridPlugin from "@fullcalendar/daygrid";
import timeGridPlugin from "@fullcalendar/timegrid"; import timeGridPlugin from "@fullcalendar/timegrid";
import interactionPlugin from "@fullcalendar/interaction"; import interactionPlugin from "@fullcalendar/interaction";
import { CalendarApi, DateSelectArg } from "@fullcalendar/core"; import { CalendarApi, DateSelectArg } from "@fullcalendar/core";
import ReactCalendar from "react-calendar";
import "./Calendar.styl"; import "./Calendar.styl";
import "./CustomCalendar.styl"; import "./CustomCalendar.styl";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
@@ -330,25 +329,39 @@ export default function CalendarApp({
filteredTempEvents, filteredTempEvents,
userId userId
)} )}
weekNumbers weekNumbers={
currentView === "timeGridWeek" || currentView === "timeGridDay"
}
weekNumberFormat={{ week: "long" }} weekNumberFormat={{ week: "long" }}
weekNumberContent={(arg) => { weekNumberContent={(arg) => {
const showSelector =
currentView === "timeGridWeek" || currentView === "timeGridDay";
return ( return (
<div className="weekSelector"> <div className="weekSelector">
<div>{arg.text}</div> <div>{arg.text}</div>
{showSelector && ( <TimezoneSelector
<TimezoneSelector value={timezone}
value={timezone} onChange={(newTimezone: string) =>
onChange={(newTimezone: string) => dispatch(setTimeZone(newTimezone))
dispatch(setTimeZone(newTimezone)) }
} />
/>
)}
</div> </div>
); );
}} }}
dayCellContent={(arg) => {
const month = arg.date.toLocaleDateString("en-US", {
month: "short",
});
if (arg.view.type === "dayGridMonth") {
return (
<span
className={`fc-daygrid-day-number ${
arg.isToday ? "current-date" : ""
}`}
>
{arg.dayNumberText === "1" ? month : ""} {arg.dayNumberText}
</span>
);
}
}}
slotDuration={"00:30:00"} slotDuration={"00:30:00"}
slotLabelInterval={"01:00:00"} slotLabelInterval={"01:00:00"}
scrollTime="12:00:00" scrollTime="12:00:00"
@@ -5,6 +5,7 @@ import { Calendars } from "../../../features/Calendars/CalendarTypes";
import { getDeltaInMilliseconds } from "../../../utils/dateUtils"; import { getDeltaInMilliseconds } from "../../../utils/dateUtils";
import { import {
getCalendarDetailAsync, getCalendarDetailAsync,
getEventAsync,
putEventAsync, putEventAsync,
updateEventInstanceAsync, updateEventInstanceAsync,
updateEventLocal, updateEventLocal,
@@ -97,6 +98,21 @@ export const createEventHandlers = (props: EventHandlersProps) => {
window.open(info.event.url); window.open(info.event.url);
} else { } else {
setOpenEventDisplay(true); 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); 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);
@@ -30,6 +30,7 @@ export const useCalendarEventHandlers = (props: EventHandlersProps) => {
props.setEventDisplayedId, props.setEventDisplayedId,
props.setEventDisplayedCalId, props.setEventDisplayedCalId,
props.setEventDisplayedTemp, props.setEventDisplayedTemp,
props.calendars,
]), ]),
handleEventAllow: useCallback(eventHandlers.handleEventAllow, []), handleEventAllow: useCallback(eventHandlers.handleEventAllow, []),
handleEventDrop: useCallback(eventHandlers.handleEventDrop, [ handleEventDrop: useCallback(eventHandlers.handleEventDrop, [
+208 -106
View File
@@ -10,15 +10,19 @@ import NotificationsNoneIcon from "@mui/icons-material/NotificationsNone";
import PeopleAltOutlinedIcon from "@mui/icons-material/PeopleAltOutlined"; import PeopleAltOutlinedIcon from "@mui/icons-material/PeopleAltOutlined";
import SubjectIcon from "@mui/icons-material/Subject"; import SubjectIcon from "@mui/icons-material/Subject";
import VideocamIcon from "@mui/icons-material/Videocam"; import VideocamIcon from "@mui/icons-material/Videocam";
import RepeatIcon from "@mui/icons-material/Repeat";
import LockOutlineIcon from "@mui/icons-material/LockOutline";
import { import {
Box, Box,
Button, Button,
ButtonGroup, ButtonGroup,
Chip,
DialogActions, DialogActions,
Divider, Divider,
IconButton, IconButton,
Menu, Menu,
MenuItem, MenuItem,
Tooltip,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
import AvatarGroup from "@mui/material/AvatarGroup"; import AvatarGroup from "@mui/material/AvatarGroup";
@@ -40,6 +44,7 @@ import { renderAttendeeBadge } from "../../components/Event/utils/eventUtils";
import { getTimezoneOffset } from "../../components/Calendar/TimezoneSelector"; import { getTimezoneOffset } from "../../components/Calendar/TimezoneSelector";
import { getCalendarRange } from "../../utils/dateUtils"; import { getCalendarRange } from "../../utils/dateUtils";
import { updateTempCalendar } from "../../components/Calendar/utils/calendarUtils"; import { updateTempCalendar } from "../../components/Calendar/utils/calendarUtils";
import { CalendarEvent } from "./EventsTypes";
export default function EventPreviewModal({ export default function EventPreviewModal({
eventId, eventId,
calId, calId,
@@ -68,6 +73,7 @@ export default function EventPreviewModal({
const user = useAppSelector((state) => state.user); const user = useAppSelector((state) => state.user);
const isRecurring = event?.uid?.includes("/"); const isRecurring = event?.uid?.includes("/");
const isOwn = calendar.ownerEmails?.includes(user.userData.email);
const [showAllAttendees, setShowAllAttendees] = useState(false); const [showAllAttendees, setShowAllAttendees] = useState(false);
const [openFullDisplay, setOpenFullDisplay] = useState(false); const [openFullDisplay, setOpenFullDisplay] = useState(false);
const [openUpdateModal, setOpenUpdateModal] = useState(false); const [openUpdateModal, setOpenUpdateModal] = useState(false);
@@ -177,12 +183,14 @@ export default function EventPreviewModal({
<EditIcon /> <EditIcon />
</IconButton> </IconButton>
)} )}
<IconButton {((event.class !== "PRIVATE" && !isOwn) || isOwn) && (
size="small" <IconButton
onClick={(e) => setToggleActionMenu(e.currentTarget)} size="small"
> onClick={(e) => setToggleActionMenu(e.currentTarget)}
<MoreVertIcon /> >
</IconButton> <MoreVertIcon />
</IconButton>
)}
<Menu <Menu
open={Boolean(toggleActionMenu)} open={Boolean(toggleActionMenu)}
onClose={() => setToggleActionMenu(null)} onClose={() => setToggleActionMenu(null)}
@@ -247,16 +255,44 @@ export default function EventPreviewModal({
</IconButton> </IconButton>
</Box> </Box>
</DialogActions> </DialogActions>
<Typography <Box display="flex" flexDirection="row">
variant="inherit" {event.class === "PRIVATE" &&
fontWeight="bold" (isOwn ? (
style={{ <Tooltip
wordBreak: "break-word", title={
}} "Only you and attendees can see the details of this event."
gutterBottom }
> placement="top"
{event.title} >
</Typography> <LockOutlineIcon />
</Tooltip>
) : (
<LockOutlineIcon />
))}
<Typography
variant="inherit"
fontWeight="bold"
style={{
wordBreak: "break-word",
}}
gutterBottom
>
{event.title}
</Typography>
{event.transp === "TRANSPARENT" && (
<Tooltip
title={
"Others see you as available during the time range of this event."
}
placement="top"
>
<Chip
icon={<CircleIcon color="success" fontSize="small" />}
label="Free"
/>
</Tooltip>
)}
</Box>
<Typography variant="body2" color="textSecondary" gutterBottom> <Typography variant="body2" color="textSecondary" gutterBottom>
{formatDate(event.start, event.allday)} {formatDate(event.start, event.allday)}
{event.end && {event.end &&
@@ -387,99 +423,140 @@ export default function EventPreviewModal({
</> </>
} }
> >
{/* Video */} {((event.class !== "PRIVATE" && !isOwn) || isOwn) && (
{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 {/* Video */}
icon={<PeopleAltOutlinedIcon />} {event.x_openpass_videoconference && (
content={ <InfoRow
<Box icon={<VideocamIcon style={{ fontSize: 18 }} />}
style={{ content={
marginBottom: 1, <Button
display: "flex", variant="contained"
flexDirection: "row", onClick={() =>
}} window.open(event.x_openpass_videoconference)
> }
<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 `} Join the video conference
</Typography> </Button>
</Box> }
} />
/> )}
{/* 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} />
)}
{/* ALARM */}
{event.alarm && (
<InfoRow
icon={<NotificationsNoneIcon />}
text={`${event.alarm.trigger} before by ${event.alarm.action}`}
/>
)}
{/* Repetition */}
{event.repetition && (
<InfoRow
icon={<RepeatIcon />}
text={makeRecurrenceString(event)}
/>
)}
</> </>
)} )}
{showAllAttendees && {event.class === "PRIVATE" && !isOwn && (
organizer && <Box
renderAttendeeBadge(organizer, "org", showAllAttendees, true)} sx={{
{showAllAttendees && backgroundColor: "#F3F4F6",
visibleAttendees.map((a, idx) => height: 48,
renderAttendeeBadge(a, idx.toString(), showAllAttendees) borderRadius: "8px",
)} gap: "16px",
{/* Location */} paddingTop: "16px",
{event.location && ( paddingBottom: " 16px",
<InfoRow icon={<LocationOnOutlinedIcon />} text={event.location} /> }}
>
<Typography
fontFamily={"Inter"}
fontWeight={500}
fontSize={"12px"}
lineHeight={"16px"}
letterSpacing={"0.5px"}
textAlign={"center"}
>
This is a private event. Details are hidden.
</Typography>
</Box>
)} )}
{/* 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 */} {/* Error */}
{event.error && ( {event.error && (
<InfoRow <InfoRow
@@ -488,7 +565,6 @@ export default function EventPreviewModal({
error error
/> />
)} )}
{/* Calendar color dot */} {/* Calendar color dot */}
<Box <Box
style={{ style={{
@@ -508,7 +584,6 @@ export default function EventPreviewModal({
/> />
<Typography variant="body2">{calendar.name}</Typography> <Typography variant="body2">{calendar.name}</Typography>
</Box> </Box>
{currentUserAttendee && !event.repetition && ( {currentUserAttendee && !event.repetition && (
<> <>
<Divider variant="fullWidth" /> <Divider variant="fullWidth" />
@@ -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<string, string> = {
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) { function formatDate(date: Date | string, allday?: boolean) {
if (allday) { if (allday) {
return new Date(date).toLocaleDateString(undefined, { return new Date(date).toLocaleDateString(undefined, {
+5 -1
View File
@@ -94,7 +94,11 @@ export function parseCalendarEvent(
case "rrule": case "rrule":
event.repetition = { freq: value.freq.toLowerCase() }; event.repetition = { freq: value.freq.toLowerCase() };
if (value.byday) { 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) { if (value.until) {
event.repetition.endDate = value.until; event.repetition.endDate = value.until;