[#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();
expect(screen.getByText("Test Event")).toBeInTheDocument();
preview.debug();
expect(screen.getByText(new RegExp(weekday, "i"))).toBeInTheDocument();
expect(screen.getByText(new RegExp(month, "i"))).toBeInTheDocument();
expect(
@@ -116,7 +116,12 @@ describe("EventPopover", () => {
expect(screen.getByLabelText("End")).toBeInTheDocument();
expect(screen.getByLabelText("Description")).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("Alarm")).toBeInTheDocument();
expect(screen.getByLabelText("Visibility")).toBeInTheDocument();
expect(screen.getByLabelText("is Busy")).toBeInTheDocument();
expect(screen.getByLabelText("Time Zone")).toBeInTheDocument();
// Calendar options
const select = screen.getByLabelText("Calendar");
+31 -6
View File
@@ -31,7 +31,12 @@ describe("parseCalendarEvent", () => {
["DTSTAMP", {}, "date-time", "2025-07-18T08:00:00Z"],
] 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.title).toBe("Team Meeting");
@@ -71,7 +76,12 @@ describe("parseCalendarEvent", () => {
["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");
});
@@ -81,7 +91,12 @@ describe("parseCalendarEvent", () => {
["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/);
const rawDataMissingStart: any = [["UID", {}, "text", "event-3"]];
@@ -89,7 +104,8 @@ describe("parseCalendarEvent", () => {
const result2 = parseCalendarEvent(
rawDataMissingStart,
baseColor,
calendarId
calendarId,
"/calendars/test.ics"
);
expect(result2.error).toMatch(/missing crucial event param/);
});
@@ -102,7 +118,12 @@ describe("parseCalendarEvent", () => {
["ORGANIZER", {}, "cal-address", "jane@example.com"],
] 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([
{
@@ -135,6 +156,8 @@ describe("calendarEventToJCal", () => {
it("should convert a CalendarEvent to JCal format", () => {
const mockEvent = {
uid: "event-123",
URL: "/calendars/test.ics",
calId: "test/test",
title: "Team Meeting",
start: new Date("2025-07-23T10: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", () => {
const mockEvent = {
uid: "event-123",
URL: "/calendars/test.ics",
calId: "test/test",
title: "Team Meeting",
start: new Date("2025-07-23"),
end: new Date("2025-07-23"),
@@ -285,7 +310,7 @@ describe("calendarEventToJCal", () => {
["summary", {}, "text", "Team Meeting"],
["transp", {}, "text", "OPAQUE"],
["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"],
["location", {}, "text", "Room 101"],
["description", {}, "text", "Discuss project roadmap."],
+1 -1
View File
@@ -372,7 +372,7 @@ export default function CalendarApp() {
start: computedNewStart,
end: computedNewEnd,
} as CalendarEvent;
console.log(event , newEvent);
dispatch(
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) {
const response = await api(eventURL, {
const response = await api(`dav${eventURL}`, {
method: "DELETE",
}).json();
return response;
+22 -8
View File
@@ -321,7 +321,20 @@ export default function EventDisplayModal({
<Checkbox
disabled={!isOwn}
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"
@@ -407,8 +420,9 @@ export default function EventDisplayModal({
{showMore && (
<>
<RepeatEvent
eventClass={eventClass}
setEventClass={setEventClass}
repetition={repetition}
setRepetition={setRepetition}
isOwn={isOwn}
/>
<FormControl fullWidth margin="dense" size="small">
@@ -436,10 +450,10 @@ export default function EventDisplayModal({
</FormControl>
<FormControl fullWidth margin="dense" size="small">
<InputLabel id="class">Class</InputLabel>
<InputLabel id="Visibility">Visibility</InputLabel>
<Select
labelId="class"
label="class"
labelId="Visibility"
label="Visibility"
value={eventClass}
disabled={!isOwn}
onChange={(e: SelectChangeEvent) =>
@@ -459,7 +473,7 @@ export default function EventDisplayModal({
disabled={!isOwn}
label="is busy"
onChange={(e: SelectChangeEvent) =>
setEventClass(e.target.value)
console.log(e.target.value)
}
>
<MenuItem value={"free"}>Free</MenuItem>
@@ -525,7 +539,7 @@ export function InfoRow({
<Box sx={{ display: "flex", alignItems: "center", gap: 1, mb: 1 }}>
{icon}
<Typography variant="body2" color={error ? "error" : "textPrimary"}>
{isValidUrl(data) && <Link href={data}>{text}</Link>}
{isValidUrl(data) ? <Link href={data}>{text}</Link> : text}
</Typography>
</Box>
);
+62 -13
View File
@@ -162,12 +162,10 @@ export default function EventPreviewModal({
{/* Time info*/}
<Typography variant="body2" color="textSecondary" gutterBottom>
{formatDate(event.start)}
{formatDate(event.start, event.allday)}
{event.end &&
` ${new Date(event.end).toLocaleTimeString(undefined, {
hour: "2-digit",
minute: "2-digit",
})}`}
formatEnd(event.start, event.end, event.allday) &&
` ${formatEnd(event.start, event.end, event.allday)}`}
</Typography>
{/* Location */}
@@ -296,12 +294,63 @@ export default function EventPreviewModal({
);
}
function formatDate(date: Date) {
return new Date(date).toLocaleString(undefined, {
weekday: "long",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
function formatDate(date: Date, allday?: boolean) {
if (allday) {
return new Date(date).toLocaleDateString(undefined, {
year: "numeric",
month: "long",
weekday: "long",
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"
checked={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);
console.log("formatedd", formatLocalDateTime(endDate));
setEnd(formatLocalDateTime(endDate));
}
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,
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);
calendarRef.current?.select(newRange);
}}
/>
All day
@@ -263,27 +282,14 @@ function EventPopover({
size="small"
margin="dense"
/>
<AttendeeSelector setAttendees={setAttendees} />
{/* Extended options */}
{showMore && (
<>
<FormControl fullWidth margin="dense" size="small">
<InputLabel id="repeat">Repetition</InputLabel>
<Select
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} />
<RepeatEvent
repetition={repetition}
setRepetition={setRepetition}
/>
<FormControl fullWidth margin="dense" size="small">
<InputLabel id="timezone-select-label">Time Zone</InputLabel>
<Select
@@ -325,10 +331,10 @@ function EventPopover({
</FormControl>
<FormControl fullWidth margin="dense" size="small">
<InputLabel id="class">Class</InputLabel>
<InputLabel id="Visibility">Visibility</InputLabel>
<Select
labelId="class"
label="class"
labelId="Visibility"
label="Visibility"
value={eventClass}
onChange={(e: SelectChangeEvent) =>
setEventClass(e.target.value)
@@ -339,10 +345,21 @@ function EventPopover({
<MenuItem value={"PRIVATE"}>Private</MenuItem>
</Select>
</FormControl>
<RepeatEvent
eventClass={eventClass}
setEventClass={setEventClass}
/>
<FormControl fullWidth margin="dense" size="small">
<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>
+17 -11
View File
@@ -7,23 +7,29 @@ import {
} from "@mui/material";
export default function RepeatEvent({
eventClass,
setEventClass,
repetition,
setRepetition,
isOwn = true,
}: {
eventClass: string;
setEventClass: React.Dispatch<React.SetStateAction<string>>;
repetition: string;
setRepetition: Function;
isOwn?: boolean;
}) {
return (
<FormControl fullWidth margin="dense" size="small">
<InputLabel id="repeat">is Busy</InputLabel>
<InputLabel id="repeat">Repetition</InputLabel>
<Select
labelId="busy"
value={eventClass}
label="is busy"
onChange={(e: SelectChangeEvent) => setEventClass(e.target.value)}
labelId="repeat"
value={repetition}
disabled={!isOwn}
label="Repetition"
onChange={(e: SelectChangeEvent) => setRepetition(e.target.value)}
>
<MenuItem value={"free"}>Free</MenuItem>
<MenuItem value={"busy"}>Busy </MenuItem>
<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>
);
+1 -1
View File
@@ -8,7 +8,7 @@ export interface CalendarEvent {
start: Date; // ISO date
end?: Date;
class?: string;
x_openpass_videoconference?: unknown;
x_openpass_videoconference?: string;
title?: string;
description?: string;
location?: string;
+19
View File
@@ -13,6 +13,7 @@ export function parseCalendarEvent(
): CalendarEvent {
const event: Partial<CalendarEvent> = { color, attendee: [] };
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) {
switch (key.toLowerCase()) {
@@ -24,9 +25,19 @@ export function parseCalendarEvent(
break;
case "dtstart":
event.start = value;
if (dateRegex.test(value)) {
event.allday = true;
} else {
event.allday = false;
}
break;
case "dtend":
event.end = value;
if (dateRegex.test(value)) {
event.allday = true;
} else {
event.allday = false;
}
break;
case "class":
event.class = value;
@@ -115,6 +126,14 @@ export function calendarEventToJCal(event: CalendarEvent): any[] {
];
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([
"dtend",
{ tzid },
+2 -2
View File
@@ -44,11 +44,11 @@ export function getLocation() {
return window.location.href;
}
export function isValidUrl(string: string) {
export function isValidUrl(string?: string) {
let url;
try {
url = new URL(string);
url = new URL(string ?? "");
} catch (_) {
return false;
}