From 5269c43354c7a03381c742f73a70eb18eafaaa7d Mon Sep 17 00:00:00 2001 From: Camille Moussu Date: Thu, 12 Mar 2026 09:26:58 +0100 Subject: [PATCH] [#625] added freebusy handling in event preview --- .../features/Events/EventDisplay.test.tsx | 4 +- src/components/Event/utils/eventUtils.tsx | 12 ++++- .../EventPreview/EventPreviewActionMenu.tsx | 6 +-- .../EventPreview/EventPreviewAttendees.tsx | 47 ++++++++++++++++++- .../EventPreview/EventPreviewDetails.tsx | 31 ++++++------ .../EventPreview/EventPreviewHeader.tsx | 8 ++-- .../Events/EventPreview/EventPreviewModal.tsx | 28 +++++------ .../EventPreview/useEventPreviewState.ts | 2 +- .../EventPreview/utils/makeAttendeePreview.ts | 29 ++++++++---- .../api/getFreeBusyForEventAttendeesPOST.ts | 31 ++++++++++-- 10 files changed, 137 insertions(+), 61 deletions(-) diff --git a/__test__/features/Events/EventDisplay.test.tsx b/__test__/features/Events/EventDisplay.test.tsx index a42bcd8..7502b8b 100644 --- a/__test__/features/Events/EventDisplay.test.tsx +++ b/__test__/features/Events/EventDisplay.test.tsx @@ -650,7 +650,7 @@ describe("Event Preview Display", () => { fireEvent.click(emailButton); - const expectedUrl = `test/mailto/?uri=mailto:john@test.com&subject=Test%20Event`; + const expectedUrl = `test/mailto/?uri=mailto%3Ajohn%40test.com&subject=Test%20Event`; expect(mockOpen).toHaveBeenCalledWith(expectedUrl); }); @@ -721,7 +721,7 @@ describe("Event Preview Display", () => { fireEvent.click(emailButton); - const expectedUrl = `test/mailto/?uri=mailto:john@test.com&subject=Meeting%20%26%20Discussion%3F%20%23Important`; + const expectedUrl = `test/mailto/?uri=mailto%3Ajohn%40test.com&subject=Meeting%20%26%20Discussion%3F%20%23Important`; expect(mockOpen).toHaveBeenCalledWith(expectedUrl); }); diff --git a/src/components/Event/utils/eventUtils.tsx b/src/components/Event/utils/eventUtils.tsx index ab51f05..68bc988 100644 --- a/src/components/Event/utils/eventUtils.tsx +++ b/src/components/Event/utils/eventUtils.tsx @@ -18,7 +18,8 @@ export function renderAttendeeBadge( key: string, t: (key: string) => string, isFull?: boolean, - isOrganizer?: boolean + isOrganizer?: boolean, + caption?: string ) { const classIcon = a.partstat === "ACCEPTED" ? ( @@ -65,12 +66,19 @@ export function renderAttendeeBadge( - {a.cn || a.cal_address} + + {a.cn || a.cal_address} + {isOrganizer && ( {t("event.organizer")} )} + {caption && ( + + {caption} + + )} ); diff --git a/src/features/Events/EventPreview/EventPreviewActionMenu.tsx b/src/features/Events/EventPreview/EventPreviewActionMenu.tsx index 8fb210b..ebf7523 100644 --- a/src/features/Events/EventPreview/EventPreviewActionMenu.tsx +++ b/src/features/Events/EventPreview/EventPreviewActionMenu.tsx @@ -36,9 +36,9 @@ export function EventPreviewActionMenu({ window.open( - `${mailSpaUrl}/mailto/?uri=mailto:${otherAttendees - .map((a) => a.cal_address) - .join(",")}&subject=${encodeURIComponent(event.title ?? "")}` + `${mailSpaUrl}/mailto/?uri=${encodeURIComponent( + `mailto:${otherAttendees.map((a) => a.cal_address).join(",")}` + )}&subject=${encodeURIComponent(event.title ?? "")}` ) } > diff --git a/src/features/Events/EventPreview/EventPreviewAttendees.tsx b/src/features/Events/EventPreview/EventPreviewAttendees.tsx index 4da1b33..5deac66 100644 --- a/src/features/Events/EventPreview/EventPreviewAttendees.tsx +++ b/src/features/Events/EventPreview/EventPreviewAttendees.tsx @@ -1,3 +1,4 @@ +import { useAttendeesFreeBusy } from "@/components/Attendees/useFreeBusy"; import { renderAttendeeBadge } from "@/components/Event/utils/eventUtils"; import { AvatarGroup, Box, Typography } from "@linagora/twake-mui"; import PeopleAltOutlinedIcon from "@mui/icons-material/PeopleAltOutlined"; @@ -11,6 +12,10 @@ interface EventPreviewAttendeesProps { attendees: userAttendee[]; organizer: userAttendee | undefined; allAttendees: userAttendee[]; + start?: string; + end?: string; + timezone?: string; + eventUid?: string | null; } const ATTENDEE_DISPLAY_LIMIT = 3; @@ -19,6 +24,10 @@ export function EventPreviewAttendees({ attendees, organizer, allAttendees, + start, + end, + timezone, + eventUid, }: EventPreviewAttendeesProps) { const { t } = useI18n(); const theme = useTheme(); @@ -29,6 +38,26 @@ export function EventPreviewAttendees({ const attendeePreview = makeAttendeePreview(allAttendees, t); + const toFreeBusyAttendee = (a: userAttendee) => ({ + email: a.cal_address, + userId: null, + }); + + const freeBusyMap = useAttendeesFreeBusy({ + existingAttendees: allAttendees.map(toFreeBusyAttendee), + newAttendees: [], + start: start ?? "", + end: end ?? "", + timezone: timezone ?? "UTC", + eventUid, + enabled: !!(start && end && showAllAttendees), + }); + + const busyCaption = (a: userAttendee) => + freeBusyMap[a.cal_address] === "busy" + ? t("event.freeBusy.busy") + : undefined; + return ( <> @@ -78,10 +107,24 @@ export function EventPreviewAttendees({ {showAllAttendees && organizer && - renderAttendeeBadge(organizer, "org", t, showAllAttendees, true)} + renderAttendeeBadge( + organizer, + "org", + t, + showAllAttendees, + true, + busyCaption(organizer) + )} {showAllAttendees && attendees.map((a, idx) => - renderAttendeeBadge(a, idx.toString(), t, showAllAttendees) + renderAttendeeBadge( + a, + idx.toString(), + t, + showAllAttendees, + false, + busyCaption(a) + ) )} ); diff --git a/src/features/Events/EventPreview/EventPreviewDetails.tsx b/src/features/Events/EventPreview/EventPreviewDetails.tsx index 04af345..41005e0 100644 --- a/src/features/Events/EventPreview/EventPreviewDetails.tsx +++ b/src/features/Events/EventPreview/EventPreviewDetails.tsx @@ -1,17 +1,13 @@ import { InfoRow } from "@/components/Event/InfoRow"; -import { renderAttendeeBadge } from "@/components/Event/utils/eventUtils"; -import { AvatarGroup, Box, Button, Typography } from "@linagora/twake-mui"; +import { Box, Button, Typography } from "@linagora/twake-mui"; import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline"; import LocationOnOutlinedIcon from "@mui/icons-material/LocationOnOutlined"; import NotificationsNoneIcon from "@mui/icons-material/NotificationsNone"; -import PeopleAltOutlinedIcon from "@mui/icons-material/PeopleAltOutlined"; import RepeatIcon from "@mui/icons-material/Repeat"; import SubjectIcon from "@mui/icons-material/Subject"; import VideocamOutlinedIcon from "@mui/icons-material/VideocamOutlined"; import { alpha, useTheme } from "@mui/material/styles"; -import { useState } from "react"; import { useI18n } from "twake-i18n"; -import { makeAttendeePreview } from "."; import { CalendarEvent } from "../EventsTypes"; import { EventPreviewAttendees } from "./EventPreviewAttendees"; import { makeRecurrenceString } from "./utils/makeRecurrenceString"; @@ -20,35 +16,26 @@ interface EventPreviewDetailsProps { event: CalendarEvent; isOwn: boolean; isNotPrivate: boolean; - userEmail: string; } -const ATTENDEE_DISPLAY_LIMIT = 3; - export function EventPreviewDetails({ event, isOwn, isNotPrivate, - userEmail, }: EventPreviewDetailsProps) { const { t } = useI18n(); const theme = useTheme(); const infoIconColor = alpha(theme.palette.grey[900], 0.9); const infoIconSx = { minWidth: "25px", marginRight: 2, color: infoIconColor }; - const [showAllAttendees, setShowAllAttendees] = useState(false); - const attendees = event.attendee?.filter( (a) => a.cal_address !== event.organizer?.cal_address ) || []; - const organizer = event.attendee?.find( (a) => a.cal_address === event.organizer?.cal_address ); - - const attendeePreview = makeAttendeePreview(event.attendee, t); - const showDetails = (isNotPrivate && !isOwn) || isOwn; + const showDetails = isNotPrivate || isOwn; if (!showDetails) { return ( @@ -84,7 +71,13 @@ export function EventPreviewDetails({