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:
lenhanphung
2025-10-08 13:12:41 +07:00
committed by Benoit TELLIER
parent ad56b71488
commit 6433086332
2 changed files with 551 additions and 490 deletions
@@ -8,7 +8,7 @@ import {
createEventHandlers, createEventHandlers,
EventHandlersProps, EventHandlersProps,
} from "../../../src/components/Calendar/handlers/eventHandlers"; } 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", () => { describe("Recurrence Event Behavior Tests", () => {
const mockOnClose = jest.fn(); const mockOnClose = jest.fn();
+423 -362
View File
@@ -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 { import {
deleteEventAsync, Box,
putEventAsync, Button,
removeEvent, ButtonGroup,
} from "../Calendars/CalendarSlice"; 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 { useAppDispatch, useAppSelector } from "../../app/hooks";
import AttendeeSelector from "../../components/Attendees/AttendeeSearch"; import AttendeeSelector from "../../components/Attendees/AttendeeSearch";
import { import {
Button, handleDelete,
Box, handleRSVP,
Typography, } from "../../components/Event/eventHandlers/eventHandlers";
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";
import RepeatEvent from "../../components/Event/EventRepeat"; 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({ export default function EventDisplayModal({
eventId, eventId,
calId, calId,
open, open,
onClose, onClose,
eventData, typeOfAction,
}: { }: {
eventId: string; eventId: string;
calId: string; calId: string;
open: boolean; open: boolean;
onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void; onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void;
eventData?: CalendarEvent | null; typeOfAction?: "solo" | "all";
}) { }) {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const calendar = useAppSelector((state) => state.calendars.list[calId]); const calendar = useAppSelector((state) => state.calendars.list[calId]);
const cachedEvent = useAppSelector( const event = useAppSelector(
(state) => state.calendars.list[calId]?.events[eventId] (state) => state.calendars.list[calId]?.events[eventId]
); );
const user = useAppSelector((state) => state.user); 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 [showAllAttendees, setShowAllAttendees] = useState(false);
const [showMore, setShowMore] = useState(false); const [showMore, setShowMore] = useState(false);
@@ -71,91 +81,135 @@ export default function EventDisplayModal({
); );
// Form state // 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>( const [repetition, setRepetition] = useState<RepetitionObject>(
event?.repetition ?? ({} as RepetitionObject) event?.repetition ?? ({} as RepetitionObject)
); );
const [alarm, setAlarm] = useState(event?.alarm?.trigger ?? ""); const [alarm, setAlarm] = useState(event?.alarm?.trigger ?? "");
const [busy, setBusy] = useState(event?.transp ?? "OPAQUE"); 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( const [calendarid, setCalendarid] = useState(
calId.split("/")[0] === user.userData?.openpaasId calId.split("/")[0] === user.userData?.openpaasId
? userPersonnalCalendars.findIndex((cal) => cal.id === calId) ? userPersonnalCalendars.findIndex((cal) => cal.id === calId)
: calendars.findIndex((cal) => cal.id === calId) : calendars.findIndex((cal) => cal.id === calId)
); );
const [attendees, setAttendees] = useState<userAttendee[]>( const [attendees, setAttendees] = useState(
event?.attendee (event?.attendee || []).filter(
? event.attendee.filter( (a) => a.cal_address !== event?.organizer?.cal_address
(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( const currentUserAttendee = event?.attendee?.find(
(a) => a.cal_address === user.userData?.email (person) => person.cal_address === user.userData.email
); );
const calList = const organizer =
calId.split("/")[0] === user.userData?.openpaasId event?.attendee?.find(
? userPersonnalCalendars (a) => a.cal_address === event?.organizer?.cal_address
: calendars; ) ?? ({} as userAttendee);
function handleRSVP(rsvp: string) { const isOwn = organizer?.cal_address === user.userData.email;
if (!event) return; const isOwnCal = userPersonnalCalendars.find((cal) => cal.id === calId);
const newEvent = { ...event }; const attendeeDisplayLimit = 3;
newEvent.attendee = newEvent.attendee.map((a) =>
a.cal_address === user.userData?.email ? { ...a, partstat: rsvp } : a useEffect(() => {
); if (!event || !calendar) {
dispatch(putEventAsync({ cal: calendar, newEvent })); 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 () => { const handleSave = async () => {
if (!event) return; const newEventUID = crypto.randomUUID();
const newEvent: CalendarEvent = { const newEvent: CalendarEvent = {
calId, calId,
title: event.title, title,
URL: event.URL ?? `/calendars/${calId}/${event.uid}.ics`, URL: event.URL ?? `/calendars/${calId}/${newEventUID}.ics`,
start: event.start, start: new Date(start),
end: event.end, end: new Date(end),
allday: event.allday, allday,
uid: event.uid, uid: event.uid ?? newEventUID,
description: event.description, description,
location: event.location, location,
repetition, repetition,
class: event.class, class: eventClass,
organizer: event.organizer, organizer: event.organizer,
timezone, timezone,
attendee: event.organizer attendee: [organizer, ...attendees],
? [event.organizer as userAttendee, ...attendees]
: attendees,
transp: busy, transp: busy,
color: userPersonnalCalendars[calendarid]?.color, color: userPersonnalCalendars[calendarid]?.color,
alarm: { trigger: alarm, action: "EMAIL" }, alarm: { trigger: alarm, action: "EMAIL" },
}; };
const [baseId, recurrenceId] = event.uid.split("/"); const [baseId, recurrenceId] = event.uid.split("/");
if (recurrenceId) { const calendarRange = getCalendarRange(new Date(start));
Object.keys(userPersonnalCalendars[calendarid].events).forEach(
(element) => { if (typeOfAction === "solo") {
if (element.split("/")[0] === baseId) { dispatch(
dispatch(removeEvent({ calendarUid: calId, eventUid: element })); updateEventInstanceAsync({
}
}
);
}
await dispatch(
putEventAsync({
cal: userPersonnalCalendars[calendarid], 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"); onClose({}, "backdropClick");
}; };
@@ -163,108 +217,73 @@ export default function EventDisplayModal({
setShowMore(!showMore); setShowMore(!showMore);
}; };
function getEventColor() { const calList =
if (!event) return "#1976d2"; calId.split("/")[0] === user.userData?.openpaasId
const color = event.color || calendar?.color || "#1976d2"; ? Object.keys(userPersonnalCalendars).map((calendar, index) => (
return color; <MenuItem key={index} value={index}>
} <Typography variant="body2">
<CircleIcon
if (!event) return null; 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 ( return (
<>
<Modal open={open} onClose={onClose}> <Modal open={open} onClose={onClose}>
<Box <Box
style={{ style={{
position: "absolute", position: "absolute",
top: "5vh", top: "5vh",
left: "50%", left: "50%",
transform: "translateX(-50%)", transform: "translate(-50%, -50%)",
width: "90%", width: "50vw",
maxWidth: "600px", maxHeight: "80vh",
maxHeight: "90vh",
overflow: "auto",
backgroundColor: "white",
borderRadius: "8px",
boxShadow: "0 4px 20px rgba(0,0,0,0.15)",
}} }}
> >
<Card> <Card style={{ padding: 16, position: "absolute" }}>
<CardHeader {/* Close button */}
title={event?.title || "Event Details"} <Box style={{ position: "absolute", top: 8, right: 8 }}>
action={ <IconButton
<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
size="small" size="small"
onClick={() => setShowAllAttendees(!showAllAttendees)} onClick={() => onClose({}, "backdropClick")}
> >
{showAllAttendees ? "Show Less" : "Show More"} <CloseIcon fontSize="small" />
</Button> </IconButton>
)}
</Box> </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 */} {/* RSVP */}
{currentUserAttendee && isOwnCal && ( {currentUserAttendee && isOwnCal && (
@@ -276,7 +295,17 @@ export default function EventDisplayModal({
? "success" ? "success"
: "primary" : "primary"
} }
onClick={() => handleRSVP("ACCEPTED")} onClick={() =>
handleRSVP(
dispatch,
calendar,
user,
event,
"ACCEPTED",
undefined,
isRecurring ? typeOfAction : undefined
)
}
> >
Accept Accept
</Button> </Button>
@@ -286,7 +315,17 @@ export default function EventDisplayModal({
? "warning" ? "warning"
: "primary" : "primary"
} }
onClick={() => handleRSVP("TENTATIVE")} onClick={() =>
handleRSVP(
dispatch,
calendar,
user,
event,
"TENTATIVE",
undefined,
isRecurring ? typeOfAction : undefined
)
}
> >
Maybe Maybe
</Button> </Button>
@@ -296,11 +335,24 @@ export default function EventDisplayModal({
? "error" ? "error"
: "primary" : "primary"
} }
onClick={() => handleRSVP("DECLINED")} onClick={() =>
handleRSVP(
dispatch,
calendar,
user,
event,
"DECLINED",
undefined,
isRecurring ? typeOfAction : undefined
)
}
> >
Decline Decline
</Button> </Button>
<Button color="primary" onClick={() => {}}> <Button
color="primary"
onClick={() => console.log("proposenewtime")}
>
Propose new time Propose new time
</Button> </Button>
</ButtonGroup> </ButtonGroup>
@@ -314,52 +366,175 @@ export default function EventDisplayModal({
disabled={!isOwn} disabled={!isOwn}
labelId="calendar-select-label" labelId="calendar-select-label"
value={calendarid.toString()} value={calendarid.toString()}
onChange={(e: SelectChangeEvent) => label="Calendar"
setCalendarid(Number(e.target.value)) onChange={(e: SelectChangeEvent) => {
} const newId = Number(e.target.value);
setCalendarid(newId);
setNewCalId(userPersonnalCalendars[newId].id);
}}
> >
{calList.map((calendar, index) => ( {calList}
<MenuItem key={index} value={index}>
{calendar.name}
</MenuItem>
))}
</Select> </Select>
</FormControl> </FormControl>
{/* Video conference */} {/* Dates */}
{event?.x_openpass_videoconference && ( <TextField
<Box style={{ margin: "16px 0" }}> fullWidth
<Typography variant="h6" gutterBottom> label="Start"
Video Conference disabled={!isOwn}
</Typography> type={allday ? "date" : "datetime-local"}
<Button value={allday ? start.split("T")[0] : start.slice(0, 16)}
startIcon={<VideocamIcon />} onChange={(e) =>
onClick={() => setStart(formatLocalDateTime(new Date(e.target.value)))
window.open(event.x_openpass_videoconference, "_blank")
} }
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" variant="contained"
color="primary" onClick={() =>
window.open(event.x_openpass_videoconference)
}
> >
Join Video Conference Join the video conference
</Button> </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> </Box>
)} )}
<Divider style={{ margin: "8px 0" }} />
{/* Extended options */} {/* Extended options */}
{showMore && ( {showMore && (
<> <>
<Divider style={{ margin: "16px 0" }} /> {isOwn && (
<Typography variant="h6" gutterBottom> <RepeatEvent
Extended Options repetition={repetition}
</Typography> eventStart={event.start}
setRepetition={setRepetition}
{/* Notification */} isOwn={isOwn && typeOfAction !== "solo"}
/>
)}
<FormControl fullWidth margin="dense" size="small"> <FormControl fullWidth margin="dense" size="small">
<InputLabel id="notification">Notification</InputLabel> <InputLabel id="notification">Notification</InputLabel>
<Select <Select
disabled={!isOwn}
labelId="notification" labelId="notification"
label="Notification"
value={alarm} value={alarm}
disabled={!isOwn}
onChange={(e: SelectChangeEvent) => onChange={(e: SelectChangeEvent) =>
setAlarm(e.target.value) setAlarm(e.target.value)
} }
@@ -379,47 +554,48 @@ export default function EventDisplayModal({
<MenuItem value={"-PT1W"}>1 week</MenuItem> <MenuItem value={"-PT1W"}>1 week</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
{/* Show me as */}
<FormControl fullWidth margin="dense" size="small"> <FormControl fullWidth margin="dense" size="small">
<InputLabel id="busy">Show me as</InputLabel> <InputLabel id="Visibility">Visibility</InputLabel>
<Select <Select
labelId="Visibility"
label="Visibility"
value={eventClass}
disabled={!isOwn} 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" labelId="busy"
value={busy} value={busy}
onChange={(e: SelectChangeEvent) => disabled={!isOwn}
setBusy(e.target.value) label="is busy"
} onChange={(e: SelectChangeEvent) => setBusy(e.target.value)}
> >
<MenuItem value={"TRANSPARENT"}>Free</MenuItem> <MenuItem value={"TRANSPARENT"}>Free</MenuItem>
<MenuItem value={"OPAQUE"}>Busy </MenuItem> <MenuItem value={"OPAQUE"}>Busy </MenuItem>
</Select> </Select>
</FormControl> </FormControl>
{/* Error */}
{/* Repeat */} {event.error && (
{showRepeat && ( <InfoRow
<Box style={{ margin: "16px 0" }}> icon={
<Typography variant="h6" gutterBottom> <ErrorOutlineIcon
Repeat color="error"
</Typography> style={{ fontSize: 18 }}
<RepeatEvent />
repetition={repetition} }
eventStart={new Date(event?.start || Date.now())} text={event.error}
setRepetition={setRepetition} error
/> />
</Box>
)} )}
{/* Attendees */}
<Box style={{ margin: "16px 0" }}>
<Typography variant="h6" gutterBottom>
Participants
</Typography>
<AttendeeSelector
attendees={attendees}
setAttendees={setAttendees}
/>
</Box>
</> </>
)} )}
</CardContent> </CardContent>
@@ -429,16 +605,18 @@ export default function EventDisplayModal({
{isOwn && ( {isOwn && (
<IconButton <IconButton
size="small" size="small"
onClick={() => { onClick={() =>
onClose({}, "backdropClick"); handleDelete(
dispatch( isRecurring,
deleteEventAsync({ typeOfAction,
onClose,
dispatch,
calendar,
event,
calId, calId,
eventId, eventId
eventURL: event.URL, )
}) }
);
}}
> >
<DeleteIcon fontSize="small" /> <DeleteIcon fontSize="small" />
</IconButton> </IconButton>
@@ -457,122 +635,5 @@ export default function EventDisplayModal({
</Card> </Card>
</Box> </Box>
</Modal> </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>
); );
} }