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<{
@@ -2,26 +2,24 @@ import { useEffect, useState } from "react";
import {
createCalendarAsync /*, updateCalendarAsync */,
patchCalendarAsync,
} from "./CalendarSlice";
} from "../../features/Calendars/CalendarSlice";
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import {
Popover,
TextField,
Button,
Box,
Typography,
ButtonGroup,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
} from "@mui/material";
import { ColorPicker } from "../../components/Calendar/CalendarColorPicker";
import { Calendars } from "./CalendarTypes";
import { ColorPicker } from "./CalendarColorPicker";
import { Calendars } from "../../features/Calendars/CalendarTypes";
function CalendarPopover({
anchorEl,
open,
onClose,
calendar,
}: {
anchorEl: HTMLElement | null;
open: boolean;
onClose: (
event: object | null,
@@ -96,21 +94,11 @@ function CalendarPopover({
];
return (
<Popover
open={open}
anchorEl={anchorEl}
onClose={(e, reason) => onClose(e, reason)}
anchorOrigin={{ vertical: "center", horizontal: "center" }}
transformOrigin={{ vertical: "center", horizontal: "center" }}
>
<Box p={2}>
<Typography
variant="h6"
gutterBottom
style={{ backgroundColor: color }}
>
Calendar configuration
</Typography>
<Dialog open={open} onClose={(e, reason) => onClose(e, reason)}>
<DialogTitle style={{ backgroundColor: color }}>
Calendar configuration
</DialogTitle>
<DialogContent>
<TextField
fullWidth
label="Name"
@@ -133,24 +121,24 @@ function CalendarPopover({
onChange={(color) => setColor(color)}
selectedColor={color}
/>
</DialogContent>
<Box mt={2} display="flex" justifyContent="flex-end" gap={1}>
<Button
variant="outlined"
onClick={(e) => onClose({}, "backdropClick")}
>
Cancel
</Button>
<Button
disabled={!name.trim()}
variant="contained"
onClick={handleSave}
>
Save
</Button>
</Box>
</Box>
</Popover>
<DialogActions>
<Button
variant="outlined"
onClick={(e) => onClose({}, "backdropClick")}
>
Cancel
</Button>
<Button
disabled={!name.trim()}
variant="contained"
onClick={handleSave}
>
Save
</Button>
</DialogActions>
</Dialog>
);
}
+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,
})
);
}
});
});
}
};
+10
View File
@@ -102,3 +102,13 @@ export async function proppatchCalendar(
});
return response;
}
export async function removeCalendar(calLink: string) {
const response = await api(`dav${calLink}`, {
method: "DELETE",
headers: {
Accept: "application/json, text/plain, */*",
},
});
return response;
}
+27
View File
@@ -7,6 +7,7 @@ import {
getCalendars,
postCalendar,
proppatchCalendar,
removeCalendar,
} from "./CalendarApi";
import { getOpenPaasUser, getUserDetails } from "../User/userAPI";
import { parseCalendarEvent } from "../Events/eventUtils";
@@ -191,6 +192,22 @@ export const patchCalendarAsync = createAsyncThunk<
};
});
export const removeCalendarAsync = createAsyncThunk<
{
calId: string;
},
{
calId: string;
calLink: string;
}
>("calendars/removeCalendar", async ({ calId, calLink }) => {
const response = await removeCalendar(calLink);
return {
calId,
calLink,
};
});
export const moveEventAsync = createAsyncThunk<
{ calId: string; events: CalendarEvent[] }, // Return type
{ cal: Calendars; newEvent: CalendarEvent; newURL: string } // Arg type
@@ -258,6 +275,7 @@ export const addSharedCalendarAsync = createAsyncThunk<
{
calId: string;
color: string;
link: string;
name: string;
desc: string;
owner: string;
@@ -278,6 +296,7 @@ export const addSharedCalendarAsync = createAsyncThunk<
.replace("/calendars/", "")
.replace(".json", ""),
color: cal.cal["apple:color"],
link: `/calendars/${userId}/${calId}.json`,
desc: cal.cal["caldav:description"],
name: cal.cal["dav:name"],
owner: `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${
@@ -527,6 +546,7 @@ const CalendarSlice = createSlice({
state.list[action.payload.calId] = {
color: action.payload.color,
id: action.payload.calId,
link: action.payload.link,
description: action.payload.desc,
name: action.payload.name,
events: {},
@@ -534,6 +554,10 @@ const CalendarSlice = createSlice({
ownerEmails: action.payload.ownerEmails,
} as Calendars;
})
.addCase(removeCalendarAsync.fulfilled, (state, action) => {
state.pending = false;
delete state.list[action.payload.calId];
})
.addCase(getCalendarDetailAsync.pending, (state) => {
state.pending = true;
})
@@ -563,6 +587,9 @@ const CalendarSlice = createSlice({
})
.addCase(addSharedCalendarAsync.pending, (state) => {
state.pending = true;
})
.addCase(removeCalendarAsync.pending, (state) => {
state.pending = true;
});
},
});