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:
@@ -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<{
|
||||
|
||||
+29
-41
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user