[#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
);
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(
+25 -12
View File
@@ -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 (
<div className="weekSelector">
<div>{arg.text}</div>
{showSelector && (
<TimezoneSelector
value={timezone}
onChange={(newTimezone: string) =>
dispatch(setTimeZone(newTimezone))
}
/>
)}
<TimezoneSelector
value={timezone}
onChange={(newTimezone: string) =>
dispatch(setTimeZone(newTimezone))
}
/>
</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"}
slotLabelInterval={"01:00:00"}
scrollTime="12:00:00"
@@ -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);
@@ -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, [
+208 -106
View File
@@ -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({
<EditIcon />
</IconButton>
)}
<IconButton
size="small"
onClick={(e) => setToggleActionMenu(e.currentTarget)}
>
<MoreVertIcon />
</IconButton>
{((event.class !== "PRIVATE" && !isOwn) || isOwn) && (
<IconButton
size="small"
onClick={(e) => setToggleActionMenu(e.currentTarget)}
>
<MoreVertIcon />
</IconButton>
)}
<Menu
open={Boolean(toggleActionMenu)}
onClose={() => setToggleActionMenu(null)}
@@ -247,16 +255,44 @@ export default function EventPreviewModal({
</IconButton>
</Box>
</DialogActions>
<Typography
variant="inherit"
fontWeight="bold"
style={{
wordBreak: "break-word",
}}
gutterBottom
>
{event.title}
</Typography>
<Box display="flex" flexDirection="row">
{event.class === "PRIVATE" &&
(isOwn ? (
<Tooltip
title={
"Only you and attendees can see the details of this event."
}
placement="top"
>
<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>
{formatDate(event.start, event.allday)}
{event.end &&
@@ -387,99 +423,140 @@ export default function EventPreviewModal({
</>
}
>
{/* 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 && (
{((event.class !== "PRIVATE" && !isOwn) || isOwn) && (
<>
<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)}
{/* Video */}
{event.x_openpass_videoconference && (
<InfoRow
icon={<VideocamIcon style={{ fontSize: 18 }} />}
content={
<Button
variant="contained"
onClick={() =>
window.open(event.x_openpass_videoconference)
}
>
{showAllAttendees ? "Show less" : `Show more `}
</Typography>
</Box>
}
/>
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} />
)}
{/* 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 &&
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} />
{event.class === "PRIVATE" && !isOwn && (
<Box
sx={{
backgroundColor: "#F3F4F6",
height: 48,
borderRadius: "8px",
gap: "16px",
paddingTop: "16px",
paddingBottom: " 16px",
}}
>
<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 */}
{event.error && (
<InfoRow
@@ -488,7 +565,6 @@ export default function EventPreviewModal({
error
/>
)}
{/* Calendar color dot */}
<Box
style={{
@@ -508,7 +584,6 @@ export default function EventPreviewModal({
/>
<Typography variant="body2">{calendar.name}</Typography>
</Box>
{currentUserAttendee && !event.repetition && (
<>
<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) {
if (allday) {
return new Date(date).toLocaleDateString(undefined, {
+5 -1
View File
@@ -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;