fix: use main's EventDisplay.tsx and fix test imports
- Revert to main's EventDisplay.tsx (had wrong version from old commits) - Fix EventRepetition.test.tsx import path
This commit is contained in:
committed by
Benoit TELLIER
parent
ad56b71488
commit
6433086332
@@ -8,7 +8,7 @@ import {
|
||||
createEventHandlers,
|
||||
EventHandlersProps,
|
||||
} from "../../../src/components/Calendar/handlers/eventHandlers";
|
||||
import EventPreviewModal from "../../../src/components/Event/EventDisplayPreview";
|
||||
import EventPreviewModal from "../../../src/features/Events/EventDisplayPreview";
|
||||
|
||||
describe("Recurrence Event Behavior Tests", () => {
|
||||
const mockOnClose = jest.fn();
|
||||
|
||||
@@ -1,64 +1,74 @@
|
||||
import { useState } from "react";
|
||||
import CircleIcon from "@mui/icons-material/Circle";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
|
||||
import VideocamIcon from "@mui/icons-material/Videocam";
|
||||
import {
|
||||
deleteEventAsync,
|
||||
putEventAsync,
|
||||
removeEvent,
|
||||
} from "../Calendars/CalendarSlice";
|
||||
Box,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Card,
|
||||
CardActions,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
Checkbox,
|
||||
Divider,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
IconButton,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Modal,
|
||||
Select,
|
||||
SelectChangeEvent,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
||||
import AttendeeSelector from "../../components/Attendees/AttendeeSearch";
|
||||
import {
|
||||
Button,
|
||||
Box,
|
||||
Typography,
|
||||
ButtonGroup,
|
||||
Card,
|
||||
CardContent,
|
||||
Divider,
|
||||
IconButton,
|
||||
Avatar,
|
||||
Badge,
|
||||
Modal,
|
||||
CardHeader,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
SelectChangeEvent,
|
||||
CardActions,
|
||||
} from "@mui/material";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import VideocamIcon from "@mui/icons-material/Videocam";
|
||||
import CircleIcon from "@mui/icons-material/Circle";
|
||||
import { userAttendee } from "../User/userDataTypes";
|
||||
import { Calendars } from "../Calendars/CalendarTypes";
|
||||
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
|
||||
import { formatLocalDateTime } from "../../components/Event/EventFormFields";
|
||||
handleDelete,
|
||||
handleRSVP,
|
||||
} from "../../components/Event/eventHandlers/eventHandlers";
|
||||
import RepeatEvent from "../../components/Event/EventRepeat";
|
||||
import { InfoRow } from "../../components/Event/InfoRow";
|
||||
import { refreshCalendars } from "../../components/Event/utils/eventUtils";
|
||||
import { renderAttendeeBadge } from "../../components/Event/utils/eventUtils";
|
||||
import { getCalendarRange } from "../../utils/dateUtils";
|
||||
import {
|
||||
moveEventAsync,
|
||||
putEventAsync,
|
||||
removeEvent,
|
||||
updateEventInstanceAsync,
|
||||
updateSeriesAsync,
|
||||
} from "../Calendars/CalendarSlice";
|
||||
import { Calendars } from "../Calendars/CalendarTypes";
|
||||
import { userAttendee } from "../User/userDataTypes";
|
||||
import { getEvent } from "./EventApi";
|
||||
import { formatLocalDateTime } from "./EventModal";
|
||||
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
|
||||
|
||||
export default function EventDisplayModal({
|
||||
eventId,
|
||||
calId,
|
||||
open,
|
||||
onClose,
|
||||
eventData,
|
||||
typeOfAction,
|
||||
}: {
|
||||
eventId: string;
|
||||
calId: string;
|
||||
open: boolean;
|
||||
onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void;
|
||||
eventData?: CalendarEvent | null;
|
||||
typeOfAction?: "solo" | "all";
|
||||
}) {
|
||||
const dispatch = useAppDispatch();
|
||||
const calendar = useAppSelector((state) => state.calendars.list[calId]);
|
||||
const cachedEvent = useAppSelector(
|
||||
const event = useAppSelector(
|
||||
(state) => state.calendars.list[calId]?.events[eventId]
|
||||
);
|
||||
const user = useAppSelector((state) => state.user);
|
||||
|
||||
// Use eventData from props if available, otherwise use cached data
|
||||
const event = eventData || cachedEvent;
|
||||
|
||||
const [showAllAttendees, setShowAllAttendees] = useState(false);
|
||||
const [showMore, setShowMore] = useState(false);
|
||||
|
||||
@@ -71,91 +81,135 @@ export default function EventDisplayModal({
|
||||
);
|
||||
|
||||
// Form state
|
||||
const [title, setTitle] = useState(event?.title ?? "");
|
||||
const [description, setDescription] = useState(event?.description ?? "");
|
||||
const [location, setLocation] = useState(event?.location ?? "");
|
||||
const [start, setStart] = useState(
|
||||
formatLocalDateTime(new Date(event?.start ?? Date.now()))
|
||||
);
|
||||
const [end, setEnd] = useState(
|
||||
formatLocalDateTime(new Date(event?.end ?? Date.now()))
|
||||
);
|
||||
const [allday, setAllDay] = useState(event?.allday);
|
||||
const [repetition, setRepetition] = useState<RepetitionObject>(
|
||||
event?.repetition ?? ({} as RepetitionObject)
|
||||
);
|
||||
const [alarm, setAlarm] = useState(event?.alarm?.trigger ?? "");
|
||||
const [busy, setBusy] = useState(event?.transp ?? "OPAQUE");
|
||||
const [timezone] = useState(event?.timezone ?? "UTC");
|
||||
const [eventClass, setEventClass] = useState(event?.class ?? "PUBLIC");
|
||||
const [timezone, setTimezone] = useState(event?.timezone ?? "UTC");
|
||||
const [newCalId, setNewCalId] = useState(event?.calId);
|
||||
const [calendarid, setCalendarid] = useState(
|
||||
calId.split("/")[0] === user.userData?.openpaasId
|
||||
? userPersonnalCalendars.findIndex((cal) => cal.id === calId)
|
||||
: calendars.findIndex((cal) => cal.id === calId)
|
||||
);
|
||||
|
||||
const [attendees, setAttendees] = useState<userAttendee[]>(
|
||||
event?.attendee
|
||||
? event.attendee.filter(
|
||||
(a) => a.cal_address !== event.organizer?.cal_address
|
||||
const [attendees, setAttendees] = useState(
|
||||
(event?.attendee || []).filter(
|
||||
(a) => a.cal_address !== event?.organizer?.cal_address
|
||||
)
|
||||
: []
|
||||
);
|
||||
|
||||
const [showRepeat] = useState(event?.repetition?.freq ? true : false);
|
||||
|
||||
const isOwn = calId.split("/")[0] === user.userData?.openpaasId;
|
||||
const isOwnCal = calendar?.id?.split("/")[0] === user.userData?.openpaasId;
|
||||
|
||||
const currentUserAttendee = event?.attendee?.find(
|
||||
(a) => a.cal_address === user.userData?.email
|
||||
(person) => person.cal_address === user.userData.email
|
||||
);
|
||||
|
||||
const calList =
|
||||
calId.split("/")[0] === user.userData?.openpaasId
|
||||
? userPersonnalCalendars
|
||||
: calendars;
|
||||
const organizer =
|
||||
event?.attendee?.find(
|
||||
(a) => a.cal_address === event?.organizer?.cal_address
|
||||
) ?? ({} as userAttendee);
|
||||
|
||||
function handleRSVP(rsvp: string) {
|
||||
if (!event) return;
|
||||
const newEvent = { ...event };
|
||||
newEvent.attendee = newEvent.attendee.map((a) =>
|
||||
a.cal_address === user.userData?.email ? { ...a, partstat: rsvp } : a
|
||||
);
|
||||
dispatch(putEventAsync({ cal: calendar, newEvent }));
|
||||
const isOwn = organizer?.cal_address === user.userData.email;
|
||||
const isOwnCal = userPersonnalCalendars.find((cal) => cal.id === calId);
|
||||
const attendeeDisplayLimit = 3;
|
||||
|
||||
useEffect(() => {
|
||||
if (!event || !calendar) {
|
||||
onClose({}, "backdropClick");
|
||||
}
|
||||
setRepetition(event?.repetition ?? ({} as RepetitionObject));
|
||||
}, [open, eventId, dispatch, onClose, event]);
|
||||
useEffect(() => {
|
||||
const fetchMasterEvent = async () => {
|
||||
const masterEvent = await getEvent(event);
|
||||
|
||||
setTitle(masterEvent.title ?? "");
|
||||
setDescription(masterEvent.description ?? "");
|
||||
setLocation(masterEvent.location ?? "");
|
||||
setStart(formatLocalDateTime(new Date(masterEvent?.start ?? Date.now())));
|
||||
setEnd(formatLocalDateTime(new Date(masterEvent?.end ?? Date.now())));
|
||||
setAllDay(masterEvent.allday ?? false);
|
||||
setRepetition(masterEvent?.repetition ?? ({} as RepetitionObject));
|
||||
setAlarm(masterEvent?.alarm?.trigger ?? "");
|
||||
setBusy(masterEvent?.transp ?? "OPAQUE");
|
||||
setEventClass(masterEvent?.class ?? "PUBLIC");
|
||||
setTimezone(masterEvent.timezone ?? "UTC");
|
||||
};
|
||||
if (typeOfAction === "all") {
|
||||
fetchMasterEvent();
|
||||
}
|
||||
}, [typeOfAction, event]);
|
||||
|
||||
if (!event || !calendar) return null;
|
||||
const isRecurring = event.uid?.includes("/");
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!event) return;
|
||||
const newEventUID = crypto.randomUUID();
|
||||
|
||||
const newEvent: CalendarEvent = {
|
||||
calId,
|
||||
title: event.title,
|
||||
URL: event.URL ?? `/calendars/${calId}/${event.uid}.ics`,
|
||||
start: event.start,
|
||||
end: event.end,
|
||||
allday: event.allday,
|
||||
uid: event.uid,
|
||||
description: event.description,
|
||||
location: event.location,
|
||||
title,
|
||||
URL: event.URL ?? `/calendars/${calId}/${newEventUID}.ics`,
|
||||
start: new Date(start),
|
||||
end: new Date(end),
|
||||
allday,
|
||||
uid: event.uid ?? newEventUID,
|
||||
description,
|
||||
location,
|
||||
repetition,
|
||||
class: event.class,
|
||||
class: eventClass,
|
||||
organizer: event.organizer,
|
||||
timezone,
|
||||
attendee: event.organizer
|
||||
? [event.organizer as userAttendee, ...attendees]
|
||||
: attendees,
|
||||
attendee: [organizer, ...attendees],
|
||||
transp: busy,
|
||||
color: userPersonnalCalendars[calendarid]?.color,
|
||||
alarm: { trigger: alarm, action: "EMAIL" },
|
||||
};
|
||||
|
||||
const [baseId, recurrenceId] = event.uid.split("/");
|
||||
if (recurrenceId) {
|
||||
Object.keys(userPersonnalCalendars[calendarid].events).forEach(
|
||||
(element) => {
|
||||
if (element.split("/")[0] === baseId) {
|
||||
dispatch(removeEvent({ calendarUid: calId, eventUid: element }));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
await dispatch(
|
||||
putEventAsync({
|
||||
const calendarRange = getCalendarRange(new Date(start));
|
||||
|
||||
if (typeOfAction === "solo") {
|
||||
dispatch(
|
||||
updateEventInstanceAsync({
|
||||
cal: userPersonnalCalendars[calendarid],
|
||||
newEvent,
|
||||
event: { ...newEvent, recurrenceId: recurrenceId },
|
||||
})
|
||||
);
|
||||
} else if (typeOfAction === "all") {
|
||||
dispatch(
|
||||
updateSeriesAsync({
|
||||
cal: userPersonnalCalendars[calendarid],
|
||||
event: { ...newEvent, recurrenceId: recurrenceId },
|
||||
})
|
||||
);
|
||||
await refreshCalendars(dispatch, calendars, calendarRange);
|
||||
} else {
|
||||
dispatch(
|
||||
putEventAsync({ cal: userPersonnalCalendars[calendarid], newEvent })
|
||||
);
|
||||
}
|
||||
|
||||
if (newCalId !== calId) {
|
||||
dispatch(
|
||||
moveEventAsync({
|
||||
cal: userPersonnalCalendars[calendarid],
|
||||
newEvent,
|
||||
newURL: `/calendars/${newCalId}/${baseId}.ics`,
|
||||
})
|
||||
);
|
||||
dispatch(removeEvent({ calendarUid: calId, eventUid: event.uid }));
|
||||
}
|
||||
onClose({}, "backdropClick");
|
||||
};
|
||||
|
||||
@@ -163,108 +217,73 @@ export default function EventDisplayModal({
|
||||
setShowMore(!showMore);
|
||||
};
|
||||
|
||||
function getEventColor() {
|
||||
if (!event) return "#1976d2";
|
||||
const color = event.color || calendar?.color || "#1976d2";
|
||||
return color;
|
||||
}
|
||||
|
||||
if (!event) return null;
|
||||
const calList =
|
||||
calId.split("/")[0] === user.userData?.openpaasId
|
||||
? Object.keys(userPersonnalCalendars).map((calendar, index) => (
|
||||
<MenuItem key={index} value={index}>
|
||||
<Typography variant="body2">
|
||||
<CircleIcon
|
||||
style={{
|
||||
color: userPersonnalCalendars[index].color ?? "#3788D8",
|
||||
width: 12,
|
||||
height: 12,
|
||||
}}
|
||||
/>
|
||||
{userPersonnalCalendars[index].name}
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
))
|
||||
: Object.keys(calendars).map((calendar, index) => (
|
||||
<MenuItem key={index} value={index}>
|
||||
<Typography variant="body2">
|
||||
<CircleIcon
|
||||
style={{
|
||||
color: calendars[index].color ?? "#3788D8",
|
||||
width: 12,
|
||||
height: 12,
|
||||
}}
|
||||
/>
|
||||
{calendars[index].name} - {calendars[index].owner}
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "5vh",
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
width: "90%",
|
||||
maxWidth: "600px",
|
||||
maxHeight: "90vh",
|
||||
overflow: "auto",
|
||||
backgroundColor: "white",
|
||||
borderRadius: "8px",
|
||||
boxShadow: "0 4px 20px rgba(0,0,0,0.15)",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: "50vw",
|
||||
maxHeight: "80vh",
|
||||
}}
|
||||
>
|
||||
<Card>
|
||||
<CardHeader
|
||||
title={event?.title || "Event Details"}
|
||||
action={
|
||||
<IconButton onClick={() => onClose({}, "backdropClick")}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
<CardContent>
|
||||
{/* Event details */}
|
||||
<InfoRow
|
||||
icon={<CircleIcon style={{ color: getEventColor() }} />}
|
||||
text={event?.title || "No title"}
|
||||
/>
|
||||
<InfoRow
|
||||
icon={<CircleIcon style={{ color: getEventColor() }} />}
|
||||
text={`${formatLocalDateTime(new Date(event?.start || Date.now()))} - ${formatLocalDateTime(new Date(event?.end || Date.now()))}`}
|
||||
/>
|
||||
<InfoRow
|
||||
icon={<CircleIcon style={{ color: getEventColor() }} />}
|
||||
text={event?.location || "No location"}
|
||||
/>
|
||||
<InfoRow
|
||||
icon={<CircleIcon style={{ color: getEventColor() }} />}
|
||||
text={event?.description || "No description"}
|
||||
/>
|
||||
|
||||
{/* Attendees */}
|
||||
{event?.attendee && event.attendee.length > 0 && (
|
||||
<Box style={{ margin: "16px 0" }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Attendees
|
||||
</Typography>
|
||||
{event.attendee
|
||||
.slice(0, showAllAttendees ? event.attendee.length : 3)
|
||||
.map((attendee, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
<Avatar {...stringAvatar(attendee.cn || "U")} />
|
||||
<Typography variant="body2">
|
||||
{attendee.cn || attendee.cal_address}
|
||||
</Typography>
|
||||
<Badge
|
||||
color={
|
||||
attendee.partstat === "ACCEPTED"
|
||||
? "success"
|
||||
: attendee.partstat === "DECLINED"
|
||||
? "error"
|
||||
: "warning"
|
||||
}
|
||||
variant="dot"
|
||||
>
|
||||
<Typography variant="caption">
|
||||
{attendee.partstat}
|
||||
</Typography>
|
||||
</Badge>
|
||||
</Box>
|
||||
))}
|
||||
{event.attendee.length > 3 && (
|
||||
<Button
|
||||
<Card style={{ padding: 16, position: "absolute" }}>
|
||||
{/* Close button */}
|
||||
<Box style={{ position: "absolute", top: 8, right: 8 }}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setShowAllAttendees(!showAllAttendees)}
|
||||
onClick={() => onClose({}, "backdropClick")}
|
||||
>
|
||||
{showAllAttendees ? "Show Less" : "Show More"}
|
||||
</Button>
|
||||
)}
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<CardHeader title={isOwn ? "Edit Event" : "Event Details"} />
|
||||
|
||||
<CardContent style={{ maxHeight: "75vh", overflow: "auto" }}>
|
||||
{/* Title */}
|
||||
<TextField
|
||||
fullWidth
|
||||
disabled={!isOwn}
|
||||
label="Title"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
size="small"
|
||||
margin="dense"
|
||||
/>
|
||||
|
||||
{/* RSVP */}
|
||||
{currentUserAttendee && isOwnCal && (
|
||||
@@ -276,7 +295,17 @@ export default function EventDisplayModal({
|
||||
? "success"
|
||||
: "primary"
|
||||
}
|
||||
onClick={() => handleRSVP("ACCEPTED")}
|
||||
onClick={() =>
|
||||
handleRSVP(
|
||||
dispatch,
|
||||
calendar,
|
||||
user,
|
||||
event,
|
||||
"ACCEPTED",
|
||||
undefined,
|
||||
isRecurring ? typeOfAction : undefined
|
||||
)
|
||||
}
|
||||
>
|
||||
Accept
|
||||
</Button>
|
||||
@@ -286,7 +315,17 @@ export default function EventDisplayModal({
|
||||
? "warning"
|
||||
: "primary"
|
||||
}
|
||||
onClick={() => handleRSVP("TENTATIVE")}
|
||||
onClick={() =>
|
||||
handleRSVP(
|
||||
dispatch,
|
||||
calendar,
|
||||
user,
|
||||
event,
|
||||
"TENTATIVE",
|
||||
undefined,
|
||||
isRecurring ? typeOfAction : undefined
|
||||
)
|
||||
}
|
||||
>
|
||||
Maybe
|
||||
</Button>
|
||||
@@ -296,11 +335,24 @@ export default function EventDisplayModal({
|
||||
? "error"
|
||||
: "primary"
|
||||
}
|
||||
onClick={() => handleRSVP("DECLINED")}
|
||||
onClick={() =>
|
||||
handleRSVP(
|
||||
dispatch,
|
||||
calendar,
|
||||
user,
|
||||
event,
|
||||
"DECLINED",
|
||||
undefined,
|
||||
isRecurring ? typeOfAction : undefined
|
||||
)
|
||||
}
|
||||
>
|
||||
Decline
|
||||
</Button>
|
||||
<Button color="primary" onClick={() => {}}>
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={() => console.log("proposenewtime")}
|
||||
>
|
||||
Propose new time
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
@@ -314,52 +366,175 @@ export default function EventDisplayModal({
|
||||
disabled={!isOwn}
|
||||
labelId="calendar-select-label"
|
||||
value={calendarid.toString()}
|
||||
onChange={(e: SelectChangeEvent) =>
|
||||
setCalendarid(Number(e.target.value))
|
||||
}
|
||||
label="Calendar"
|
||||
onChange={(e: SelectChangeEvent) => {
|
||||
const newId = Number(e.target.value);
|
||||
setCalendarid(newId);
|
||||
setNewCalId(userPersonnalCalendars[newId].id);
|
||||
}}
|
||||
>
|
||||
{calList.map((calendar, index) => (
|
||||
<MenuItem key={index} value={index}>
|
||||
{calendar.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
{calList}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{/* Video conference */}
|
||||
{event?.x_openpass_videoconference && (
|
||||
<Box style={{ margin: "16px 0" }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Video Conference
|
||||
</Typography>
|
||||
<Button
|
||||
startIcon={<VideocamIcon />}
|
||||
onClick={() =>
|
||||
window.open(event.x_openpass_videoconference, "_blank")
|
||||
{/* Dates */}
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Start"
|
||||
disabled={!isOwn}
|
||||
type={allday ? "date" : "datetime-local"}
|
||||
value={allday ? start.split("T")[0] : start.slice(0, 16)}
|
||||
onChange={(e) =>
|
||||
setStart(formatLocalDateTime(new Date(e.target.value)))
|
||||
}
|
||||
size="small"
|
||||
margin="dense"
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
disabled={!isOwn}
|
||||
label="End"
|
||||
type={allday ? "date" : "datetime-local"}
|
||||
value={allday ? end.split("T")[0] : end.slice(0, 16)}
|
||||
onChange={(e) =>
|
||||
setEnd(formatLocalDateTime(new Date(e.target.value)))
|
||||
}
|
||||
size="small"
|
||||
margin="dense"
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
disabled={!isOwn}
|
||||
checked={allday}
|
||||
onChange={() => {
|
||||
const endDate = new Date(end);
|
||||
const startDate = new Date(start);
|
||||
setAllDay(!allday);
|
||||
if (endDate.getDate() === startDate.getDate()) {
|
||||
endDate.setDate(startDate.getDate() + 1);
|
||||
setEnd(formatLocalDateTime(endDate));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label="All day"
|
||||
/>
|
||||
|
||||
{/* Description & Location */}
|
||||
<TextField
|
||||
fullWidth
|
||||
disabled={!isOwn}
|
||||
label="Description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
size="small"
|
||||
margin="dense"
|
||||
multiline
|
||||
rows={2}
|
||||
/>
|
||||
|
||||
{isOwn && (
|
||||
<AttendeeSelector
|
||||
attendees={attendees}
|
||||
setAttendees={setAttendees}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Location"
|
||||
disabled={!isOwn}
|
||||
value={location}
|
||||
onChange={(e) => setLocation(e.target.value)}
|
||||
size="small"
|
||||
margin="dense"
|
||||
/>
|
||||
|
||||
{/* Video */}
|
||||
{event.x_openpass_videoconference && (
|
||||
<InfoRow
|
||||
icon={<VideocamIcon style={{ fontSize: 18 }} />}
|
||||
content={
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() =>
|
||||
window.open(event.x_openpass_videoconference)
|
||||
}
|
||||
>
|
||||
Join Video Conference
|
||||
Join the video conference
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Attendees */}
|
||||
{event.attendee?.length > 0 && (
|
||||
<Box style={{ marginBottom: 8 }}>
|
||||
<Typography variant="subtitle2">Attendees:</Typography>
|
||||
{organizer.cal_address &&
|
||||
renderAttendeeBadge(organizer, "org", true, true)}
|
||||
{(showAllAttendees
|
||||
? attendees
|
||||
: attendees.slice(0, attendeeDisplayLimit)
|
||||
).map((a, idx) => (
|
||||
<Box key={a.cal_address}>
|
||||
{renderAttendeeBadge(a, idx.toString(), true)}
|
||||
{isOwn && (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
const newAttendeesList = [...attendees];
|
||||
newAttendeesList.splice(idx, 1);
|
||||
setAttendees(newAttendeesList);
|
||||
}}
|
||||
>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
{attendees.length > attendeeDisplayLimit && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="primary"
|
||||
style={{ cursor: "pointer", marginTop: 4 }}
|
||||
onClick={() => setShowAllAttendees(!showAllAttendees)}
|
||||
>
|
||||
{showAllAttendees
|
||||
? "Show less"
|
||||
: `Show more (${
|
||||
attendees.length - attendeeDisplayLimit
|
||||
} more)`}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Divider style={{ margin: "8px 0" }} />
|
||||
|
||||
{/* Extended options */}
|
||||
{showMore && (
|
||||
<>
|
||||
<Divider style={{ margin: "16px 0" }} />
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Extended Options
|
||||
</Typography>
|
||||
|
||||
{/* Notification */}
|
||||
{isOwn && (
|
||||
<RepeatEvent
|
||||
repetition={repetition}
|
||||
eventStart={event.start}
|
||||
setRepetition={setRepetition}
|
||||
isOwn={isOwn && typeOfAction !== "solo"}
|
||||
/>
|
||||
)}
|
||||
<FormControl fullWidth margin="dense" size="small">
|
||||
<InputLabel id="notification">Notification</InputLabel>
|
||||
<Select
|
||||
disabled={!isOwn}
|
||||
labelId="notification"
|
||||
label="Notification"
|
||||
value={alarm}
|
||||
disabled={!isOwn}
|
||||
onChange={(e: SelectChangeEvent) =>
|
||||
setAlarm(e.target.value)
|
||||
}
|
||||
@@ -379,47 +554,48 @@ export default function EventDisplayModal({
|
||||
<MenuItem value={"-PT1W"}>1 week</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{/* Show me as */}
|
||||
<FormControl fullWidth margin="dense" size="small">
|
||||
<InputLabel id="busy">Show me as</InputLabel>
|
||||
<InputLabel id="Visibility">Visibility</InputLabel>
|
||||
<Select
|
||||
labelId="Visibility"
|
||||
label="Visibility"
|
||||
value={eventClass}
|
||||
disabled={!isOwn}
|
||||
onChange={(e: SelectChangeEvent) =>
|
||||
setEventClass(e.target.value)
|
||||
}
|
||||
>
|
||||
<MenuItem value={"PUBLIC"}>Public</MenuItem>
|
||||
<MenuItem value={"CONFIDENTIAL"}>Show time only</MenuItem>
|
||||
<MenuItem value={"PRIVATE"}>Private</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl fullWidth margin="dense" size="small">
|
||||
<InputLabel id="busy">is Busy</InputLabel>
|
||||
<Select
|
||||
labelId="busy"
|
||||
value={busy}
|
||||
onChange={(e: SelectChangeEvent) =>
|
||||
setBusy(e.target.value)
|
||||
}
|
||||
disabled={!isOwn}
|
||||
label="is busy"
|
||||
onChange={(e: SelectChangeEvent) => setBusy(e.target.value)}
|
||||
>
|
||||
<MenuItem value={"TRANSPARENT"}>Free</MenuItem>
|
||||
<MenuItem value={"OPAQUE"}>Busy </MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{/* Repeat */}
|
||||
{showRepeat && (
|
||||
<Box style={{ margin: "16px 0" }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Repeat
|
||||
</Typography>
|
||||
<RepeatEvent
|
||||
repetition={repetition}
|
||||
eventStart={new Date(event?.start || Date.now())}
|
||||
setRepetition={setRepetition}
|
||||
{/* Error */}
|
||||
{event.error && (
|
||||
<InfoRow
|
||||
icon={
|
||||
<ErrorOutlineIcon
|
||||
color="error"
|
||||
style={{ fontSize: 18 }}
|
||||
/>
|
||||
}
|
||||
text={event.error}
|
||||
error
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Attendees */}
|
||||
<Box style={{ margin: "16px 0" }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Participants
|
||||
</Typography>
|
||||
<AttendeeSelector
|
||||
attendees={attendees}
|
||||
setAttendees={setAttendees}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
@@ -429,16 +605,18 @@ export default function EventDisplayModal({
|
||||
{isOwn && (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
onClose({}, "backdropClick");
|
||||
dispatch(
|
||||
deleteEventAsync({
|
||||
onClick={() =>
|
||||
handleDelete(
|
||||
isRecurring,
|
||||
typeOfAction,
|
||||
onClose,
|
||||
dispatch,
|
||||
calendar,
|
||||
event,
|
||||
calId,
|
||||
eventId,
|
||||
eventURL: event.URL,
|
||||
})
|
||||
);
|
||||
}}
|
||||
eventId
|
||||
)
|
||||
}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
@@ -457,122 +635,5 @@ export default function EventDisplayModal({
|
||||
</Card>
|
||||
</Box>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function InfoRow({
|
||||
icon,
|
||||
text,
|
||||
error = false,
|
||||
data,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
text: string;
|
||||
error?: boolean;
|
||||
data?: string;
|
||||
}) {
|
||||
return (
|
||||
<Box
|
||||
style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 8 }}
|
||||
>
|
||||
{icon}
|
||||
<Typography variant="body2" color={error ? "error" : "textPrimary"}>
|
||||
{text}
|
||||
</Typography>
|
||||
{data && (
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
{data}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function AttendeeRow({
|
||||
attendee,
|
||||
key,
|
||||
}: {
|
||||
attendee: userAttendee;
|
||||
key: number;
|
||||
}) {
|
||||
return (
|
||||
<Box
|
||||
key={key}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
<Avatar {...stringAvatar(attendee.cn || "U")} />
|
||||
<Typography variant="body2">
|
||||
{attendee.cn || attendee.cal_address}
|
||||
</Typography>
|
||||
<Badge
|
||||
color={
|
||||
attendee.partstat === "ACCEPTED"
|
||||
? "success"
|
||||
: attendee.partstat === "DECLINED"
|
||||
? "error"
|
||||
: "warning"
|
||||
}
|
||||
variant="dot"
|
||||
>
|
||||
<Typography variant="caption">{attendee.partstat}</Typography>
|
||||
</Badge>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function stringToColor(string: string) {
|
||||
let hash = 0;
|
||||
let i;
|
||||
|
||||
/* eslint-disable no-bitwise */
|
||||
for (i = 0; i < string.length; i += 1) {
|
||||
hash = string.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
|
||||
let color = "#";
|
||||
|
||||
for (i = 0; i < 3; i += 1) {
|
||||
const value = (hash >> (i * 8)) & 0xff;
|
||||
color += `00${value.toString(16)}`.substr(-2);
|
||||
}
|
||||
/* eslint-enable no-bitwise */
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
export function stringAvatar(name: string) {
|
||||
return {
|
||||
sx: { width: 24, height: 24, fontSize: 18, bgcolor: stringToColor(name) },
|
||||
children: name[0],
|
||||
};
|
||||
}
|
||||
|
||||
export function renderAttendeeBadge(
|
||||
attendee: userAttendee,
|
||||
key: string,
|
||||
isOrganizer = false
|
||||
) {
|
||||
return (
|
||||
<Box
|
||||
key={key}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
<Avatar {...stringAvatar(attendee.cn || "U")} />
|
||||
<Typography variant="body2">
|
||||
{attendee.cn || attendee.cal_address}
|
||||
{isOrganizer && " (Organizer)"}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user