update cal details (#113)

* [#65] added button to access cal details

* [#65] added proppatch call

* [#65] added tests

* fixup! [#65] added proppatch call

* [#65] added trimming to name and desc

* fixup! [#65] added proppatch call

* fixup! [#65] added proppatch call

* fixup! [#65] added proppatch call

---------

Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
Camille Moussu
2025-09-17 15:05:27 +02:00
committed by GitHub
parent 5522673342
commit f640cac8ac
8 changed files with 358 additions and 85 deletions
+24
View File
@@ -51,3 +51,27 @@ export async function postCalendar(
});
return response;
}
export async function proppatchCalendar(
calLink: string,
patch: { name: string; desc: string; color: string }
) {
const body: Record<string, string> = {};
if (patch.name) {
body["dav:name"] = patch.name;
}
if (patch.desc) {
body["caldav:description"] = patch.desc;
}
if (patch.color) {
body["apple:color"] = patch.color;
}
const response = await api(`dav${calLink}`, {
method: "PROPPATCH",
headers: {
Accept: "application/json, text/plain, */*",
},
body: JSON.stringify(body),
});
return response;
}
+68 -31
View File
@@ -1,5 +1,8 @@
import { useState } from "react";
import { createCalendar, createCalendarAsync } from "./CalendarSlice";
import { useEffect, useState } from "react";
import {
createCalendarAsync /*, updateCalendarAsync */,
patchCalendarAsync,
} from "./CalendarSlice";
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import {
Popover,
@@ -7,38 +10,76 @@ import {
Button,
Box,
Typography,
Select,
ButtonGroup,
} from "@mui/material";
import { Calendars } from "./CalendarTypes";
function CalendarPopover({
anchorEl,
open,
onClose,
calendar,
}: {
anchorEl: HTMLElement | null;
open: boolean;
onClose: (Calendar: {}, reason: "backdropClick" | "escapeKeyDown") => void;
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("");
const [description, setDescription] = useState("");
const [color, setColor] = useState("");
const handleSave = () => {
const calId = crypto.randomUUID();
if (name) {
dispatch(
createCalendarAsync({ name, desc: description, color, userId, calId })
);
onClose({}, "backdropClick");
const [name, setName] = useState(calendar?.name ?? "");
const [description, setDescription] = useState(calendar?.description ?? "");
const [color, setColor] = useState(calendar?.color ?? "");
// Reset
setName("");
setDescription("");
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",
@@ -52,19 +93,14 @@ function CalendarPopover({
"#8E24AA",
"#616161",
];
return (
<Popover
open={open}
anchorEl={anchorEl}
onClose={onClose}
anchorOrigin={{
vertical: "center",
horizontal: "center",
}}
transformOrigin={{
vertical: "center",
horizontal: "center",
}}
onClose={(e, reason) => onClose(e, reason)}
anchorOrigin={{ vertical: "center", horizontal: "center" }}
transformOrigin={{ vertical: "center", horizontal: "center" }}
>
<Box p={2}>
<Typography
@@ -93,22 +129,23 @@ function CalendarPopover({
rows={2}
/>
<ButtonGroup>
{palette.map((color) => (
{palette.map((c) => (
<Button
style={{ backgroundColor: color }}
onClick={() => setColor(color)}
key={c}
style={{ backgroundColor: c }}
onClick={() => setColor(c)}
/>
))}
</ButtonGroup>
<Box mt={2} display="flex" justifyContent="flex-end" gap={1}>
<Button
variant="outlined"
onClick={() => onClose({}, "backdropClick")}
onClick={(e) => onClose({}, "backdropClick")}
>
Cancel
</Button>
<Button
disabled={name && name !== "" ? false : true}
disabled={!name.trim()}
variant="contained"
onClick={handleSave}
>
+57 -2
View File
@@ -1,7 +1,12 @@
import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit";
import { Calendars } from "./CalendarTypes";
import { CalendarEvent } from "../Events/EventsTypes";
import { getCalendar, getCalendars, postCalendar } from "./CalendarApi";
import {
getCalendar,
getCalendars,
postCalendar,
proppatchCalendar,
} from "./CalendarApi";
import { getOpenPaasUser, getUserDetails } from "../User/userAPI";
import { parseCalendarEvent } from "../Events/eventUtils";
import { deleteEvent, getEvent, moveEvent, putEvent } from "../Events/EventApi";
@@ -17,11 +22,12 @@ export const getCalendarsListAsync = createAsyncThunk<
for (const cal of rawCalendars) {
const name = cal["dav:name"];
const description = cal["dav:description"];
const description = cal["caldav:description"];
let delegated = false;
let source = cal["calendarserver:source"]
? cal["calendarserver:source"]._links.self.href
: cal._links.self.href;
const link = cal._links.self.href;
if (cal["calendarserver:delegatedsource"]) {
source = cal["calendarserver:delegatedsource"];
delegated = true;
@@ -33,6 +39,7 @@ export const getCalendarsListAsync = createAsyncThunk<
importedCalendars[id] = {
id,
name,
link,
owner: `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${
ownerData.lastname
}`,
@@ -121,6 +128,25 @@ export const getEventAsync = createAsyncThunk<
event: response,
};
});
export const patchCalendarAsync = createAsyncThunk<
{
calId: string;
calLink: string;
patch: { name: string; desc: string; color: string };
}, // Return type
{
calId: string;
calLink: string;
patch: { name: string; desc: string; color: string };
} // Arg type
>("calendars/patchCalendar", async ({ calId, calLink, patch }) => {
const response = await proppatchCalendar(calLink, patch);
return {
calId,
calLink,
patch,
};
});
export const moveEventAsync = createAsyncThunk<
{ calId: string; events: CalendarEvent[] }, // Return type
@@ -339,6 +365,32 @@ const CalendarSlice = createSlice({
name: action.payload.name,
} as unknown as Calendars;
})
.addCase(patchCalendarAsync.fulfilled, (state, action) => {
state.pending = false;
if (action.payload.patch.color) {
state.list[action.payload.calId] = {
...state.list[action.payload.calId],
color: action.payload.patch.color,
};
Object.keys(state.list[action.payload.calId].events).forEach(
(evId) =>
(state.list[action.payload.calId].events[evId].color =
action.payload.patch.color)
);
}
if (action.payload.patch.desc) {
state.list[action.payload.calId] = {
...state.list[action.payload.calId],
description: action.payload.patch.desc,
};
}
if (action.payload.patch.name) {
state.list[action.payload.calId] = {
...state.list[action.payload.calId],
name: action.payload.patch.name,
};
}
})
.addCase(getCalendarDetailAsync.pending, (state) => {
state.pending = true;
})
@@ -357,6 +409,9 @@ const CalendarSlice = createSlice({
.addCase(deleteEventAsync.pending, (state) => {
state.pending = true;
})
.addCase(patchCalendarAsync.pending, (state) => {
state.pending = true;
})
.addCase(createCalendarAsync.pending, (state) => {
state.pending = true;
});
+1
View File
@@ -2,6 +2,7 @@ import { CalendarEvent } from "../Events/EventsTypes";
export interface Calendars {
id: string;
link: string;
name: string;
delegated?: boolean;
prodid?: string;