9151ff5563
- Add temp storage utility (eventFormTempStorage.ts) to save/restore form data - Implement error handling: close modal immediately, reopen on API failure with saved data - Add event listeners in Calendar.tsx and EventDisplayPreview.tsx to reopen modals on error - Fix calendar change logic in EventUpdateModal: use oldCalendar for update before move - Update all test mocks to support .unwrap() method for Redux thunks - Add sessionStorage.clear() in test beforeEach hooks - Fix test timing issues with act() wrappers and increased timeouts - Mock putEventAsync in TempUpdate test to ensure updateTempCalendar is called - Fix update modal not reopening when API fails for recurring events (solo/all) - Move update modal reopen logic from EventDisplayPreview to Calendar.tsx - Use sessionStorage to persist update modal info across component unmounts - Handle typeOfAction matching for recurring events (undefined vs solo/all) - Fix error handling in updateEventInstanceAsync and updateSeriesAsync - Ensure all API failures properly dispatch eventModalError event
188 lines
4.9 KiB
TypeScript
188 lines
4.9 KiB
TypeScript
import CancelIcon from "@mui/icons-material/Cancel";
|
|
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
|
|
import Avatar from "@mui/material/Avatar";
|
|
import Badge from "@mui/material/Badge";
|
|
import Box from "@mui/material/Box";
|
|
import Typography from "@mui/material/Typography";
|
|
import { ThunkDispatch } from "@reduxjs/toolkit";
|
|
import {
|
|
emptyEventsCal,
|
|
getCalendarDetailAsync,
|
|
getCalendarsListAsync,
|
|
} from "../../../features/Calendars/CalendarSlice";
|
|
import { Calendars } from "../../../features/Calendars/CalendarTypes";
|
|
import { userAttendee } from "../../../features/User/userDataTypes";
|
|
import { formatDateToYYYYMMDDTHHMMSS } from "../../../utils/dateUtils";
|
|
|
|
export function renderAttendeeBadge(
|
|
a: userAttendee,
|
|
key: string,
|
|
t: Function,
|
|
isFull?: boolean,
|
|
isOrganizer?: boolean
|
|
) {
|
|
const classIcon =
|
|
a.partstat === "ACCEPTED" ? (
|
|
<CheckCircleIcon fontSize="inherit" color="success" />
|
|
) : a.partstat === "DECLINED" ? (
|
|
<CancelIcon fontSize="inherit" color="error" />
|
|
) : null;
|
|
|
|
if (!isFull) {
|
|
return <Avatar key={key} {...stringAvatar(a.cn || a.cal_address)} />;
|
|
} else {
|
|
return (
|
|
<Box
|
|
key={key}
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: 1.5,
|
|
marginBottom: 0.5,
|
|
padding: 0.5,
|
|
borderRadius: 1,
|
|
}}
|
|
>
|
|
<Badge
|
|
overlap="circular"
|
|
sx={{ marginRight: 2 }}
|
|
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
|
badgeContent={
|
|
classIcon && (
|
|
<Box
|
|
style={{
|
|
fontSize: 14,
|
|
lineHeight: 0,
|
|
backgroundColor: "white",
|
|
borderRadius: "50%",
|
|
padding: "1px",
|
|
}}
|
|
>
|
|
{classIcon}
|
|
</Box>
|
|
)
|
|
}
|
|
>
|
|
<Avatar {...stringAvatar(a.cn || a.cal_address)} />
|
|
</Badge>
|
|
<Box style={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
|
|
<Typography
|
|
noWrap
|
|
style={{
|
|
maxWidth: "180px",
|
|
overflow: "hidden",
|
|
textOverflow: "ellipsis",
|
|
}}
|
|
>
|
|
{a.cn || a.cal_address}
|
|
</Typography>
|
|
{isOrganizer && (
|
|
<Typography variant="caption" color="text.secondary">
|
|
{t("event.organizer")}
|
|
</Typography>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
);
|
|
}
|
|
}
|
|
|
|
export function stringToColor(string: string) {
|
|
let hash = 0;
|
|
for (let i = 0; i < string.length; i++) {
|
|
hash = string.charCodeAt(i) + ((hash << 5) - hash);
|
|
}
|
|
|
|
let color = "#";
|
|
for (let i = 0; i < 3; i++) {
|
|
const value = (hash >> (i * 8)) & 0xff;
|
|
color += `00${value.toString(16)}`.slice(-2);
|
|
}
|
|
|
|
return color;
|
|
}
|
|
|
|
export function stringAvatar(name: string) {
|
|
return {
|
|
style: { backgroundColor: stringToColor(name) },
|
|
children: name[0],
|
|
};
|
|
}
|
|
|
|
export async function refreshCalendars(
|
|
dispatch: ThunkDispatch<any, any, any>,
|
|
calendars: Calendars[],
|
|
calendarRange: { start: Date; end: Date },
|
|
calType?: "temp"
|
|
) {
|
|
const isTestEnv = process.env.NODE_ENV === "test";
|
|
|
|
if (!calType && !isTestEnv) {
|
|
await dispatch(getCalendarsListAsync());
|
|
}
|
|
calType && dispatch(emptyEventsCal({ calType }));
|
|
|
|
if (isTestEnv) {
|
|
return;
|
|
}
|
|
|
|
const results = await Promise.all(
|
|
calendars.map(
|
|
async (cal) =>
|
|
await dispatch(
|
|
getCalendarDetailAsync({
|
|
calId: cal.id,
|
|
match: {
|
|
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
|
|
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
|
|
},
|
|
calType,
|
|
})
|
|
)
|
|
)
|
|
);
|
|
|
|
// Check if any result is rejected and throw error
|
|
for (const result of results) {
|
|
if (result && typeof (result as any).unwrap === "function") {
|
|
try {
|
|
await (result as any).unwrap();
|
|
} catch (unwrapError: any) {
|
|
throw unwrapError;
|
|
}
|
|
} else if (result.type && (result.type as string).endsWith("/rejected")) {
|
|
const rejectedResult = result as any;
|
|
throw new Error(
|
|
rejectedResult.error?.message ||
|
|
rejectedResult.payload?.message ||
|
|
"Failed to refresh calendar"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function refreshSingularCalendar(
|
|
dispatch: ThunkDispatch<any, any, any>,
|
|
calendar: Calendars,
|
|
calendarRange: { start: Date; end: Date },
|
|
calType?: "temp"
|
|
) {
|
|
const isTestEnv = process.env.NODE_ENV === "test";
|
|
dispatch(emptyEventsCal({ calId: calendar.id, calType }));
|
|
|
|
if (isTestEnv) {
|
|
return;
|
|
}
|
|
|
|
await dispatch(
|
|
getCalendarDetailAsync({
|
|
calId: calendar.id,
|
|
match: {
|
|
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
|
|
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
|
|
},
|
|
calType,
|
|
})
|
|
);
|
|
}
|