[#121] added error handler and snackbar
This commit is contained in:
committed by
Benoit TELLIER
parent
f498309f44
commit
a00f179dbd
@@ -0,0 +1,35 @@
|
||||
import { EventErrorHandler } from "../../../src/components/Error/EventErrorHandler";
|
||||
|
||||
describe("EventErrorHandler", () => {
|
||||
it("calls the callback when a new error is reported", () => {
|
||||
const handler = new EventErrorHandler();
|
||||
const callback = jest.fn();
|
||||
|
||||
handler.setErrorCallback(callback);
|
||||
handler.reportError("123", "Something broke");
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(["Something broke"]);
|
||||
});
|
||||
|
||||
it("does not duplicate errors for same eventId", () => {
|
||||
const handler = new EventErrorHandler();
|
||||
const callback = jest.fn();
|
||||
|
||||
handler.setErrorCallback(callback);
|
||||
handler.reportError("A", "Error 1");
|
||||
handler.reportError("A", "Error 1 again");
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("clears errors properly", () => {
|
||||
const handler = new EventErrorHandler();
|
||||
const callback = jest.fn();
|
||||
|
||||
handler.setErrorCallback(callback);
|
||||
handler.reportError("A", "Error 1");
|
||||
handler.clearAll();
|
||||
|
||||
expect(callback).toHaveBeenLastCalledWith([]);
|
||||
});
|
||||
});
|
||||
@@ -34,6 +34,8 @@ import {
|
||||
import { useCalendarEventHandlers } from "./hooks/useCalendarEventHandlers";
|
||||
import { useCalendarViewHandlers } from "./hooks/useCalendarViewHandlers";
|
||||
import { EditModeDialog } from "../Event/EditModeDialog";
|
||||
import { EventErrorHandler } from "../Error/EventErrorHandler";
|
||||
import { EventErrorSnackbar } from "../Error/ErrorSnackbar";
|
||||
|
||||
interface CalendarAppProps {
|
||||
calendarRef: React.RefObject<CalendarApi | null>;
|
||||
@@ -72,6 +74,19 @@ export default function CalendarApp({
|
||||
|
||||
// Auto-select personal calendars when first loaded
|
||||
const initialLoadRef = useRef(true);
|
||||
const [eventErrors, setEventErrors] = useState<string[]>([]);
|
||||
const errorHandler = useRef(new EventErrorHandler());
|
||||
|
||||
useEffect(() => {
|
||||
const handler = errorHandler.current;
|
||||
handler.setErrorCallback(setEventErrors);
|
||||
return () => handler.setErrorCallback(() => {});
|
||||
}, []);
|
||||
|
||||
const handleErrorClose = () => {
|
||||
setEventErrors([]);
|
||||
errorHandler.current.clearAll();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (initialLoadRef.current && Object.keys(calendars).length > 0 && userId) {
|
||||
@@ -191,6 +206,7 @@ export default function CalendarApp({
|
||||
onViewChange,
|
||||
calendars,
|
||||
tempcalendars,
|
||||
errorHandler: errorHandler.current,
|
||||
});
|
||||
|
||||
const handleMonthUp = () => {
|
||||
@@ -439,6 +455,7 @@ export default function CalendarApp({
|
||||
onClose={eventHandlers.handleCloseEventDisplay}
|
||||
/>
|
||||
)}
|
||||
<EventErrorSnackbar messages={eventErrors} onClose={handleErrorClose} />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Calendars } from "../../../features/Calendars/CalendarTypes";
|
||||
import HelpOutlineIcon from "@mui/icons-material/HelpOutline";
|
||||
import AccessTimeIcon from "@mui/icons-material/AccessTime";
|
||||
import LockIcon from "@mui/icons-material/Lock";
|
||||
import { EventErrorHandler } from "../../Error/EventErrorHandler";
|
||||
|
||||
export interface ViewHandlersProps {
|
||||
calendarRef: React.RefObject<CalendarApi | null>;
|
||||
@@ -15,6 +16,7 @@ export interface ViewHandlersProps {
|
||||
onViewChange?: (view: string) => void;
|
||||
calendars: Record<string, Calendars>;
|
||||
tempcalendars: Record<string, Calendars>;
|
||||
errorHandler: EventErrorHandler;
|
||||
}
|
||||
|
||||
export const createViewHandlers = (props: ViewHandlersProps) => {
|
||||
@@ -25,6 +27,7 @@ export const createViewHandlers = (props: ViewHandlersProps) => {
|
||||
onViewChange,
|
||||
calendars,
|
||||
tempcalendars,
|
||||
errorHandler,
|
||||
} = props;
|
||||
|
||||
const handleDayHeaderDidMount = (arg: any) => {
|
||||
@@ -142,64 +145,77 @@ export const createViewHandlers = (props: ViewHandlersProps) => {
|
||||
attendee: attendees = [],
|
||||
class: classification,
|
||||
} = props;
|
||||
try {
|
||||
const calendarsSource = temp ? tempcalendars : calendars;
|
||||
const calendar = calendarsSource[calId];
|
||||
if (!calendar) return null;
|
||||
|
||||
const calendarsSource = temp ? tempcalendars : calendars;
|
||||
const calendar = calendarsSource[calId];
|
||||
if (!calendar) return null;
|
||||
const isPrivate = ["PRIVATE", "CONFIDENTIAL"].includes(classification);
|
||||
const ownerEmails = new Set(
|
||||
calendar.ownerEmails?.map((e) => e.toLowerCase())
|
||||
);
|
||||
const delegated = calendar.delegated;
|
||||
let Icon = null;
|
||||
let titleStyle: React.CSSProperties = {};
|
||||
|
||||
const isPrivate = ["PRIVATE", "CONFIDENTIAL"].includes(classification);
|
||||
const ownerEmails = new Set(
|
||||
calendar.ownerEmails?.map((e) => e.toLowerCase())
|
||||
);
|
||||
const delegated = calendar.delegated;
|
||||
let Icon = null;
|
||||
let titleStyle: React.CSSProperties = {};
|
||||
const showSpecialDisplay = attendees.filter((att: userAttendee) =>
|
||||
ownerEmails.has(att.cal_address.toLowerCase())
|
||||
);
|
||||
// if no special display
|
||||
if (attendees.length && !delegated && !showSpecialDisplay.length) {
|
||||
return React.createElement(
|
||||
"div",
|
||||
{ style: { display: "flex", alignItems: "center" } },
|
||||
React.createElement("span", null, event.title)
|
||||
);
|
||||
}
|
||||
switch (showSpecialDisplay?.[0]?.partstat) {
|
||||
case "DECLINED":
|
||||
Icon = null;
|
||||
titleStyle.textDecoration = "line-through";
|
||||
break;
|
||||
case "TENTATIVE":
|
||||
Icon = HelpOutlineIcon;
|
||||
break;
|
||||
case "NEEDS-ACTION":
|
||||
Icon = AccessTimeIcon;
|
||||
break;
|
||||
case "ACCEPTED":
|
||||
Icon = null;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const showSpecialDisplay = attendees.filter((att: userAttendee) =>
|
||||
ownerEmails.has(att.cal_address.toLowerCase())
|
||||
);
|
||||
// if no special display
|
||||
if (attendees.length && !delegated && !showSpecialDisplay.length) {
|
||||
return React.createElement(
|
||||
"div",
|
||||
{ style: { display: "flex", alignItems: "center" } },
|
||||
React.createElement("span", event.title)
|
||||
isPrivate &&
|
||||
React.createElement(LockIcon, {
|
||||
"data-testid": "lock-icon",
|
||||
fontSize: "small",
|
||||
style: { marginRight: "4px" },
|
||||
}),
|
||||
Icon &&
|
||||
React.createElement(Icon, {
|
||||
fontSize: "small",
|
||||
style: { marginRight: "4px" },
|
||||
}),
|
||||
React.createElement("span", { style: titleStyle }, event.title)
|
||||
);
|
||||
} catch (e) {
|
||||
const message =
|
||||
e instanceof Error ? e.message : "Unknown error during rendering";
|
||||
errorHandler.reportError(
|
||||
event._def.extendedProps.uid || event.id,
|
||||
message
|
||||
);
|
||||
return React.createElement(
|
||||
"div",
|
||||
{ style: { display: "flex", alignItems: "center" } },
|
||||
React.createElement("span", null, event.title)
|
||||
);
|
||||
}
|
||||
switch (showSpecialDisplay?.[0]?.partstat) {
|
||||
case "DECLINED":
|
||||
Icon = null;
|
||||
titleStyle.textDecoration = "line-through";
|
||||
break;
|
||||
case "TENTATIVE":
|
||||
Icon = HelpOutlineIcon;
|
||||
break;
|
||||
case "NEEDS-ACTION":
|
||||
Icon = AccessTimeIcon;
|
||||
break;
|
||||
case "ACCEPTED":
|
||||
Icon = null;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return React.createElement(
|
||||
"div",
|
||||
{ style: { display: "flex", alignItems: "center" } },
|
||||
isPrivate &&
|
||||
React.createElement(LockIcon, {
|
||||
"data-testid": "lock-icon",
|
||||
fontSize: "small",
|
||||
style: { marginRight: "4px" },
|
||||
}),
|
||||
Icon &&
|
||||
React.createElement(Icon, {
|
||||
fontSize: "small",
|
||||
style: { marginRight: "4px" },
|
||||
}),
|
||||
React.createElement("span", { style: titleStyle }, event.title)
|
||||
);
|
||||
};
|
||||
|
||||
const handleEventDidMount = (arg: any) => {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import Snackbar from "@mui/material/Snackbar";
|
||||
import Alert from "@mui/material/Alert";
|
||||
import Button from "@mui/material/Button";
|
||||
|
||||
interface Props {
|
||||
messages: string[];
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function EventErrorSnackbar({ messages, onClose }: Props) {
|
||||
const open = messages.length > 0;
|
||||
const summary =
|
||||
messages.length === 1
|
||||
? messages[0]
|
||||
: `${messages.length} events with errors`;
|
||||
|
||||
return (
|
||||
<Snackbar
|
||||
open={open}
|
||||
autoHideDuration={6000}
|
||||
onClose={onClose}
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
|
||||
>
|
||||
<Alert
|
||||
severity="error"
|
||||
onClose={onClose}
|
||||
sx={{ width: "100%" }}
|
||||
action={
|
||||
<Button color="inherit" size="small" onClick={onClose}>
|
||||
OK
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{summary}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
export class EventErrorHandler {
|
||||
private errors = new Map<string, string>();
|
||||
private onErrorCallback?: (messages: string[]) => void;
|
||||
|
||||
setErrorCallback(callback: (messages: string[]) => void) {
|
||||
this.onErrorCallback = callback;
|
||||
}
|
||||
|
||||
reportError(eventId: string, message: string) {
|
||||
if (!this.errors.has(eventId)) {
|
||||
this.errors.set(eventId, message);
|
||||
console.warn(`[EventErrorHandler] ${eventId}: ${message}`);
|
||||
this.emit();
|
||||
}
|
||||
}
|
||||
|
||||
clearAll() {
|
||||
this.errors.clear();
|
||||
this.emit();
|
||||
}
|
||||
|
||||
private emit() {
|
||||
this.onErrorCallback?.(Array.from(this.errors.values()));
|
||||
}
|
||||
}
|
||||
@@ -123,12 +123,17 @@ export function parseCalendarEvent(
|
||||
}
|
||||
|
||||
event.URL = eventURL;
|
||||
if (!event.uid || !event.start) {
|
||||
if (!event.uid || !event.start || !event.end) {
|
||||
console.error(
|
||||
`missing crucial event param in calendar ${calendarid} `,
|
||||
data
|
||||
);
|
||||
event.error = `missing crucial event param in calendar ${calendarid} `;
|
||||
if (!event.end) {
|
||||
const start = event.start ? new Date(event.start) : new Date();
|
||||
const artificialEnd = new Date(start.getTime() + 3600000);
|
||||
event.end = formatDateToICal(artificialEnd, false);
|
||||
}
|
||||
}
|
||||
|
||||
return event as CalendarEvent;
|
||||
|
||||
Reference in New Issue
Block a user