Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { useI18n } from "twake-i18n";
|
||||
import { Calendar } from "../../Calendars/CalendarTypes";
|
||||
import { userData } from "../../User/userDataTypes";
|
||||
import { ContextualizedEvent } from "../EventsTypes";
|
||||
import { RSVPButton } from "./RSVPButton";
|
||||
|
||||
interface AttendanceValidationProps {
|
||||
contextualizedEvent: ContextualizedEvent;
|
||||
calendarList: Calendar[];
|
||||
user: userData | undefined;
|
||||
setAfterChoiceFunc: Dispatch<SetStateAction<Function | undefined>>;
|
||||
setOpenEditModePopup: Dispatch<SetStateAction<string | null>>;
|
||||
}
|
||||
|
||||
export function AttendanceValidation({
|
||||
contextualizedEvent,
|
||||
calendarList,
|
||||
user,
|
||||
setAfterChoiceFunc,
|
||||
setOpenEditModePopup,
|
||||
}: AttendanceValidationProps) {
|
||||
const { currentUserAttendee, isOwn } = contextualizedEvent;
|
||||
const { t } = useI18n();
|
||||
|
||||
// Check if we should show RSVP buttons
|
||||
const hasNoAttendeesOrOrganizer =
|
||||
!(contextualizedEvent.event?.attendee?.length > 0) &&
|
||||
!contextualizedEvent.event?.organizer;
|
||||
|
||||
if (!((currentUserAttendee || hasNoAttendeesOrOrganizer) && isOwn)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const commonButtonProps = {
|
||||
contextualizedEvent,
|
||||
user,
|
||||
calendarList,
|
||||
setAfterChoiceFunc,
|
||||
setOpenEditModePopup,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography sx={{ marginRight: 2 }}>
|
||||
{t("eventPreview.attendingQuestion")}
|
||||
</Typography>
|
||||
<Box display="flex" gap="15px" alignItems="center">
|
||||
<RSVPButton rsvpValue="ACCEPTED" {...commonButtonProps} />
|
||||
<RSVPButton rsvpValue="TENTATIVE" {...commonButtonProps} />
|
||||
<RSVPButton rsvpValue="DECLINED" {...commonButtonProps} />
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Button } from "@mui/material";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { useI18n } from "twake-i18n";
|
||||
import { useAppDispatch } from "../../../app/hooks";
|
||||
import { Calendar } from "../../Calendars/CalendarTypes";
|
||||
import { PartStat } from "../../User/models/attendee";
|
||||
import { userData } from "../../User/userDataTypes";
|
||||
import { ContextualizedEvent } from "../EventsTypes";
|
||||
import { handleRSVPClick } from "./handleRSVPClick";
|
||||
|
||||
const rsvpColor: Record<PartStat, "success" | "error" | "warning" | "primary"> =
|
||||
{
|
||||
ACCEPTED: "success",
|
||||
DECLINED: "error",
|
||||
TENTATIVE: "warning",
|
||||
"NEEDS-ACTION": "primary",
|
||||
} as const;
|
||||
|
||||
interface RSVPButtonProps {
|
||||
rsvpValue: PartStat;
|
||||
contextualizedEvent: ContextualizedEvent;
|
||||
user: userData | undefined;
|
||||
calendarList: Calendar[];
|
||||
setAfterChoiceFunc: Dispatch<SetStateAction<Function | undefined>>;
|
||||
setOpenEditModePopup: Dispatch<SetStateAction<string | null>>;
|
||||
}
|
||||
|
||||
export function RSVPButton({
|
||||
rsvpValue,
|
||||
contextualizedEvent,
|
||||
user,
|
||||
calendarList,
|
||||
setAfterChoiceFunc,
|
||||
setOpenEditModePopup,
|
||||
}: RSVPButtonProps) {
|
||||
const { t } = useI18n();
|
||||
const dispatch = useAppDispatch();
|
||||
const { currentUserAttendee } = contextualizedEvent;
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant={
|
||||
currentUserAttendee?.partstat === rsvpValue ? "contained" : "outlined"
|
||||
}
|
||||
color={
|
||||
currentUserAttendee?.partstat === rsvpValue
|
||||
? rsvpColor[rsvpValue]
|
||||
: "primary"
|
||||
}
|
||||
size="large"
|
||||
sx={{ borderRadius: "50px" }}
|
||||
onClick={() =>
|
||||
handleRSVPClick(
|
||||
rsvpValue,
|
||||
contextualizedEvent,
|
||||
user,
|
||||
calendarList,
|
||||
setAfterChoiceFunc,
|
||||
setOpenEditModePopup,
|
||||
dispatch
|
||||
)
|
||||
}
|
||||
>
|
||||
{t(`eventPreview.${rsvpValue}`)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { AppDispatch } from "../../../app/store";
|
||||
import { handleRSVP } from "../../../components/Event/eventHandlers/eventHandlers";
|
||||
import { Calendar } from "../../Calendars/CalendarTypes";
|
||||
import { PartStat } from "../../User/models/attendee";
|
||||
import { userData } from "../../User/userDataTypes";
|
||||
import { ContextualizedEvent } from "../EventsTypes";
|
||||
|
||||
export async function handleRSVPClick(
|
||||
rsvp: PartStat,
|
||||
contextualizedEvent: ContextualizedEvent,
|
||||
user: userData | undefined,
|
||||
calendarList: Calendar[],
|
||||
setAfterChoiceFunc: Dispatch<SetStateAction<Function | undefined>>,
|
||||
setOpenEditModePopup: Dispatch<SetStateAction<string | null>>,
|
||||
dispatch: AppDispatch
|
||||
) {
|
||||
const { isRecurring, calendar, event } = contextualizedEvent;
|
||||
if (isRecurring) {
|
||||
setAfterChoiceFunc(() => async (type: string) => {
|
||||
try {
|
||||
await handleRSVP(
|
||||
dispatch,
|
||||
calendar,
|
||||
user,
|
||||
event,
|
||||
rsvp,
|
||||
type,
|
||||
calendarList
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error handling RSVP:", error);
|
||||
}
|
||||
});
|
||||
setOpenEditModePopup("attendance");
|
||||
} else {
|
||||
try {
|
||||
await handleRSVP(dispatch, calendar, user, event, rsvp);
|
||||
} catch (error) {
|
||||
console.error("Error handling RSVP:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,10 +36,7 @@ import {
|
||||
import ResponsiveDialog from "../../components/Dialog/ResponsiveDialog";
|
||||
import { EditModeDialog } from "../../components/Event/EditModeDialog";
|
||||
import EventDuplication from "../../components/Event/EventDuplicate";
|
||||
import {
|
||||
handleDelete,
|
||||
handleRSVP,
|
||||
} from "../../components/Event/eventHandlers/eventHandlers";
|
||||
import { handleDelete } from "../../components/Event/eventHandlers/eventHandlers";
|
||||
import { InfoRow } from "../../components/Event/InfoRow";
|
||||
import { renderAttendeeBadge } from "../../components/Event/utils/eventUtils";
|
||||
import { getCalendarRange } from "../../utils/dateUtils";
|
||||
@@ -48,8 +45,12 @@ import { dlEvent } from "./EventApi";
|
||||
import { CalendarEvent } from "./EventsTypes";
|
||||
import EventUpdateModal from "./EventUpdateModal";
|
||||
import { useI18n } from "twake-i18n";
|
||||
import { userAttendee } from "../User/userDataTypes";
|
||||
import { userAttendee } from "../User/models/attendee";
|
||||
import { browserDefaultTimeZone } from "../../utils/timezone";
|
||||
import { AttendanceValidation } from "./AttendanceValidation/AttendanceValidation";
|
||||
import { Calendar } from "../Calendars/CalendarTypes";
|
||||
import { userData } from "../User/userDataTypes";
|
||||
import { createEventContext } from "./createEventContext";
|
||||
|
||||
export default function EventPreviewModal({
|
||||
eventId,
|
||||
@@ -77,12 +78,13 @@ export default function EventPreviewModal({
|
||||
? calendars.templist[calId]
|
||||
: calendars.list[calId];
|
||||
const event = calendar.events[eventId];
|
||||
const user = useAppSelector((state) => state.user);
|
||||
const user = useAppSelector((state) => state.user.userData);
|
||||
if (!user) return null;
|
||||
|
||||
const isRecurring = event?.uid?.includes("/");
|
||||
const isOwn = calendar.ownerEmails?.includes(user.userData?.email);
|
||||
const isOwn = calendar.ownerEmails?.includes(user.email);
|
||||
const isOrganizer = event.organizer
|
||||
? user.userData?.email === event.organizer.cal_address
|
||||
? user.email === event.organizer.cal_address
|
||||
: isOwn;
|
||||
const [showAllAttendees, setShowAllAttendees] = useState(false);
|
||||
const [openUpdateModal, setOpenUpdateModal] = useState(false);
|
||||
@@ -274,8 +276,9 @@ export default function EventPreviewModal({
|
||||
: attendees.slice(0, attendeeDisplayLimit);
|
||||
|
||||
const currentUserAttendee = event.attendee?.find(
|
||||
(person) => person.cal_address === user.userData?.email
|
||||
(person) => person.cal_address === user.email
|
||||
);
|
||||
const contextualizedEvent = createEventContext(event, calendar, user);
|
||||
|
||||
const organizer = event.attendee?.find(
|
||||
(a) => a.cal_address === event.organizer?.cal_address
|
||||
@@ -365,7 +368,7 @@ export default function EventPreviewModal({
|
||||
window.open(
|
||||
`${mailSpaUrl}/mailto/?uri=mailto:${event.attendee
|
||||
.map((a) => a.cal_address)
|
||||
.filter((mail) => mail !== user.userData?.email)
|
||||
.filter((mail) => mail !== user.email)
|
||||
.join(",")}?subject=${event.title}`
|
||||
)
|
||||
}
|
||||
@@ -467,147 +470,13 @@ export default function EventPreviewModal({
|
||||
</>
|
||||
}
|
||||
actions={
|
||||
currentUserAttendee &&
|
||||
isOwn && (
|
||||
<>
|
||||
<>
|
||||
<Typography sx={{ marginRight: 2 }}>
|
||||
{t("eventPreview.attendingQuestion")}
|
||||
</Typography>
|
||||
<Box display="flex" gap="15px" alignItems="center">
|
||||
<Button
|
||||
variant={
|
||||
currentUserAttendee?.partstat === "ACCEPTED"
|
||||
? "contained"
|
||||
: "outlined"
|
||||
}
|
||||
color={
|
||||
currentUserAttendee?.partstat === "ACCEPTED"
|
||||
? "success"
|
||||
: "primary"
|
||||
}
|
||||
size="large"
|
||||
sx={{ borderRadius: "50px" }}
|
||||
onClick={() => {
|
||||
if (isRecurring) {
|
||||
setAfterChoiceFunc(
|
||||
() => (type: string) =>
|
||||
handleRSVP(
|
||||
dispatch,
|
||||
calendar,
|
||||
user,
|
||||
event,
|
||||
"ACCEPTED",
|
||||
onClose,
|
||||
type,
|
||||
calendarList
|
||||
)
|
||||
);
|
||||
setOpenEditModePopup("attendance");
|
||||
} else {
|
||||
handleRSVP(
|
||||
dispatch,
|
||||
calendar,
|
||||
user,
|
||||
event,
|
||||
"ACCEPTED",
|
||||
onClose
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("eventPreview.accept")}
|
||||
</Button>
|
||||
<Button
|
||||
variant={
|
||||
currentUserAttendee?.partstat === "TENTATIVE"
|
||||
? "contained"
|
||||
: "outlined"
|
||||
}
|
||||
color={
|
||||
currentUserAttendee?.partstat === "TENTATIVE"
|
||||
? "warning"
|
||||
: "primary"
|
||||
}
|
||||
size="large"
|
||||
sx={{ borderRadius: "50px" }}
|
||||
onClick={() => {
|
||||
if (isRecurring) {
|
||||
setAfterChoiceFunc(
|
||||
() => (type: string) =>
|
||||
handleRSVP(
|
||||
dispatch,
|
||||
calendar,
|
||||
user,
|
||||
event,
|
||||
"TENTATIVE",
|
||||
onClose,
|
||||
type,
|
||||
calendarList
|
||||
)
|
||||
);
|
||||
setOpenEditModePopup("attendance");
|
||||
} else {
|
||||
handleRSVP(
|
||||
dispatch,
|
||||
calendar,
|
||||
user,
|
||||
event,
|
||||
"TENTATIVE",
|
||||
onClose
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("eventPreview.maybe")}
|
||||
</Button>
|
||||
<Button
|
||||
variant={
|
||||
currentUserAttendee?.partstat === "DECLINED"
|
||||
? "contained"
|
||||
: "outlined"
|
||||
}
|
||||
color={
|
||||
currentUserAttendee?.partstat === "DECLINED"
|
||||
? "error"
|
||||
: "primary"
|
||||
}
|
||||
size="large"
|
||||
sx={{ borderRadius: "50px" }}
|
||||
onClick={() => {
|
||||
if (isRecurring) {
|
||||
setAfterChoiceFunc(
|
||||
() => (type: string) =>
|
||||
handleRSVP(
|
||||
dispatch,
|
||||
calendar,
|
||||
user,
|
||||
event,
|
||||
"DECLINED",
|
||||
onClose,
|
||||
type,
|
||||
calendarList
|
||||
)
|
||||
);
|
||||
setOpenEditModePopup("attendance");
|
||||
} else {
|
||||
handleRSVP(
|
||||
dispatch,
|
||||
calendar,
|
||||
user,
|
||||
event,
|
||||
"DECLINED",
|
||||
onClose
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("eventPreview.decline")}
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
</>
|
||||
)
|
||||
<AttendanceValidation
|
||||
contextualizedEvent={contextualizedEvent}
|
||||
calendarList={calendarList}
|
||||
user={user}
|
||||
setAfterChoiceFunc={setAfterChoiceFunc}
|
||||
setOpenEditModePopup={setOpenEditModePopup}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{((event.class !== "PRIVATE" && !isOwn) || isOwn) && (
|
||||
|
||||
@@ -12,8 +12,8 @@ import React, {
|
||||
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
||||
import { ResponsiveDialog } from "../../components/Dialog";
|
||||
import { putEventAsync } from "../Calendars/CalendarSlice";
|
||||
import { Calendars } from "../Calendars/CalendarTypes";
|
||||
import { userAttendee } from "../User/userDataTypes";
|
||||
import { Calendar } from "../Calendars/CalendarTypes";
|
||||
import { userAttendee } from "../User/models/attendee";
|
||||
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
|
||||
import { createSelector } from "@reduxjs/toolkit";
|
||||
import { TIMEZONES } from "../../utils/timezone-data";
|
||||
@@ -75,11 +75,11 @@ function EventPopover({
|
||||
if (id.split("/")[0] === userId) {
|
||||
return calendars.list?.[id];
|
||||
}
|
||||
return {} as Calendars;
|
||||
return {} as Calendar;
|
||||
})
|
||||
.filter((calendar) => calendar.id)
|
||||
);
|
||||
const userPersonalCalendars: Calendars[] = useAppSelector(
|
||||
const userPersonalCalendars: Calendar[] = useAppSelector(
|
||||
selectPersonalCalendars
|
||||
);
|
||||
|
||||
@@ -722,10 +722,10 @@ function EventPopover({
|
||||
const newEventUID = crypto.randomUUID();
|
||||
|
||||
// Resolve target calendar safely
|
||||
const targetCalendar: Calendars | undefined =
|
||||
const targetCalendar: Calendar | undefined =
|
||||
calList[calendarid] ||
|
||||
userPersonalCalendars[0] ||
|
||||
(Object.values(calList)[0] as Calendars | undefined);
|
||||
(Object.values(calList)[0] as Calendar | undefined);
|
||||
if (!targetCalendar || !targetCalendar.id) {
|
||||
console.error("No target calendar available to save event");
|
||||
return;
|
||||
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
updateEventLocal,
|
||||
clearFetchCache,
|
||||
} from "../Calendars/CalendarSlice";
|
||||
import { Calendars } from "../Calendars/CalendarTypes";
|
||||
import { userAttendee } from "../User/userDataTypes";
|
||||
import { Calendar } from "../Calendars/CalendarTypes";
|
||||
import { userAttendee } from "../User/models/attendee";
|
||||
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
|
||||
import { TIMEZONES } from "../../utils/timezone-data";
|
||||
import { addVideoConferenceToDescription } from "../../utils/videoConferenceUtils";
|
||||
@@ -95,10 +95,10 @@ function EventUpdateModal({
|
||||
|
||||
const calendarsList = useAppSelector((state) => state.calendars.list);
|
||||
|
||||
const userPersonalCalendars: Calendars[] = useMemo(() => {
|
||||
const allCalendars = Object.values(calendarsList) as Calendars[];
|
||||
const userPersonalCalendars: Calendar[] = useMemo(() => {
|
||||
const allCalendars = Object.values(calendarsList) as Calendar[];
|
||||
return allCalendars.filter(
|
||||
(c: Calendars) => c.id?.split("/")[0] === user.userData?.openpaasId
|
||||
(c: Calendar) => c.id?.split("/")[0] === user.userData?.openpaasId
|
||||
);
|
||||
}, [calendarsList, user.userData?.openpaasId]);
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { userAttendee, userOrganiser } from "../User/userDataTypes";
|
||||
import { Calendar } from "../Calendars/CalendarTypes";
|
||||
import { userAttendee } from "../User/models/attendee";
|
||||
import { userOrganiser } from "../User/userDataTypes";
|
||||
|
||||
export interface CalendarEvent {
|
||||
URL: string;
|
||||
@@ -39,3 +41,12 @@ export interface AlarmObject {
|
||||
trigger: string;
|
||||
action: string;
|
||||
}
|
||||
|
||||
export interface ContextualizedEvent {
|
||||
event: CalendarEvent;
|
||||
calendar: Calendar;
|
||||
currentUserAttendee: userAttendee | undefined;
|
||||
isOwn: boolean;
|
||||
isRecurring: boolean;
|
||||
isOrganizer: boolean;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Calendar } from "../Calendars/CalendarTypes";
|
||||
import { userData } from "../User/userDataTypes";
|
||||
import { CalendarEvent, ContextualizedEvent } from "./EventsTypes";
|
||||
|
||||
export function createEventContext(
|
||||
event: CalendarEvent,
|
||||
calendar: Calendar,
|
||||
user: userData
|
||||
): ContextualizedEvent {
|
||||
const isOwn = calendar.ownerEmails?.includes(user.email) ?? false;
|
||||
const isRecurring = event?.uid?.includes("/") ?? false;
|
||||
const isOrganizer = event.organizer
|
||||
? user?.email === event.organizer.cal_address
|
||||
: isOwn;
|
||||
const currentUserAttendee = event.attendee?.find(
|
||||
(person) => person.cal_address === user.email
|
||||
);
|
||||
|
||||
return {
|
||||
event,
|
||||
calendar,
|
||||
currentUserAttendee,
|
||||
isOwn,
|
||||
isRecurring,
|
||||
isOrganizer,
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { userAttendee } from "../User/userDataTypes";
|
||||
import { userAttendee } from "../User/models/attendee";
|
||||
import { AlarmObject, CalendarEvent, RepetitionObject } from "./EventsTypes";
|
||||
import ICAL from "ical.js";
|
||||
import { TIMEZONES } from "../../utils/timezone-data";
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
convertFormDateTimeToISO,
|
||||
detectDateTimeFormat,
|
||||
} from "../../components/Event/utils/dateTimeHelpers";
|
||||
import { createAttendee } from "../User/models/attendee.mapper";
|
||||
type RawEntry = [string, Record<string, string>, string, any];
|
||||
|
||||
function resolveTimezoneId(tzid?: string): string | undefined {
|
||||
@@ -119,14 +120,16 @@ export function parseCalendarEvent(
|
||||
};
|
||||
break;
|
||||
case "attendee":
|
||||
(event.attendee as userAttendee[]).push({
|
||||
cn: params?.cn ?? "",
|
||||
cal_address: value.replace(/^mailto:/i, ""),
|
||||
partstat: params?.partstat ?? "",
|
||||
rsvp: params?.rsvp ?? "",
|
||||
role: params?.role ?? "",
|
||||
cutype: params?.cutype ?? "",
|
||||
});
|
||||
(event.attendee as userAttendee[]).push(
|
||||
createAttendee({
|
||||
cn: params?.cn,
|
||||
cal_address: value.replace(/^mailto:/i, ""),
|
||||
partstat: params?.partstat as userAttendee["partstat"],
|
||||
rsvp: params?.rsvp as userAttendee["rsvp"],
|
||||
role: params?.role as userAttendee["role"],
|
||||
cutype: params?.cutype as userAttendee["cutype"],
|
||||
})
|
||||
);
|
||||
break;
|
||||
case "dtstamp":
|
||||
event.stamp = value;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { TIMEZONES } from "../../utils/timezone-data";
|
||||
import { resolveTimezone } from "../../components/Calendar/TimezoneSelector";
|
||||
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
|
||||
import { userAttendee } from "../User/userDataTypes";
|
||||
import { userAttendee } from "../User/models/attendee";
|
||||
import { formatDateTimeInTimezone } from "../../components/Event/utils/dateTimeFormatters";
|
||||
import { addVideoConferenceToDescription } from "../../utils/videoConferenceUtils";
|
||||
import { browserDefaultTimeZone } from "../../utils/timezone";
|
||||
|
||||
Reference in New Issue
Block a user