Co-authored-by: lethemanh <lethemanh@lethemanhs-MacBook-Pro.local>
This commit is contained in:
@@ -43,6 +43,7 @@ import { userAttendee } from "../User/models/attendee";
|
||||
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
|
||||
import { useEventOrganizer } from "./useEventOrganizer";
|
||||
import { buildDelegatedEventURL } from "./utils/buildDelegatedEventURL";
|
||||
import { Resource } from "@/components/Attendees/ResourceSearch";
|
||||
|
||||
function EventPopover({
|
||||
open,
|
||||
@@ -86,6 +87,16 @@ function EventPopover({
|
||||
return resolveTimezone(tz);
|
||||
}, [calendarTimezone]);
|
||||
|
||||
const resources: Resource[] = useMemo(() => {
|
||||
const resourcesInEvent =
|
||||
event?.attendee?.filter((attendee) => attendee.cutype === "RESOURCE") ??
|
||||
[];
|
||||
return resourcesInEvent.map((resource) => ({
|
||||
email: resource.cal_address,
|
||||
displayName: resource.cn,
|
||||
}));
|
||||
}, [event?.attendee]);
|
||||
|
||||
const [showMore, setShowMore] = useState(false);
|
||||
const [showDescription, setShowDescription] = useState(
|
||||
event?.description ? true : false
|
||||
@@ -127,6 +138,7 @@ function EventPopover({
|
||||
const [repetition, setRepetition] = useState<RepetitionObject>(
|
||||
event?.repetition ?? ({} as RepetitionObject)
|
||||
);
|
||||
const [selectedResources, setSelectedResources] = useState(resources ?? []);
|
||||
|
||||
// Derive the effective organizer based on the selected calendar.
|
||||
// When a delegated calendar is selected, the organizer must be the
|
||||
@@ -139,11 +151,16 @@ function EventPopover({
|
||||
|
||||
const [attendees, setAttendees] = useState<userAttendee[]>(
|
||||
event?.attendee
|
||||
? event.attendee.filter((a) => a.cal_address !== organizer?.cal_address)
|
||||
? event.attendee.filter(
|
||||
(a) =>
|
||||
a.cal_address !== organizer?.cal_address && a.cutype !== "RESOURCE"
|
||||
)
|
||||
: []
|
||||
);
|
||||
const [alarm, setAlarm] = useState(event?.alarm?.trigger ?? "");
|
||||
const [eventClass, setEventClass] = useState(event?.class ?? "PUBLIC");
|
||||
const [eventClass, setEventClass] = useState<string>(
|
||||
event?.class ?? "PUBLIC"
|
||||
);
|
||||
const [busy, setBusy] = useState(event?.transp ?? "OPAQUE");
|
||||
const [timezone, setTimezone] = useState(
|
||||
event?.timezone ? resolveTimezone(event.timezone) : resolvedCalendarTimezone
|
||||
@@ -200,6 +217,7 @@ function EventPopover({
|
||||
setHasVideoConference(false);
|
||||
setMeetingLink(null);
|
||||
setHasEndDateChanged(false);
|
||||
setSelectedResources([]);
|
||||
}, [resolvedCalendarTimezone, defaultCalendarId]);
|
||||
|
||||
// Track if we should sync from selectedRange (only on initial selection, not on toggle)
|
||||
@@ -466,7 +484,9 @@ function EventPopover({
|
||||
setAttendees(
|
||||
event.attendee
|
||||
? event.attendee.filter(
|
||||
(a) => a.cal_address !== organizer?.cal_address
|
||||
(a) =>
|
||||
a.cal_address !== organizer?.cal_address &&
|
||||
a.cutype !== "RESOURCE"
|
||||
)
|
||||
: []
|
||||
);
|
||||
@@ -491,17 +511,23 @@ function EventPopover({
|
||||
setDescription(event.description);
|
||||
}
|
||||
}
|
||||
setSelectedResources(resources ?? []);
|
||||
} else if (event && event.attendee && event.attendee.length > 0) {
|
||||
// Handle tempEvent case (no uid but has attendees from temp calendar search)
|
||||
setAttendees(
|
||||
event.attendee.filter((a) => a.cal_address !== organizer?.cal_address)
|
||||
event.attendee.filter(
|
||||
(a) =>
|
||||
a.cal_address !== organizer?.cal_address && a.cutype !== "RESOURCE"
|
||||
)
|
||||
);
|
||||
setSelectedResources(resources ?? []);
|
||||
}
|
||||
}, [
|
||||
event,
|
||||
organizer?.cal_address,
|
||||
resolvedCalendarTimezone,
|
||||
defaultCalendarId,
|
||||
resources,
|
||||
]);
|
||||
|
||||
// Reset state when creating new event (event is empty object or undefined)
|
||||
@@ -535,6 +561,7 @@ function EventPopover({
|
||||
setHasVideoConference(false);
|
||||
setMeetingLink(null);
|
||||
setHasEndDateChanged(false);
|
||||
setSelectedResources([]);
|
||||
}
|
||||
|
||||
if (!isCreatingNew) {
|
||||
@@ -640,6 +667,7 @@ function EventPopover({
|
||||
resetAllStateToDefault();
|
||||
setStart("");
|
||||
setEnd("");
|
||||
setSelectedResources([]);
|
||||
shouldSyncFromRangeRef.current = true; // Reset for next time
|
||||
isCalendarIdUserSelectedRef.current = false; // Reset so next open gets fresh default
|
||||
};
|
||||
@@ -666,6 +694,7 @@ function EventPopover({
|
||||
showDescription,
|
||||
showRepeat,
|
||||
hasEndDateChanged,
|
||||
resources: selectedResources,
|
||||
};
|
||||
return buildEventFormTempData(formState);
|
||||
}, [
|
||||
@@ -688,6 +717,7 @@ function EventPopover({
|
||||
showDescription,
|
||||
showRepeat,
|
||||
hasEndDateChanged,
|
||||
selectedResources,
|
||||
]);
|
||||
|
||||
// Check for temp data when modal opens
|
||||
@@ -722,6 +752,7 @@ function EventPopover({
|
||||
setShowDescription,
|
||||
setShowRepeat,
|
||||
setHasEndDateChanged,
|
||||
setSelectedResources,
|
||||
});
|
||||
// Clear the error flag but keep data until successful save
|
||||
const updatedTempData = { ...tempData, fromError: false };
|
||||
@@ -766,7 +797,7 @@ function EventPopover({
|
||||
uid: newEventUID,
|
||||
description,
|
||||
location,
|
||||
class: eventClass,
|
||||
class: eventClass as "PUBLIC" | "PRIVATE" | "CONFIDENTIAL",
|
||||
repetition,
|
||||
organizer,
|
||||
timezone,
|
||||
@@ -787,6 +818,20 @@ function EventPopover({
|
||||
x_openpass_videoconference: meetingLink || undefined,
|
||||
};
|
||||
|
||||
// Map data of resources to attendee before creating event
|
||||
if (selectedResources?.length) {
|
||||
selectedResources.forEach((resource: Resource) => {
|
||||
newEvent.attendee.push({
|
||||
cn: resource?.displayName ?? "",
|
||||
cal_address: resource?.email ?? "",
|
||||
partstat: "NEEDS-ACTION",
|
||||
rsvp: "TRUE",
|
||||
role: "REQ-PARTICIPANT",
|
||||
cutype: "RESOURCE",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (allday) {
|
||||
const startDateOnly = (start || "").split("T")[0];
|
||||
const endDateOnlyUI = (end || start || "").split("T")[0];
|
||||
@@ -957,6 +1002,8 @@ function EventPopover({
|
||||
onValidationChange={setIsFormValid}
|
||||
showValidationErrors={showValidationErrors}
|
||||
onHasEndDateChangedChange={setHasEndDateChanged}
|
||||
setSelectedResources={setSelectedResources}
|
||||
selectedResources={selectedResources}
|
||||
/>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
|
||||
@@ -28,7 +28,9 @@ export function EventPreviewActionMenu({
|
||||
const mailSpaUrl = window.MAIL_SPA_URL ?? null;
|
||||
|
||||
const attendees = event.attendee ?? [];
|
||||
const otherAttendees = attendees.filter((a) => a.cal_address !== userEmail);
|
||||
const otherAttendees = attendees.filter(
|
||||
(a) => a.cal_address !== userEmail && a.cutype !== "RESOURCE"
|
||||
);
|
||||
|
||||
return (
|
||||
<Menu open={Boolean(anchorEl)} onClose={onClose} anchorEl={anchorEl}>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { InfoRow } from "@/components/Event/InfoRow";
|
||||
import { Box, Button, Typography } from "@linagora/twake-mui";
|
||||
import LayersOutlinedIcon from "@mui/icons-material/LayersOutlined";
|
||||
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
|
||||
import LocationOnOutlinedIcon from "@mui/icons-material/LocationOnOutlined";
|
||||
import NotificationsNoneIcon from "@mui/icons-material/NotificationsNone";
|
||||
@@ -11,6 +12,7 @@ import { useI18n } from "twake-i18n";
|
||||
import { CalendarEvent } from "../EventsTypes";
|
||||
import { EventPreviewAttendees } from "./EventPreviewAttendees";
|
||||
import { makeRecurrenceString } from "./utils/makeRecurrenceString";
|
||||
import { useMemo } from "react";
|
||||
|
||||
interface EventPreviewDetailsProps {
|
||||
event: CalendarEvent;
|
||||
@@ -28,13 +30,25 @@ export function EventPreviewDetails({
|
||||
const infoIconColor = alpha(theme.palette.grey[900], 0.9);
|
||||
const infoIconSx = { minWidth: "25px", marginRight: 2, color: infoIconColor };
|
||||
|
||||
const resources = useMemo(
|
||||
() => event?.attendee?.filter((attendee) => attendee.cutype === "RESOURCE"),
|
||||
[event?.attendee]
|
||||
);
|
||||
const eventAttendees = useMemo(
|
||||
() =>
|
||||
event?.attendee?.filter((attendee) => attendee.cutype !== "RESOURCE") ??
|
||||
[],
|
||||
[event?.attendee]
|
||||
);
|
||||
|
||||
const attendees =
|
||||
event.attendee?.filter(
|
||||
eventAttendees?.filter(
|
||||
(a) => a.cal_address !== event.organizer?.cal_address
|
||||
) || [];
|
||||
const organizer = event.attendee?.find(
|
||||
const organizer = eventAttendees?.find(
|
||||
(a) => a.cal_address === event.organizer?.cal_address
|
||||
);
|
||||
|
||||
const showDetails = isNotPrivate || isOwn;
|
||||
|
||||
if (!showDetails) {
|
||||
@@ -91,7 +105,7 @@ export function EventPreviewDetails({
|
||||
<EventPreviewAttendees
|
||||
attendees={attendees}
|
||||
organizer={organizer}
|
||||
allAttendees={event.attendee ?? []}
|
||||
allAttendees={eventAttendees ?? []}
|
||||
start={event.start}
|
||||
end={event.end}
|
||||
timezone={event.timezone}
|
||||
@@ -112,6 +126,56 @@ export function EventPreviewDetails({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Resource */}
|
||||
{resources && (
|
||||
<InfoRow
|
||||
alignItems="flex-start"
|
||||
icon={
|
||||
<Box sx={infoIconSx}>
|
||||
<LayersOutlinedIcon />
|
||||
</Box>
|
||||
}
|
||||
content={resources.map((resource, index) => (
|
||||
<Box
|
||||
sx={{
|
||||
marginRight: "5px",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="textPrimary"
|
||||
sx={{
|
||||
wordBreak: "break-word",
|
||||
whiteSpace: "pre-line",
|
||||
maxHeight: "33vh",
|
||||
overflowY: "auto",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{resource.cn}
|
||||
{index < resources.length - 1 ? "," : ""}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
wordBreak: "break-word",
|
||||
whiteSpace: "pre-line",
|
||||
overflowY: "auto",
|
||||
width: "100%",
|
||||
fontSize: "13px",
|
||||
color: "#717D96",
|
||||
}}
|
||||
>
|
||||
{t(`eventPreview.${resource.partstat}`)}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
style={{
|
||||
fontSize: "16px",
|
||||
fontFamily: "'Inter', sans-serif",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Description */}
|
||||
{event.description && (
|
||||
<InfoRow
|
||||
|
||||
@@ -44,6 +44,7 @@ import { deleteEvent, getEvent, putEvent } from "./EventApi";
|
||||
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
|
||||
import { moveEventBetweenCalendars } from "./updateEventHelpers/moveEventBetweenCalendars";
|
||||
import { detectRecurringEventChanges } from "./utils/detectRecurringEventChanges";
|
||||
import { Resource } from "@/components/Attendees/ResourceSearch";
|
||||
|
||||
function EventUpdateModal({
|
||||
eventId,
|
||||
@@ -91,6 +92,16 @@ function EventUpdateModal({
|
||||
return { zones, browserTz, getTimezoneOffset };
|
||||
}, []);
|
||||
|
||||
const resources: Resource[] = useMemo(() => {
|
||||
const resourcesInEvent =
|
||||
event?.attendee?.filter((attendee) => attendee.cutype === "RESOURCE") ??
|
||||
[];
|
||||
return resourcesInEvent.map((resource) => ({
|
||||
email: resource.cal_address,
|
||||
displayName: resource.cn,
|
||||
}));
|
||||
}, [event?.attendee]);
|
||||
|
||||
const [showMore, setShowMore] = useState(false);
|
||||
const [showDescription, setShowDescription] = useState(
|
||||
event?.description ? true : false
|
||||
@@ -129,6 +140,7 @@ function EventUpdateModal({
|
||||
const [isFormValid, setIsFormValid] = useState(false);
|
||||
const [showValidationErrors, setShowValidationErrors] = useState(false);
|
||||
const [hasEndDateChanged, setHasEndDateChanged] = useState(false);
|
||||
const [selectedResources, setSelectedResources] = useState(resources ?? []);
|
||||
|
||||
const resetAllStateToDefault = useCallback(() => {
|
||||
setShowMore(false);
|
||||
@@ -151,6 +163,7 @@ function EventUpdateModal({
|
||||
setTimezone(resolveTimezone(browserDefaultTimeZone));
|
||||
setHasVideoConference(false);
|
||||
setMeetingLink(null);
|
||||
setSelectedResources([]);
|
||||
}, [defaultCalendarId]);
|
||||
|
||||
// Prevent repeated initialization loops
|
||||
@@ -297,7 +310,8 @@ function EventUpdateModal({
|
||||
eventToDisplay.attendee
|
||||
? eventToDisplay.attendee.filter(
|
||||
(a: userAttendee) =>
|
||||
a.cal_address !== eventToDisplay.organizer?.cal_address
|
||||
a.cal_address !== eventToDisplay.organizer?.cal_address &&
|
||||
a.cutype !== "RESOURCE"
|
||||
)
|
||||
: []
|
||||
);
|
||||
@@ -335,6 +349,15 @@ function EventUpdateModal({
|
||||
setDescription(eventToDisplay.description);
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedResources(
|
||||
(eventToDisplay.attendee ?? [])
|
||||
.filter((a: userAttendee) => a.cutype === "RESOURCE")
|
||||
.map((a) => ({
|
||||
email: a.cal_address,
|
||||
displayName: a.cn,
|
||||
}))
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
@@ -345,6 +368,7 @@ function EventUpdateModal({
|
||||
calList,
|
||||
masterEvent,
|
||||
isLoadingMasterEvent,
|
||||
resources,
|
||||
]);
|
||||
|
||||
// Helper to close modal(s) - use onCloseAll if available to close preview modal too
|
||||
@@ -387,6 +411,7 @@ function EventUpdateModal({
|
||||
showDescription,
|
||||
showRepeat,
|
||||
hasEndDateChanged,
|
||||
resources: selectedResources,
|
||||
};
|
||||
const context: EventFormContext = {
|
||||
eventId,
|
||||
@@ -417,6 +442,7 @@ function EventUpdateModal({
|
||||
eventId,
|
||||
calId,
|
||||
typeOfAction,
|
||||
selectedResources,
|
||||
]);
|
||||
|
||||
// Check for temp data when modal opens
|
||||
@@ -454,6 +480,7 @@ function EventUpdateModal({
|
||||
setShowDescription,
|
||||
setShowRepeat,
|
||||
setHasEndDateChanged,
|
||||
setSelectedResources,
|
||||
});
|
||||
// Clear the error flag but keep data until successful save
|
||||
const updatedTempData = { ...tempData, fromError: false };
|
||||
@@ -612,6 +639,23 @@ function EventUpdateModal({
|
||||
x_openpass_videoconference: meetingLink || undefined,
|
||||
};
|
||||
|
||||
// Map data of resources to attendee before creating event
|
||||
if (selectedResources?.length) {
|
||||
if (!newEvent.attendee) {
|
||||
newEvent.attendee = [];
|
||||
}
|
||||
selectedResources.forEach((resource: Resource) => {
|
||||
newEvent.attendee.push({
|
||||
cn: resource?.displayName ?? "",
|
||||
cal_address: resource?.email ?? "",
|
||||
partstat: "NEEDS-ACTION",
|
||||
rsvp: "TRUE",
|
||||
role: "REQ-PARTICIPANT",
|
||||
cutype: "RESOURCE",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Special case: When converting recurring event to non-recurring
|
||||
if (
|
||||
recurrenceId &&
|
||||
@@ -1088,6 +1132,8 @@ function EventUpdateModal({
|
||||
onValidationChange={setIsFormValid}
|
||||
showValidationErrors={showValidationErrors}
|
||||
onHasEndDateChangedChange={setHasEndDateChanged}
|
||||
selectedResources={selectedResources}
|
||||
setSelectedResources={setSelectedResources}
|
||||
/>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Resource } from "@/components/Attendees/ResourceSearch";
|
||||
import { Calendar } from "../Calendars/CalendarTypes";
|
||||
import { VObjectProperty } from "../Calendars/types/CalendarData";
|
||||
import { userAttendee } from "../User/models/attendee";
|
||||
@@ -29,6 +30,7 @@ export interface CalendarEvent {
|
||||
alarm?: AlarmObject;
|
||||
exdates?: string[];
|
||||
passthroughProps?: VObjectProperty[];
|
||||
selectedResources?: Resource[];
|
||||
}
|
||||
|
||||
export interface RepetitionObject {
|
||||
|
||||
Reference in New Issue
Block a user