Delete Calendars (#148)

* [#66] added delete popup and logic

* [#66] Homogenize Dialogs components between modify and delete to keep them coherent+ fixed tests

* [#66] added tests

* [#66] text adjusment

* fixup! [#66] added delete popup and logic

---------

Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
Camille Moussu
2025-09-30 10:27:47 +02:00
committed by GitHub
parent b46be3af29
commit 475ab79731
10 changed files with 324 additions and 111 deletions
+9 -2
View File
@@ -153,17 +153,24 @@ export default function CalendarApp({
calendarRange.end,
]);
const [prevTempCalendars, setPrevTempCalendars] = useState<string[]>([]);
const [prevRangeKey, setPrevRangeKey] = useState<string>("");
useEffect(() => {
updateCalsDetails(
Object.keys(tempcalendars),
prevTempCalendars,
pending,
tempcalendars,
rangeKey,
prevRangeKey,
dispatch,
calendarRange,
"temp"
);
}, [rangeKey, Object.keys(tempcalendars).join(",")]);
setPrevTempCalendars(Object.keys(tempcalendars));
setPrevRangeKey(rangeKey);
}, [rangeKey, Object.keys(tempcalendars).join(","), pending]);
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
const [anchorPosition, setAnchorPosition] = useState<{
+145
View File
@@ -0,0 +1,145 @@
import { useEffect, useState } from "react";
import {
createCalendarAsync /*, updateCalendarAsync */,
patchCalendarAsync,
} from "../../features/Calendars/CalendarSlice";
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import {
TextField,
Button,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
} from "@mui/material";
import { ColorPicker } from "./CalendarColorPicker";
import { Calendars } from "../../features/Calendars/CalendarTypes";
function CalendarPopover({
open,
onClose,
calendar,
}: {
open: boolean;
onClose: (
event: object | null,
reason: "backdropClick" | "escapeKeyDown"
) => void;
calendar?: Calendars;
}) {
const dispatch = useAppDispatch();
const userId =
useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
const [name, setName] = useState(calendar?.name ?? "");
const [description, setDescription] = useState(calendar?.description ?? "");
const [color, setColor] = useState(calendar?.color ?? "");
useEffect(() => {
if (open) {
if (calendar) {
setName(calendar.name);
setDescription(calendar.description ?? "");
setColor(calendar.color ?? "");
} else {
setName("");
setDescription("");
setColor("");
}
}
}, [calendar, open]);
const handleSave = () => {
const trimmedName = name.trim();
const trimmedDesc = description.trim();
if (trimmedName) {
const calId = calendar ? calendar.id : crypto.randomUUID();
if (calendar?.id) {
dispatch(
patchCalendarAsync({
calId: calendar.id,
calLink: calendar.link,
patch: { name: trimmedName, desc: trimmedDesc, color },
})
);
} else {
dispatch(
createCalendarAsync({
name: trimmedName,
desc: trimmedDesc,
color,
userId,
calId,
})
);
}
onClose({}, "backdropClick");
}
};
const palette = [
"#D50000",
"#E67C73",
"#F4511E",
"#F6BF26",
"#33B679",
"#0B8043",
"#039BE5",
"#3F51B5",
"#7986CB",
"#8E24AA",
"#616161",
];
return (
<Dialog open={open} onClose={(e, reason) => onClose(e, reason)}>
<DialogTitle style={{ backgroundColor: color }}>
Calendar configuration
</DialogTitle>
<DialogContent>
<TextField
fullWidth
label="Name"
value={name}
onChange={(e) => setName(e.target.value)}
size="small"
margin="dense"
/>
<TextField
fullWidth
label="Description"
value={description}
onChange={(e) => setDescription(e.target.value)}
size="small"
margin="dense"
multiline
rows={2}
/>
<ColorPicker
onChange={(color) => setColor(color)}
selectedColor={color}
/>
</DialogContent>
<DialogActions>
<Button
variant="outlined"
onClick={(e) => onClose({}, "backdropClick")}
>
Cancel
</Button>
<Button
disabled={!name.trim()}
variant="contained"
onClick={handleSave}
>
Save
</Button>
</DialogActions>
</Dialog>
);
}
export default CalendarPopover;
+90 -38
View File
@@ -2,15 +2,19 @@ import Accordion from "@mui/material/Accordion";
import AccordionDetails from "@mui/material/AccordionDetails";
import AccordionSummary from "@mui/material/AccordionSummary";
import Typography from "@mui/material/Typography";
import { useAppSelector } from "../../app/hooks";
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import AddIcon from "@mui/icons-material/Add";
import { useState } from "react";
import CalendarPopover from "../../features/Calendars/CalendarModal";
import CalendarPopover from "./CalendarModal";
import { Calendars } from "../../features/Calendars/CalendarTypes";
import MoreVertIcon from "@mui/icons-material/MoreVert";
import IconButton from "@mui/material/IconButton";
import Checkbox from "@mui/material/Checkbox";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import CalendarSearch from "./CalendarSearch";
import { Divider, Menu, MenuItem } from "@mui/material";
import { removeCalendarAsync } from "../../features/Calendars/CalendarSlice";
import { DeleteCalendarDialog } from "./DeleteCalendarDialog";
function CalendarAccordion({
title,
@@ -59,21 +63,21 @@ function CalendarAccordion({
)}
</AccordionSummary>
<AccordionDetails>
{calendars.map((id) =>
CalendarSelector(
allCalendars,
id,
selectedCalendars,
handleToggle,
() => setOpen(id)
)
)}
{calendars.map((id) => (
<CalendarSelector
key={id}
calendars={allCalendars}
id={id}
isPersonnal={defaultExpanded}
selectedCalendars={selectedCalendars}
handleCalendarToggle={handleToggle}
setOpen={() => setOpen(id)}
/>
))}
</AccordionDetails>
</Accordion>
);
}
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
import CalendarSearch from "./CalendarSearch";
export default function CalendarSelection({
selectedCalendars,
@@ -151,7 +155,6 @@ export default function CalendarSelection({
/>
</div>
<CalendarPopover
anchorEl={anchorElCal}
open={Boolean(anchorElCal)}
calendar={calendars[selectedCalId] ?? undefined}
onClose={() => {
@@ -173,30 +176,79 @@ export default function CalendarSelection({
);
}
function CalendarSelector(
calendars: Record<string, Calendars>,
id: string,
selectedCalendars: string[],
handleCalendarToggle: (name: string) => void,
setOpen: Function
) {
function CalendarSelector({
calendars,
id,
isPersonnal,
selectedCalendars,
handleCalendarToggle,
setOpen,
}: {
calendars: Record<string, Calendars>;
id: string;
isPersonnal: boolean;
selectedCalendars: string[];
handleCalendarToggle: (name: string) => void;
setOpen: Function;
}) {
const dispatch = useAppDispatch();
const calLink =
useAppSelector((state) => state.calendars.list[id].link) ?? "";
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const [userId, calId] = id.split("/");
const isDefault = isPersonnal && userId === calId;
const [deletePopupOpen, setDeletePopupOpen] = useState(false);
const handleDeleteConfirm = () => {
dispatch(removeCalendarAsync({ calId: id, calLink }));
setDeletePopupOpen(false);
handleClose();
};
return (
<div key={id}>
<label>
<Checkbox
sx={{
color: calendars[id].color,
"&.Mui-checked": { color: calendars[id].color },
}}
size="small"
checked={selectedCalendars.includes(id)}
onChange={() => handleCalendarToggle(id)}
/>
{calendars[id].name}
</label>
<IconButton onClick={() => setOpen()}>
<MoreVertIcon />
</IconButton>
</div>
<>
<div>
<label>
<Checkbox
sx={{
color: calendars[id].color,
"&.Mui-checked": { color: calendars[id].color },
}}
size="small"
checked={selectedCalendars.includes(id)}
onChange={() => handleCalendarToggle(id)}
/>
{calendars[id].name}
</label>
<IconButton onClick={handleClick}>
<MoreVertIcon />
</IconButton>
</div>
<Menu id={id} anchorEl={anchorEl} open={open} onClose={handleClose}>
<MenuItem onClick={() => setOpen()}>Modify</MenuItem>
{!isDefault && <Divider />}
{!isDefault && (
<MenuItem onClick={() => setDeletePopupOpen(!deletePopupOpen)}>
{isPersonnal ? "Delete" : "Remove"}
</MenuItem>
)}
</Menu>
<DeleteCalendarDialog
deletePopupOpen={deletePopupOpen}
setDeletePopupOpen={setDeletePopupOpen}
calendars={calendars}
id={id}
isPersonnal={isPersonnal}
handleDeleteConfirm={handleDeleteConfirm}
/>
</>
);
}
@@ -0,0 +1,43 @@
import Dialog from "@mui/material/Dialog";
import { Calendars } from "../../features/Calendars/CalendarTypes";
import DialogTitle from "@mui/material/DialogTitle";
import DialogContent from "@mui/material/DialogContent";
import DialogContentText from "@mui/material/DialogContentText";
import DialogActions from "@mui/material/DialogActions";
import Button from "@mui/material/Button";
export function DeleteCalendarDialog({
deletePopupOpen,
setDeletePopupOpen,
calendars,
id,
isPersonnal,
handleDeleteConfirm,
}: {
deletePopupOpen: boolean;
setDeletePopupOpen: (e: boolean) => void;
calendars: Record<string, Calendars>;
id: string;
isPersonnal: boolean;
handleDeleteConfirm: () => void;
}) {
return (
<Dialog open={deletePopupOpen} onClose={() => setDeletePopupOpen(false)}>
<DialogTitle>Remove {calendars[id].name}?</DialogTitle>
<DialogContent>
<DialogContentText>
Are you sure you want to remove this calendar?{" "}
{isPersonnal
? "You will loose all events in this calendar."
: "You will loose access to its events. You will still be able to add it back later."}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setDeletePopupOpen(false)}>Cancel</Button>
<Button onClick={handleDeleteConfirm} variant="contained">
{isPersonnal ? "Delete" : "Remove"}
</Button>
</DialogActions>
</Dialog>
);
}
+25 -8
View File
@@ -66,18 +66,35 @@ export const extractEvents = (
export const updateCalsDetails = (
selectedCalendars: string[],
previousSelectedCalendars: string[],
pending: boolean,
calendars: Record<string, Calendars>,
rangeKey: string,
previousRangeKey: string,
dispatch: Function,
calendarRange: { start: Date; end: Date },
calType?: "temp"
) => {
selectedCalendars.forEach((id) => {
if (Object.keys(calendars[id].events).length > 0) {
return;
}
if (!pending && rangeKey) {
if (pending || !rangeKey) return;
const newCalendars = selectedCalendars.filter(
(id) => !previousSelectedCalendars.includes(id)
);
newCalendars.forEach((id) => {
dispatch(
getCalendarDetailAsync({
calId: id,
match: {
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
},
calType,
})
);
});
if (rangeKey !== previousRangeKey) {
selectedCalendars.forEach((id) => {
dispatch(
getCalendarDetailAsync({
calId: id,
@@ -88,6 +105,6 @@ export const updateCalsDetails = (
calType,
})
);
}
});
});
}
};