[#7] fixed allday implementation to prevent errors with openpaas

This commit is contained in:
Camille Moussu
2025-08-27 14:55:14 +02:00
parent b1c3331518
commit 6669eeb456
12 changed files with 210 additions and 74 deletions
@@ -109,6 +109,7 @@ describe("Event Display", () => {
const dayOfMonth = day.getDate().toString(); const dayOfMonth = day.getDate().toString();
expect(screen.getByText("Test Event")).toBeInTheDocument(); expect(screen.getByText("Test Event")).toBeInTheDocument();
preview.debug();
expect(screen.getByText(new RegExp(weekday, "i"))).toBeInTheDocument(); expect(screen.getByText(new RegExp(weekday, "i"))).toBeInTheDocument();
expect(screen.getByText(new RegExp(month, "i"))).toBeInTheDocument(); expect(screen.getByText(new RegExp(month, "i"))).toBeInTheDocument();
expect( expect(
@@ -116,7 +116,12 @@ describe("EventPopover", () => {
expect(screen.getByLabelText("End")).toBeInTheDocument(); expect(screen.getByLabelText("End")).toBeInTheDocument();
expect(screen.getByLabelText("Description")).toBeInTheDocument(); expect(screen.getByLabelText("Description")).toBeInTheDocument();
expect(screen.getByLabelText("Location")).toBeInTheDocument(); expect(screen.getByLabelText("Location")).toBeInTheDocument();
expect(screen.getByText("Show More")).toBeInTheDocument();
fireEvent.click(screen.getByText("Show More"));
expect(screen.getByLabelText("Repetition")).toBeInTheDocument(); expect(screen.getByLabelText("Repetition")).toBeInTheDocument();
expect(screen.getByLabelText("Alarm")).toBeInTheDocument();
expect(screen.getByLabelText("Visibility")).toBeInTheDocument();
expect(screen.getByLabelText("is Busy")).toBeInTheDocument();
expect(screen.getByLabelText("Time Zone")).toBeInTheDocument(); expect(screen.getByLabelText("Time Zone")).toBeInTheDocument();
// Calendar options // Calendar options
const select = screen.getByLabelText("Calendar"); const select = screen.getByLabelText("Calendar");
+31 -6
View File
@@ -31,7 +31,12 @@ describe("parseCalendarEvent", () => {
["DTSTAMP", {}, "date-time", "2025-07-18T08:00:00Z"], ["DTSTAMP", {}, "date-time", "2025-07-18T08:00:00Z"],
] as unknown as [string, Record<string, string>, string, any]; ] as unknown as [string, Record<string, string>, string, any];
const result = parseCalendarEvent(rawData, baseColor, calendarId); const result = parseCalendarEvent(
rawData,
baseColor,
calendarId,
"/calendars/test.ics"
);
expect(result.uid).toBe("event-1"); expect(result.uid).toBe("event-1");
expect(result.title).toBe("Team Meeting"); expect(result.title).toBe("Team Meeting");
@@ -71,7 +76,12 @@ describe("parseCalendarEvent", () => {
["DTSTART", {}, "date-time", "2025-07-18T09:00:00Z"], ["DTSTART", {}, "date-time", "2025-07-18T09:00:00Z"],
]; ];
const result = parseCalendarEvent(rawData, baseColor, calendarId); const result = parseCalendarEvent(
rawData,
baseColor,
calendarId,
"/calendars/test.ics"
);
expect(result.uid).toBe("event-2/2025-07-18T09:00:00Z"); expect(result.uid).toBe("event-2/2025-07-18T09:00:00Z");
}); });
@@ -81,7 +91,12 @@ describe("parseCalendarEvent", () => {
["DTSTART", {}, "date-time", "2025-07-18T09:00:00Z"], ["DTSTART", {}, "date-time", "2025-07-18T09:00:00Z"],
]; ];
const result = parseCalendarEvent(rawDataMissingUid, baseColor, calendarId); const result = parseCalendarEvent(
rawDataMissingUid,
baseColor,
calendarId,
"/calendars/test.ics"
);
expect(result.error).toMatch(/missing crucial event param/); expect(result.error).toMatch(/missing crucial event param/);
const rawDataMissingStart: any = [["UID", {}, "text", "event-3"]]; const rawDataMissingStart: any = [["UID", {}, "text", "event-3"]];
@@ -89,7 +104,8 @@ describe("parseCalendarEvent", () => {
const result2 = parseCalendarEvent( const result2 = parseCalendarEvent(
rawDataMissingStart, rawDataMissingStart,
baseColor, baseColor,
calendarId calendarId,
"/calendars/test.ics"
); );
expect(result2.error).toMatch(/missing crucial event param/); expect(result2.error).toMatch(/missing crucial event param/);
}); });
@@ -102,7 +118,12 @@ describe("parseCalendarEvent", () => {
["ORGANIZER", {}, "cal-address", "jane@example.com"], ["ORGANIZER", {}, "cal-address", "jane@example.com"],
] as unknown as [string, Record<string, string>, string, any]; ] as unknown as [string, Record<string, string>, string, any];
const result = parseCalendarEvent(rawData, baseColor, calendarId); const result = parseCalendarEvent(
rawData,
baseColor,
calendarId,
"/calendars/test.ics"
);
expect(result.attendee).toEqual([ expect(result.attendee).toEqual([
{ {
@@ -135,6 +156,8 @@ describe("calendarEventToJCal", () => {
it("should convert a CalendarEvent to JCal format", () => { it("should convert a CalendarEvent to JCal format", () => {
const mockEvent = { const mockEvent = {
uid: "event-123", uid: "event-123",
URL: "/calendars/test.ics",
calId: "test/test",
title: "Team Meeting", title: "Team Meeting",
start: new Date("2025-07-23T10:00:00"), start: new Date("2025-07-23T10:00:00"),
end: new Date("2025-07-23T11:00:00"), end: new Date("2025-07-23T11:00:00"),
@@ -246,6 +269,8 @@ describe("calendarEventToJCal", () => {
it("should convert a CalendarEvent to JCal format, with all day activated", () => { it("should convert a CalendarEvent to JCal format, with all day activated", () => {
const mockEvent = { const mockEvent = {
uid: "event-123", uid: "event-123",
URL: "/calendars/test.ics",
calId: "test/test",
title: "Team Meeting", title: "Team Meeting",
start: new Date("2025-07-23"), start: new Date("2025-07-23"),
end: new Date("2025-07-23"), end: new Date("2025-07-23"),
@@ -285,7 +310,7 @@ describe("calendarEventToJCal", () => {
["summary", {}, "text", "Team Meeting"], ["summary", {}, "text", "Team Meeting"],
["transp", {}, "text", "OPAQUE"], ["transp", {}, "text", "OPAQUE"],
["dtstart", { tzid: "Europe/Paris" }, "date", "2025-07-23"], ["dtstart", { tzid: "Europe/Paris" }, "date", "2025-07-23"],
["dtend", { tzid: "Europe/Paris" }, "date", "2025-07-23"], ["dtend", { tzid: "Europe/Paris" }, "date", "2025-07-24"],
["class", {}, "text", "PUBLIC"], ["class", {}, "text", "PUBLIC"],
["location", {}, "text", "Room 101"], ["location", {}, "text", "Room 101"],
["description", {}, "text", "Discuss project roadmap."], ["description", {}, "text", "Discuss project roadmap."],
+1 -1
View File
@@ -372,7 +372,7 @@ export default function CalendarApp() {
start: computedNewStart, start: computedNewStart,
end: computedNewEnd, end: computedNewEnd,
} as CalendarEvent; } as CalendarEvent;
console.log(event , newEvent);
dispatch( dispatch(
putEventAsync({ cal: calendars[newEvent.calId], newEvent }) putEventAsync({ cal: calendars[newEvent.calId], newEvent })
); );
+1 -1
View File
@@ -17,7 +17,7 @@ export async function putEvent(event: CalendarEvent) {
} }
export async function deleteEvent(eventURL: string) { export async function deleteEvent(eventURL: string) {
const response = await api(eventURL, { const response = await api(`dav${eventURL}`, {
method: "DELETE", method: "DELETE",
}).json(); }).json();
return response; return response;
+22 -8
View File
@@ -321,7 +321,20 @@ export default function EventDisplayModal({
<Checkbox <Checkbox
disabled={!isOwn} disabled={!isOwn}
checked={allday} checked={allday}
onChange={() => setAllDay(!allday)} onChange={() => {
const endDate = new Date(end);
const startDate = new Date(start);
setAllDay(!allday);
console.log(
endDate.getDate() === startDate.getDate(),
endDate.getDate(),
startDate.getDate()
);
if (endDate.getDate() === startDate.getDate()) {
endDate.setDate(startDate.getDate() + 1);
setEnd(formatLocalDateTime(endDate));
}
}}
/> />
} }
label="All day" label="All day"
@@ -407,8 +420,9 @@ export default function EventDisplayModal({
{showMore && ( {showMore && (
<> <>
<RepeatEvent <RepeatEvent
eventClass={eventClass} repetition={repetition}
setEventClass={setEventClass} setRepetition={setRepetition}
isOwn={isOwn}
/> />
<FormControl fullWidth margin="dense" size="small"> <FormControl fullWidth margin="dense" size="small">
@@ -436,10 +450,10 @@ export default function EventDisplayModal({
</FormControl> </FormControl>
<FormControl fullWidth margin="dense" size="small"> <FormControl fullWidth margin="dense" size="small">
<InputLabel id="class">Class</InputLabel> <InputLabel id="Visibility">Visibility</InputLabel>
<Select <Select
labelId="class" labelId="Visibility"
label="class" label="Visibility"
value={eventClass} value={eventClass}
disabled={!isOwn} disabled={!isOwn}
onChange={(e: SelectChangeEvent) => onChange={(e: SelectChangeEvent) =>
@@ -459,7 +473,7 @@ export default function EventDisplayModal({
disabled={!isOwn} disabled={!isOwn}
label="is busy" label="is busy"
onChange={(e: SelectChangeEvent) => onChange={(e: SelectChangeEvent) =>
setEventClass(e.target.value) console.log(e.target.value)
} }
> >
<MenuItem value={"free"}>Free</MenuItem> <MenuItem value={"free"}>Free</MenuItem>
@@ -525,7 +539,7 @@ export function InfoRow({
<Box sx={{ display: "flex", alignItems: "center", gap: 1, mb: 1 }}> <Box sx={{ display: "flex", alignItems: "center", gap: 1, mb: 1 }}>
{icon} {icon}
<Typography variant="body2" color={error ? "error" : "textPrimary"}> <Typography variant="body2" color={error ? "error" : "textPrimary"}>
{isValidUrl(data) && <Link href={data}>{text}</Link>} {isValidUrl(data) ? <Link href={data}>{text}</Link> : text}
</Typography> </Typography>
</Box> </Box>
); );
+62 -13
View File
@@ -162,12 +162,10 @@ export default function EventPreviewModal({
{/* Time info*/} {/* Time info*/}
<Typography variant="body2" color="textSecondary" gutterBottom> <Typography variant="body2" color="textSecondary" gutterBottom>
{formatDate(event.start)} {formatDate(event.start, event.allday)}
{event.end && {event.end &&
` ${new Date(event.end).toLocaleTimeString(undefined, { formatEnd(event.start, event.end, event.allday) &&
hour: "2-digit", ` ${formatEnd(event.start, event.end, event.allday)}`}
minute: "2-digit",
})}`}
</Typography> </Typography>
{/* Location */} {/* Location */}
@@ -296,12 +294,63 @@ export default function EventPreviewModal({
); );
} }
function formatDate(date: Date) { function formatDate(date: Date, allday?: boolean) {
return new Date(date).toLocaleString(undefined, { if (allday) {
weekday: "long", return new Date(date).toLocaleDateString(undefined, {
month: "long", year: "numeric",
day: "numeric", month: "long",
hour: "2-digit", weekday: "long",
minute: "2-digit", day: "numeric",
}); });
} else {
return new Date(date).toLocaleString(undefined, {
year: "numeric",
month: "long",
weekday: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
}
}
function formatEnd(start: Date, end: Date, allday?: boolean) {
const startDate = new Date(start);
const endDate = new Date(end);
const sameDay =
startDate.getFullYear() === endDate.getFullYear() &&
startDate.getMonth() === endDate.getMonth() &&
startDate.getDate() === endDate.getDate();
if (allday) {
console.log(
endDate.toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
})
);
return sameDay
? null
: endDate.toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
});
} else {
if (sameDay) {
return endDate.toLocaleTimeString(undefined, {
hour: "2-digit",
minute: "2-digit",
});
}
return endDate.toLocaleString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
}
} }
+48 -31
View File
@@ -230,17 +230,36 @@ function EventPopover({
type="checkbox" type="checkbox"
checked={allday} checked={allday}
onChange={() => { onChange={() => {
const endDate = new Date(end);
const startDate = new Date(start);
setAllDay(!allday); setAllDay(!allday);
console.log(
endDate.getDate() === startDate.getDate(),
endDate.getDate(),
startDate.getDate()
);
if (endDate.getDate() === startDate.getDate()) {
endDate.setDate(startDate.getDate() + 1);
console.log("formatedd", formatLocalDateTime(endDate));
setEnd(formatLocalDateTime(endDate));
}
const newRange = { const newRange = {
startStr: allday ? start.split("T")[0] : start,
endStr: allday ? end.split("T")[0] : end,
start: new Date(allday ? start.split("T")[0] : start),
end: new Date(allday ? end.split("T")[0] : end),
allday,
...selectedRange, ...selectedRange,
startStr: allday ? start.split("T")[0] : start,
endStr: allday
? endDate.toISOString().split("T")[0]
: endDate.toISOString(),
start: new Date(allday ? start.split("T")[0] : start),
end: new Date(
allday
? endDate.toISOString().split("T")[0]
: endDate.toISOString()
),
allDay: allday,
}; };
console.log(newRange, selectedRange);
setSelectedRange(newRange); setSelectedRange(newRange);
calendarRef.current?.select(newRange);
}} }}
/> />
All day All day
@@ -263,27 +282,14 @@ function EventPopover({
size="small" size="small"
margin="dense" margin="dense"
/> />
<AttendeeSelector setAttendees={setAttendees} />
{/* Extended options */} {/* Extended options */}
{showMore && ( {showMore && (
<> <>
<FormControl fullWidth margin="dense" size="small"> <RepeatEvent
<InputLabel id="repeat">Repetition</InputLabel> repetition={repetition}
<Select setRepetition={setRepetition}
labelId="repeat" />
value={repetition}
label="Time Zone"
onChange={(e: SelectChangeEvent) =>
setRepetition(e.target.value)
}
>
<MenuItem value={""}>No Repetition</MenuItem>
<MenuItem value={"daily"}>Repeat daily</MenuItem>
<MenuItem value={"weekly"}>Repeat weekly</MenuItem>
<MenuItem value={"monthly"}>Repeat monthly</MenuItem>
<MenuItem value={"yearly"}>Repeat yearly</MenuItem>
</Select>
</FormControl>
<AttendeeSelector setAttendees={setAttendees} />
<FormControl fullWidth margin="dense" size="small"> <FormControl fullWidth margin="dense" size="small">
<InputLabel id="timezone-select-label">Time Zone</InputLabel> <InputLabel id="timezone-select-label">Time Zone</InputLabel>
<Select <Select
@@ -325,10 +331,10 @@ function EventPopover({
</FormControl> </FormControl>
<FormControl fullWidth margin="dense" size="small"> <FormControl fullWidth margin="dense" size="small">
<InputLabel id="class">Class</InputLabel> <InputLabel id="Visibility">Visibility</InputLabel>
<Select <Select
labelId="class" labelId="Visibility"
label="class" label="Visibility"
value={eventClass} value={eventClass}
onChange={(e: SelectChangeEvent) => onChange={(e: SelectChangeEvent) =>
setEventClass(e.target.value) setEventClass(e.target.value)
@@ -339,10 +345,21 @@ function EventPopover({
<MenuItem value={"PRIVATE"}>Private</MenuItem> <MenuItem value={"PRIVATE"}>Private</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
<RepeatEvent
eventClass={eventClass} <FormControl fullWidth margin="dense" size="small">
setEventClass={setEventClass} <InputLabel id="busy">is Busy</InputLabel>
/> <Select
labelId="busy"
value={eventClass}
label="is busy"
onChange={(e: SelectChangeEvent) =>
console.log(e.target.value)
}
>
<MenuItem value={"free"}>Free</MenuItem>
<MenuItem value={"busy"}>Busy </MenuItem>
</Select>
</FormControl>
</> </>
)} )}
</CardContent> </CardContent>
+17 -11
View File
@@ -7,23 +7,29 @@ import {
} from "@mui/material"; } from "@mui/material";
export default function RepeatEvent({ export default function RepeatEvent({
eventClass, repetition,
setEventClass, setRepetition,
isOwn = true,
}: { }: {
eventClass: string; repetition: string;
setEventClass: React.Dispatch<React.SetStateAction<string>>; setRepetition: Function;
isOwn?: boolean;
}) { }) {
return ( return (
<FormControl fullWidth margin="dense" size="small"> <FormControl fullWidth margin="dense" size="small">
<InputLabel id="repeat">is Busy</InputLabel> <InputLabel id="repeat">Repetition</InputLabel>
<Select <Select
labelId="busy" labelId="repeat"
value={eventClass} value={repetition}
label="is busy" disabled={!isOwn}
onChange={(e: SelectChangeEvent) => setEventClass(e.target.value)} label="Repetition"
onChange={(e: SelectChangeEvent) => setRepetition(e.target.value)}
> >
<MenuItem value={"free"}>Free</MenuItem> <MenuItem value={""}>No Repetition</MenuItem>
<MenuItem value={"busy"}>Busy </MenuItem> <MenuItem value={"daily"}>Repeat daily</MenuItem>
<MenuItem value={"weekly"}>Repeat weekly</MenuItem>
<MenuItem value={"monthly"}>Repeat monthly</MenuItem>
<MenuItem value={"yearly"}>Repeat yearly</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
); );
+1 -1
View File
@@ -8,7 +8,7 @@ export interface CalendarEvent {
start: Date; // ISO date start: Date; // ISO date
end?: Date; end?: Date;
class?: string; class?: string;
x_openpass_videoconference?: unknown; x_openpass_videoconference?: string;
title?: string; title?: string;
description?: string; description?: string;
location?: string; location?: string;
+19
View File
@@ -13,6 +13,7 @@ export function parseCalendarEvent(
): CalendarEvent { ): CalendarEvent {
const event: Partial<CalendarEvent> = { color, attendee: [] }; const event: Partial<CalendarEvent> = { color, attendee: [] };
let recurrenceId; let recurrenceId;
const dateRegex = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/;
for (const [key, params, type, value] of data) { for (const [key, params, type, value] of data) {
switch (key.toLowerCase()) { switch (key.toLowerCase()) {
@@ -24,9 +25,19 @@ export function parseCalendarEvent(
break; break;
case "dtstart": case "dtstart":
event.start = value; event.start = value;
if (dateRegex.test(value)) {
event.allday = true;
} else {
event.allday = false;
}
break; break;
case "dtend": case "dtend":
event.end = value; event.end = value;
if (dateRegex.test(value)) {
event.allday = true;
} else {
event.allday = false;
}
break; break;
case "class": case "class":
event.class = value; event.class = value;
@@ -115,6 +126,14 @@ export function calendarEventToJCal(event: CalendarEvent): any[] {
]; ];
if (event.end) { if (event.end) {
console.log(
event.end,
event.start,
event.end.getTime() === event.start.getTime()
);
if (event.allday && event.end.getTime() === event.start.getTime()) {
event.end.setDate(event.start.getDate() + 1);
}
vevent[1].push([ vevent[1].push([
"dtend", "dtend",
{ tzid }, { tzid },
+2 -2
View File
@@ -44,11 +44,11 @@ export function getLocation() {
return window.location.href; return window.location.href;
} }
export function isValidUrl(string: string) { export function isValidUrl(string?: string) {
let url; let url;
try { try {
url = new URL(string); url = new URL(string ?? "");
} catch (_) { } catch (_) {
return false; return false;
} }