[#625] added freebusy handling in event preview
This commit is contained in:
committed by
Benoit TELLIER
parent
f41c3cbd8d
commit
5269c43354
@@ -650,7 +650,7 @@ describe("Event Preview Display", () => {
|
|||||||
|
|
||||||
fireEvent.click(emailButton);
|
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);
|
expect(mockOpen).toHaveBeenCalledWith(expectedUrl);
|
||||||
});
|
});
|
||||||
@@ -721,7 +721,7 @@ describe("Event Preview Display", () => {
|
|||||||
|
|
||||||
fireEvent.click(emailButton);
|
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);
|
expect(mockOpen).toHaveBeenCalledWith(expectedUrl);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ export function renderAttendeeBadge(
|
|||||||
key: string,
|
key: string,
|
||||||
t: (key: string) => string,
|
t: (key: string) => string,
|
||||||
isFull?: boolean,
|
isFull?: boolean,
|
||||||
isOrganizer?: boolean
|
isOrganizer?: boolean,
|
||||||
|
caption?: string
|
||||||
) {
|
) {
|
||||||
const classIcon =
|
const classIcon =
|
||||||
a.partstat === "ACCEPTED" ? (
|
a.partstat === "ACCEPTED" ? (
|
||||||
@@ -65,12 +66,19 @@ export function renderAttendeeBadge(
|
|||||||
<Avatar {...stringAvatar(a.cn || a.cal_address)} />
|
<Avatar {...stringAvatar(a.cn || a.cal_address)} />
|
||||||
</Badge>
|
</Badge>
|
||||||
<Box style={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
|
<Box style={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
|
||||||
<Typography noWrap>{a.cn || a.cal_address}</Typography>
|
<Typography variant="body2" noWrap>
|
||||||
|
{a.cn || a.cal_address}
|
||||||
|
</Typography>
|
||||||
{isOrganizer && (
|
{isOrganizer && (
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
{t("event.organizer")}
|
{t("event.organizer")}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
{caption && (
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{caption}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -36,9 +36,9 @@ export function EventPreviewActionMenu({
|
|||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
window.open(
|
window.open(
|
||||||
`${mailSpaUrl}/mailto/?uri=mailto:${otherAttendees
|
`${mailSpaUrl}/mailto/?uri=${encodeURIComponent(
|
||||||
.map((a) => a.cal_address)
|
`mailto:${otherAttendees.map((a) => a.cal_address).join(",")}`
|
||||||
.join(",")}&subject=${encodeURIComponent(event.title ?? "")}`
|
)}&subject=${encodeURIComponent(event.title ?? "")}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useAttendeesFreeBusy } from "@/components/Attendees/useFreeBusy";
|
||||||
import { renderAttendeeBadge } from "@/components/Event/utils/eventUtils";
|
import { renderAttendeeBadge } from "@/components/Event/utils/eventUtils";
|
||||||
import { AvatarGroup, Box, Typography } from "@linagora/twake-mui";
|
import { AvatarGroup, Box, Typography } from "@linagora/twake-mui";
|
||||||
import PeopleAltOutlinedIcon from "@mui/icons-material/PeopleAltOutlined";
|
import PeopleAltOutlinedIcon from "@mui/icons-material/PeopleAltOutlined";
|
||||||
@@ -11,6 +12,10 @@ interface EventPreviewAttendeesProps {
|
|||||||
attendees: userAttendee[];
|
attendees: userAttendee[];
|
||||||
organizer: userAttendee | undefined;
|
organizer: userAttendee | undefined;
|
||||||
allAttendees: userAttendee[];
|
allAttendees: userAttendee[];
|
||||||
|
start?: string;
|
||||||
|
end?: string;
|
||||||
|
timezone?: string;
|
||||||
|
eventUid?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ATTENDEE_DISPLAY_LIMIT = 3;
|
const ATTENDEE_DISPLAY_LIMIT = 3;
|
||||||
@@ -19,6 +24,10 @@ export function EventPreviewAttendees({
|
|||||||
attendees,
|
attendees,
|
||||||
organizer,
|
organizer,
|
||||||
allAttendees,
|
allAttendees,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
timezone,
|
||||||
|
eventUid,
|
||||||
}: EventPreviewAttendeesProps) {
|
}: EventPreviewAttendeesProps) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@@ -29,6 +38,26 @@ export function EventPreviewAttendees({
|
|||||||
|
|
||||||
const attendeePreview = makeAttendeePreview(allAttendees, t);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box display="flex" alignItems="flex-start">
|
<Box display="flex" alignItems="flex-start">
|
||||||
@@ -78,10 +107,24 @@ export function EventPreviewAttendees({
|
|||||||
|
|
||||||
{showAllAttendees &&
|
{showAllAttendees &&
|
||||||
organizer &&
|
organizer &&
|
||||||
renderAttendeeBadge(organizer, "org", t, showAllAttendees, true)}
|
renderAttendeeBadge(
|
||||||
|
organizer,
|
||||||
|
"org",
|
||||||
|
t,
|
||||||
|
showAllAttendees,
|
||||||
|
true,
|
||||||
|
busyCaption(organizer)
|
||||||
|
)}
|
||||||
{showAllAttendees &&
|
{showAllAttendees &&
|
||||||
attendees.map((a, idx) =>
|
attendees.map((a, idx) =>
|
||||||
renderAttendeeBadge(a, idx.toString(), t, showAllAttendees)
|
renderAttendeeBadge(
|
||||||
|
a,
|
||||||
|
idx.toString(),
|
||||||
|
t,
|
||||||
|
showAllAttendees,
|
||||||
|
false,
|
||||||
|
busyCaption(a)
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
import { InfoRow } from "@/components/Event/InfoRow";
|
import { InfoRow } from "@/components/Event/InfoRow";
|
||||||
import { renderAttendeeBadge } from "@/components/Event/utils/eventUtils";
|
import { Box, Button, Typography } from "@linagora/twake-mui";
|
||||||
import { AvatarGroup, Box, Button, Typography } from "@linagora/twake-mui";
|
|
||||||
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
|
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
|
||||||
import LocationOnOutlinedIcon from "@mui/icons-material/LocationOnOutlined";
|
import LocationOnOutlinedIcon from "@mui/icons-material/LocationOnOutlined";
|
||||||
import NotificationsNoneIcon from "@mui/icons-material/NotificationsNone";
|
import NotificationsNoneIcon from "@mui/icons-material/NotificationsNone";
|
||||||
import PeopleAltOutlinedIcon from "@mui/icons-material/PeopleAltOutlined";
|
|
||||||
import RepeatIcon from "@mui/icons-material/Repeat";
|
import RepeatIcon from "@mui/icons-material/Repeat";
|
||||||
import SubjectIcon from "@mui/icons-material/Subject";
|
import SubjectIcon from "@mui/icons-material/Subject";
|
||||||
import VideocamOutlinedIcon from "@mui/icons-material/VideocamOutlined";
|
import VideocamOutlinedIcon from "@mui/icons-material/VideocamOutlined";
|
||||||
import { alpha, useTheme } from "@mui/material/styles";
|
import { alpha, useTheme } from "@mui/material/styles";
|
||||||
import { useState } from "react";
|
|
||||||
import { useI18n } from "twake-i18n";
|
import { useI18n } from "twake-i18n";
|
||||||
import { makeAttendeePreview } from ".";
|
|
||||||
import { CalendarEvent } from "../EventsTypes";
|
import { CalendarEvent } from "../EventsTypes";
|
||||||
import { EventPreviewAttendees } from "./EventPreviewAttendees";
|
import { EventPreviewAttendees } from "./EventPreviewAttendees";
|
||||||
import { makeRecurrenceString } from "./utils/makeRecurrenceString";
|
import { makeRecurrenceString } from "./utils/makeRecurrenceString";
|
||||||
@@ -20,35 +16,26 @@ interface EventPreviewDetailsProps {
|
|||||||
event: CalendarEvent;
|
event: CalendarEvent;
|
||||||
isOwn: boolean;
|
isOwn: boolean;
|
||||||
isNotPrivate: boolean;
|
isNotPrivate: boolean;
|
||||||
userEmail: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ATTENDEE_DISPLAY_LIMIT = 3;
|
|
||||||
|
|
||||||
export function EventPreviewDetails({
|
export function EventPreviewDetails({
|
||||||
event,
|
event,
|
||||||
isOwn,
|
isOwn,
|
||||||
isNotPrivate,
|
isNotPrivate,
|
||||||
userEmail,
|
|
||||||
}: EventPreviewDetailsProps) {
|
}: EventPreviewDetailsProps) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const infoIconColor = alpha(theme.palette.grey[900], 0.9);
|
const infoIconColor = alpha(theme.palette.grey[900], 0.9);
|
||||||
const infoIconSx = { minWidth: "25px", marginRight: 2, color: infoIconColor };
|
const infoIconSx = { minWidth: "25px", marginRight: 2, color: infoIconColor };
|
||||||
|
|
||||||
const [showAllAttendees, setShowAllAttendees] = useState(false);
|
|
||||||
|
|
||||||
const attendees =
|
const attendees =
|
||||||
event.attendee?.filter(
|
event.attendee?.filter(
|
||||||
(a) => a.cal_address !== event.organizer?.cal_address
|
(a) => a.cal_address !== event.organizer?.cal_address
|
||||||
) || [];
|
) || [];
|
||||||
|
|
||||||
const organizer = event.attendee?.find(
|
const organizer = event.attendee?.find(
|
||||||
(a) => a.cal_address === event.organizer?.cal_address
|
(a) => a.cal_address === event.organizer?.cal_address
|
||||||
);
|
);
|
||||||
|
const showDetails = isNotPrivate || isOwn;
|
||||||
const attendeePreview = makeAttendeePreview(event.attendee, t);
|
|
||||||
const showDetails = (isNotPrivate && !isOwn) || isOwn;
|
|
||||||
|
|
||||||
if (!showDetails) {
|
if (!showDetails) {
|
||||||
return (
|
return (
|
||||||
@@ -84,7 +71,13 @@ export function EventPreviewDetails({
|
|||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
size="medium"
|
size="medium"
|
||||||
onClick={() => window.open(event.x_openpass_videoconference)}
|
onClick={() =>
|
||||||
|
window.open(
|
||||||
|
event.x_openpass_videoconference,
|
||||||
|
"_blank",
|
||||||
|
"noopener,noreferrer"
|
||||||
|
)
|
||||||
|
}
|
||||||
sx={{ borderRadius: "4px" }}
|
sx={{ borderRadius: "4px" }}
|
||||||
>
|
>
|
||||||
{t("eventPreview.joinVideo")}
|
{t("eventPreview.joinVideo")}
|
||||||
@@ -98,7 +91,11 @@ export function EventPreviewDetails({
|
|||||||
<EventPreviewAttendees
|
<EventPreviewAttendees
|
||||||
attendees={attendees}
|
attendees={attendees}
|
||||||
organizer={organizer}
|
organizer={organizer}
|
||||||
allAttendees={event.attendee}
|
allAttendees={event.attendee ?? []}
|
||||||
|
start={event.start}
|
||||||
|
end={event.end}
|
||||||
|
timezone={event.timezone}
|
||||||
|
eventUid={event.uid}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { dlEvent } from "../EventApi";
|
|
||||||
import { CalendarEvent } from "../EventsTypes";
|
|
||||||
import { Box, IconButton } from "@linagora/twake-mui";
|
import { Box, IconButton } from "@linagora/twake-mui";
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
import EditIcon from "@mui/icons-material/Edit";
|
import EditIcon from "@mui/icons-material/Edit";
|
||||||
import FileDownloadOutlinedIcon from "@mui/icons-material/FileDownloadOutlined";
|
import FileDownloadOutlinedIcon from "@mui/icons-material/FileDownloadOutlined";
|
||||||
import MoreVertIcon from "@mui/icons-material/MoreVert";
|
import MoreVertIcon from "@mui/icons-material/MoreVert";
|
||||||
|
import { dlEvent } from "../EventApi";
|
||||||
|
import { CalendarEvent } from "../EventsTypes";
|
||||||
|
|
||||||
interface EventPreviewHeaderProps {
|
interface EventPreviewHeaderProps {
|
||||||
event: CalendarEvent;
|
event: CalendarEvent;
|
||||||
@@ -13,7 +13,6 @@ interface EventPreviewHeaderProps {
|
|||||||
isOwn: boolean;
|
isOwn: boolean;
|
||||||
isWriteDelegated: boolean;
|
isWriteDelegated: boolean;
|
||||||
isNotPrivate: boolean;
|
isNotPrivate: boolean;
|
||||||
isRecurring: boolean;
|
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onEdit: () => void;
|
onEdit: () => void;
|
||||||
onMoreClick: (e: React.MouseEvent<HTMLElement>) => void;
|
onMoreClick: (e: React.MouseEvent<HTMLElement>) => void;
|
||||||
@@ -26,13 +25,12 @@ export function EventPreviewHeader({
|
|||||||
isOwn,
|
isOwn,
|
||||||
isWriteDelegated,
|
isWriteDelegated,
|
||||||
isNotPrivate,
|
isNotPrivate,
|
||||||
isRecurring,
|
|
||||||
onClose,
|
onClose,
|
||||||
onEdit,
|
onEdit,
|
||||||
onMoreClick,
|
onMoreClick,
|
||||||
}: EventPreviewHeaderProps) {
|
}: EventPreviewHeaderProps) {
|
||||||
const canEdit = isOrganizer && (isOwn || (isWriteDelegated && isNotPrivate));
|
const canEdit = isOrganizer && (isOwn || (isWriteDelegated && isNotPrivate));
|
||||||
const canSeeMore = (isNotPrivate && !isOwn) || isOwn;
|
const canSeeMore = isNotPrivate || isOwn;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useAppSelector } from "@/app/hooks";
|
|
||||||
import { CalendarName } from "@/components/Calendar/CalendarName";
|
import { CalendarName } from "@/components/Calendar/CalendarName";
|
||||||
import { formatEventChipTitle } from "@/components/Calendar/utils/calendarUtils";
|
import { formatEventChipTitle } from "@/components/Calendar/utils/calendarUtils";
|
||||||
import ResponsiveDialog from "@/components/Dialog/ResponsiveDialog";
|
import ResponsiveDialog from "@/components/Dialog/ResponsiveDialog";
|
||||||
import { EditModeDialog } from "@/components/Event/EditModeDialog";
|
import { EditModeDialog } from "@/components/Event/EditModeDialog";
|
||||||
import { browserDefaultTimeZone, getTimezoneOffset } from "@/utils/timezone";
|
import { getTimezoneOffset } from "@/utils/timezone";
|
||||||
import { DateSelectArg } from "@fullcalendar/core";
|
import { DateSelectArg } from "@fullcalendar/core";
|
||||||
import { Box, Chip, Tooltip, Typography } from "@linagora/twake-mui";
|
import { Box, Chip, Tooltip, Typography } from "@linagora/twake-mui";
|
||||||
import CircleIcon from "@mui/icons-material/Circle";
|
import CircleIcon from "@mui/icons-material/Circle";
|
||||||
@@ -12,13 +11,13 @@ import { useEffect } from "react";
|
|||||||
import { useI18n } from "twake-i18n";
|
import { useI18n } from "twake-i18n";
|
||||||
import { AttendanceValidation } from "../AttendanceValidation/AttendanceValidation";
|
import { AttendanceValidation } from "../AttendanceValidation/AttendanceValidation";
|
||||||
import EventPopover from "../EventModal";
|
import EventPopover from "../EventModal";
|
||||||
import EventUpdateModal from "../EventUpdateModal";
|
|
||||||
import { EventPreviewActionMenu } from "../EventPreview/EventPreviewActionMenu";
|
import { EventPreviewActionMenu } from "../EventPreview/EventPreviewActionMenu";
|
||||||
import { EventPreviewDetails } from "../EventPreview/EventPreviewDetails";
|
import { EventPreviewDetails } from "../EventPreview/EventPreviewDetails";
|
||||||
import { EventPreviewHeader } from "../EventPreview/EventPreviewHeader";
|
import { EventPreviewHeader } from "../EventPreview/EventPreviewHeader";
|
||||||
import { useEventPreviewState } from "../EventPreview/useEventPreviewState";
|
import { useEventPreviewState } from "../EventPreview/useEventPreviewState";
|
||||||
import { formatEnd } from "./utils/formatEnd";
|
import EventUpdateModal from "../EventUpdateModal";
|
||||||
import { formatDate } from "./utils/formatDate";
|
import { formatDate } from "./utils/formatDate";
|
||||||
|
import { formatEnd } from "./utils/formatEnd";
|
||||||
|
|
||||||
export default function EventPreviewModal({
|
export default function EventPreviewModal({
|
||||||
eventId,
|
eventId,
|
||||||
@@ -42,7 +41,6 @@ export default function EventPreviewModal({
|
|||||||
timezone,
|
timezone,
|
||||||
contextualizedEvent,
|
contextualizedEvent,
|
||||||
attendanceUser,
|
attendanceUser,
|
||||||
isRecurring,
|
|
||||||
isOwn,
|
isOwn,
|
||||||
isWriteDelegated,
|
isWriteDelegated,
|
||||||
isOrganizer,
|
isOrganizer,
|
||||||
@@ -57,7 +55,6 @@ export default function EventPreviewModal({
|
|||||||
setToggleActionMenu,
|
setToggleActionMenu,
|
||||||
openEditModePopup,
|
openEditModePopup,
|
||||||
setOpenEditModePopup,
|
setOpenEditModePopup,
|
||||||
typeOfAction,
|
|
||||||
setTypeOfAction,
|
setTypeOfAction,
|
||||||
afterChoiceFunc,
|
afterChoiceFunc,
|
||||||
setAfterChoiceFunc,
|
setAfterChoiceFunc,
|
||||||
@@ -67,11 +64,14 @@ export default function EventPreviewModal({
|
|||||||
handleDuplicateClick,
|
handleDuplicateClick,
|
||||||
} = useEventPreviewState(eventId, calId, tempEvent, open, onClose);
|
} = useEventPreviewState(eventId, calId, tempEvent, open, onClose);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(
|
||||||
if (!event || !calendar) {
|
() => {
|
||||||
onClose({}, "backdropClick");
|
if (open && (!event || !calendar)) {
|
||||||
}
|
onClose({}, "backdropClick");
|
||||||
}, [event, calendar, onClose]);
|
}
|
||||||
|
}, // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[open, event, calendar]
|
||||||
|
);
|
||||||
|
|
||||||
if (!user || !event || !calendar) return null;
|
if (!user || !event || !calendar) return null;
|
||||||
|
|
||||||
@@ -94,7 +94,6 @@ export default function EventPreviewModal({
|
|||||||
isOwn={isOwn}
|
isOwn={isOwn}
|
||||||
isWriteDelegated={isWriteDelegated}
|
isWriteDelegated={isWriteDelegated}
|
||||||
isNotPrivate={isNotPrivate}
|
isNotPrivate={isNotPrivate}
|
||||||
isRecurring={isRecurring}
|
|
||||||
onClose={() => onClose({}, "backdropClick")}
|
onClose={() => onClose({}, "backdropClick")}
|
||||||
onEdit={handleEditClick}
|
onEdit={handleEditClick}
|
||||||
onMoreClick={(e) => setToggleActionMenu(e.currentTarget)}
|
onMoreClick={(e) => setToggleActionMenu(e.currentTarget)}
|
||||||
@@ -164,7 +163,6 @@ export default function EventPreviewModal({
|
|||||||
event={event}
|
event={event}
|
||||||
isOwn={isOwn}
|
isOwn={isOwn}
|
||||||
isNotPrivate={isNotPrivate}
|
isNotPrivate={isNotPrivate}
|
||||||
userEmail={user.email}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Calendar label */}
|
{/* Calendar label */}
|
||||||
@@ -219,8 +217,8 @@ export default function EventPreviewModal({
|
|||||||
{
|
{
|
||||||
start: new Date(event.start),
|
start: new Date(event.start),
|
||||||
startStr: event.start,
|
startStr: event.start,
|
||||||
end: new Date(event.end ?? ""),
|
end: new Date(event.end ?? event.start),
|
||||||
endStr: event.end ?? "",
|
endStr: event.end ?? event.start,
|
||||||
allDay: event.allday ?? false,
|
allDay: event.allday ?? false,
|
||||||
} as DateSelectArg
|
} as DateSelectArg
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ export function useEventPreviewState(
|
|||||||
const result = await dispatch(
|
const result = await dispatch(
|
||||||
deleteEventAsync({ calId, eventId, eventURL: event.URL })
|
deleteEventAsync({ calId, eventId, eventURL: event.URL })
|
||||||
);
|
);
|
||||||
assertThunkSuccess(result);
|
await assertThunkSuccess(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to delete event:", error);
|
console.error("Failed to delete event:", error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,27 @@
|
|||||||
import { userAttendee } from "@/src/features/User/models/attendee";
|
import { userAttendee } from "@/features/User/models/attendee";
|
||||||
|
|
||||||
export function makeAttendeePreview(
|
export function makeAttendeePreview(
|
||||||
attendees: userAttendee[],
|
attendees: userAttendee[] | undefined,
|
||||||
t: (k: string, p?: string | object) => string
|
t: (k: string, p?: string | object) => string
|
||||||
) {
|
) {
|
||||||
const attendeePreview = [];
|
const attendeePreview = [];
|
||||||
const yesCount = attendees?.filter((a) => a.partstat === "ACCEPTED").length;
|
const counts = (attendees ?? []).reduce(
|
||||||
const noCount = attendees?.filter((a) => a.partstat === "DECLINED").length;
|
(acc, a) => {
|
||||||
const maybeCount = attendees?.filter(
|
if (a.partstat === "ACCEPTED") acc.yes++;
|
||||||
(a) => a.partstat === "TENTATIVE"
|
else if (a.partstat === "DECLINED") acc.no++;
|
||||||
).length;
|
else if (a.partstat === "TENTATIVE") acc.maybe++;
|
||||||
const needActionCount = attendees?.filter(
|
else if (a.partstat === "NEEDS-ACTION") acc.needAction++;
|
||||||
(a) => a.partstat === "NEEDS-ACTION"
|
return acc;
|
||||||
).length;
|
},
|
||||||
|
{ yes: 0, no: 0, maybe: 0, needAction: 0 }
|
||||||
|
);
|
||||||
|
const {
|
||||||
|
yes: yesCount,
|
||||||
|
no: noCount,
|
||||||
|
maybe: maybeCount,
|
||||||
|
needAction: needActionCount,
|
||||||
|
} = counts;
|
||||||
|
|
||||||
if (yesCount) {
|
if (yesCount) {
|
||||||
attendeePreview.push(t("eventPreview.yesCount", { count: yesCount }));
|
attendeePreview.push(t("eventPreview.yesCount", { count: yesCount }));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,21 @@
|
|||||||
import { api } from "@/utils/apiUtils";
|
import { api } from "@/utils/apiUtils";
|
||||||
|
import { extractEventBaseUuid } from "@/utils/extractEventBaseUuid";
|
||||||
|
|
||||||
|
interface BusySlot {
|
||||||
|
uid: string;
|
||||||
|
start: string;
|
||||||
|
end: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CalendarFreeBusy {
|
||||||
|
id: string;
|
||||||
|
busy: BusySlot[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserFreeBusy {
|
||||||
|
id: string;
|
||||||
|
calendars: CalendarFreeBusy[];
|
||||||
|
}
|
||||||
|
|
||||||
export async function getFreeBusyForEventAttendeesPOST(
|
export async function getFreeBusyForEventAttendeesPOST(
|
||||||
userIds: string[],
|
userIds: string[],
|
||||||
@@ -13,10 +30,16 @@ export async function getFreeBusyForEventAttendeesPOST(
|
|||||||
});
|
});
|
||||||
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||||
|
|
||||||
const { users } = (await r.json()) as {
|
const { users } = (await r.json()) as { users: UserFreeBusy[] };
|
||||||
users: { id: string; calendars: { busy: unknown[] }[] }[];
|
|
||||||
};
|
const eventUidBase = extractEventBaseUuid(eventUid);
|
||||||
|
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
users.map((u) => [u.id, u.calendars.some((cal) => cal.busy.length > 0)])
|
users.map((u) => {
|
||||||
|
const isBusy = u.calendars.some((cal) =>
|
||||||
|
cal.busy.some((slot) => extractEventBaseUuid(slot.uid) !== eventUidBase)
|
||||||
|
);
|
||||||
|
return [u.id, isBusy];
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user