From b87b77beefe31e70d21e57096622c7068a56ac18 Mon Sep 17 00:00:00 2001 From: Camille Moussu Date: Mon, 8 Sep 2025 17:35:16 +0200 Subject: [PATCH 1/4] [#78] added valarm support --- src/features/Calendars/CalendarSlice.ts | 12 +++++- src/features/Events/EventApi.ts | 4 +- src/features/Events/EventDisplay.tsx | 52 ++++++++++++------------- src/features/Events/EventModal.tsx | 1 + src/features/Events/EventsTypes.ts | 6 +++ src/features/Events/eventUtils.ts | 42 ++++++++++++++++++-- 6 files changed, 83 insertions(+), 34 deletions(-) diff --git a/src/features/Calendars/CalendarSlice.ts b/src/features/Calendars/CalendarSlice.ts index e8273fe..3c64aac 100644 --- a/src/features/Calendars/CalendarSlice.ts +++ b/src/features/Calendars/CalendarSlice.ts @@ -56,9 +56,10 @@ export const getCalendarDetailAsync = createAsyncThunk< const events: CalendarEvent[] = calendar._embedded["dav:item"].flatMap( (eventdata: any) => { const vevents = eventdata.data[2] as any[][]; // array of ['vevent', RawEntry[], []] + const valarm = eventdata.data[2][0][2][0]; const eventURL = eventdata._links.self.href; return vevents.map((vevent: any[]) => { - return parseCalendarEvent(vevent[1], color, calId, eventURL); + return parseCalendarEvent(vevent[1], color, calId, eventURL, valarm); }); } ); @@ -88,8 +89,15 @@ export const putEventAsync = createAsyncThunk< (eventdata: any) => { const vevents = eventdata.data[2] as any[][]; const eventURL = eventdata._links.self.href; + const valarm = eventdata.data[3][0][1]; return vevents.map((vevent: any[]) => { - return parseCalendarEvent(vevent[1], cal.color ?? "", cal.id, eventURL); + return parseCalendarEvent( + vevent[1], + cal.color ?? "", + cal.id, + eventURL, + valarm + ); }); } ); diff --git a/src/features/Events/EventApi.ts b/src/features/Events/EventApi.ts index 734c9f3..e7a616b 100644 --- a/src/features/Events/EventApi.ts +++ b/src/features/Events/EventApi.ts @@ -16,10 +16,10 @@ export async function getEvent(event: CalendarEvent) { return { ...eventjson, ...event }; } -export async function putEvent(event: CalendarEvent) { +export async function putEvent(event: CalendarEvent, calOwnerEmail?: string) { const response = await api(`dav${event.URL}`, { method: "PUT", - body: JSON.stringify(calendarEventToJCal(event)), + body: JSON.stringify(calendarEventToJCal(event, calOwnerEmail)), headers: { "content-type": "text/calendar; charset=utf-8", }, diff --git a/src/features/Events/EventDisplay.tsx b/src/features/Events/EventDisplay.tsx index e6c9ccd..e9f9472 100644 --- a/src/features/Events/EventDisplay.tsx +++ b/src/features/Events/EventDisplay.tsx @@ -91,7 +91,7 @@ export default function EventDisplayModal({ const [repetition, setRepetition] = useState( event.repetition ?? ({} as RepetitionObject) ); - const [alarm, setAlarm] = useState(""); + const [alarm, setAlarm] = useState(event.alarm.trigger); const [busy, setBusy] = useState(""); const [eventClass, setEventClass] = useState(event?.class ?? "PUBLIC"); const [timezone, setTimezone] = useState(event?.timezone ?? "UTC"); @@ -160,6 +160,7 @@ export default function EventDisplayModal({ attendee: [organizer, ...attendees], transp: "OPAQUE", color: userPersonnalCalendars[calendarid]?.color, + alarm: { trigger: alarm, action: "EMAIL" }, }; const [baseId, recurrenceId] = event.uid.split("/"); @@ -471,31 +472,30 @@ export default function EventDisplayModal({ isOwn={isOwn} /> - - Alarm - - + + Alarm + + Visibility diff --git a/src/features/Events/EventModal.tsx b/src/features/Events/EventModal.tsx index 9b2fbd6..dcd0e6d 100644 --- a/src/features/Events/EventModal.tsx +++ b/src/features/Events/EventModal.tsx @@ -117,6 +117,7 @@ function EventPopover({ ], transp: "OPAQUE", color: userPersonnalCalendars[calendarid]?.color, + alarm: { trigger: alarm, action: "EMAIL" }, }; if (end) { newEvent.end = new Date(end); diff --git a/src/features/Events/EventsTypes.ts b/src/features/Events/EventsTypes.ts index eb0a13f..93df014 100644 --- a/src/features/Events/EventsTypes.ts +++ b/src/features/Events/EventsTypes.ts @@ -22,6 +22,7 @@ export interface CalendarEvent { status?: string; timezone: string; repetition?: RepetitionObject; + alarm: AlarmObject; } export interface RepetitionObject { @@ -31,3 +32,8 @@ export interface RepetitionObject { occurrences?: number; endDate?: string; } + +export interface AlarmObject { + trigger: string; + action: string; +} diff --git a/src/features/Events/eventUtils.ts b/src/features/Events/eventUtils.ts index 0e19bab..d48dd89 100644 --- a/src/features/Events/eventUtils.ts +++ b/src/features/Events/eventUtils.ts @@ -1,6 +1,6 @@ import { Calendars } from "../Calendars/CalendarTypes"; import { userAttendee } from "../User/userDataTypes"; -import { CalendarEvent } from "./EventsTypes"; +import { AlarmObject, CalendarEvent } from "./EventsTypes"; import ICAL from "ical.js"; import { TIMEZONES } from "../../utils/timezone-data"; type RawEntry = [string, Record, string, any]; @@ -9,7 +9,8 @@ export function parseCalendarEvent( data: RawEntry[], color: string, calendarid: string, - eventURL: string + eventURL: string, + valarm?: RawEntry[] ): CalendarEvent { const event: Partial = { color, attendee: [] }; let recurrenceId; @@ -103,6 +104,21 @@ export function parseCalendarEvent( event.uid = `${event.uid}/${recurrenceId}`; } + if (valarm) { + console.log(valarm); + event.alarm = {} as AlarmObject; + for (const [key, params, type, value] of valarm[1]) { + switch (key.toLowerCase()) { + case "action": + event.alarm.action = value; + break; + case "trigger": + event.alarm.trigger = value; + break; + } + } + } + event.URL = eventURL; if (!event.uid || !event.start) { console.error( @@ -115,7 +131,10 @@ export function parseCalendarEvent( return event as CalendarEvent; } -export function calendarEventToJCal(event: CalendarEvent): any[] { +export function calendarEventToJCal( + event: CalendarEvent, + calOwnerEmail?: string +): any[] { const tzid = event.timezone; // Fallback to UTC if no timezone provided const vevent: any[] = [ @@ -138,8 +157,23 @@ export function calendarEventToJCal(event: CalendarEvent): any[] { ], ["summary", {}, "text", event.title ?? ""], ], - [], ]; + if (event.alarm) { + const valarm = [ + ["trigger", {}, "duration", event.alarm.trigger], + ["action", {}, "text", event.alarm.action], + ["attendee", {}, "cal-address", `mailto:${calOwnerEmail}`], + ["summary", {}, "text", event.title], + [ + "description", + {}, + "text", + "This is an automatic alarm sent by OpenPaas", + ], + ]; + vevent.push([["valarm", valarm]]); + } + vevent.push([]); if (event.end) { if (event.allday && event.end.getTime() === event.start.getTime()) { From 94a18dd8456643717617e7f549365d39a10b0722 Mon Sep 17 00:00:00 2001 From: Camille Moussu Date: Tue, 9 Sep 2025 10:25:28 +0200 Subject: [PATCH 2/4] [#78] added tests --- __test__/features/Events/eventUtils.test.ts | 95 ++++++++++++++++++++- src/features/Events/EventDisplay.tsx | 2 +- src/features/Events/eventUtils.ts | 1 - 3 files changed, 94 insertions(+), 4 deletions(-) diff --git a/__test__/features/Events/eventUtils.test.ts b/__test__/features/Events/eventUtils.test.ts index 2807740..4cb01b1 100644 --- a/__test__/features/Events/eventUtils.test.ts +++ b/__test__/features/Events/eventUtils.test.ts @@ -1,3 +1,4 @@ +import { CalendarEvent } from "../../../src/features/Events/EventsTypes"; import { calendarEventToJCal, parseCalendarEvent, @@ -69,6 +70,23 @@ describe("parseCalendarEvent", () => { ]); }); + it("marks allday true for DATE format", () => { + const rawData = [ + ["UID", {}, "text", "event-2"], + ["DTSTART", {}, "date", "2025-07-20"], + ["DTEND", {}, "date", "2025-07-21"], + ] as any; + + const result = parseCalendarEvent( + rawData, + baseColor, + calendarId, + "/calendars/test.ics" + ); + + expect(result.allday).toBe(true); + }); + it("appends recurrence-id to UID if present", () => { const rawData: any = [ ["UID", {}, "text", "event-2"], @@ -110,6 +128,32 @@ describe("parseCalendarEvent", () => { expect(result2.error).toMatch(/missing crucial event param/); }); + it("parses alarm block correctly", () => { + const rawData = [ + ["UID", {}, "text", "event-5"], + ["DTSTART", {}, "date-time", "2025-07-18T09:00:00Z"], + ] as any; + + const valarm: any = [ + "VALARM", + [ + ["ACTION", {}, "text", "DISPLAY"], + ["TRIGGER", {}, "duration", "-PT15M"], + ], + ]; + + const result = parseCalendarEvent( + rawData, + baseColor, + calendarId, + "/calendars/test.ics", + valarm + ); + + expect(result.alarm?.action).toBe("DISPLAY"); + expect(result.alarm?.trigger).toBe("-PT15M"); + }); + it("handles optional organizer and attendee fields gracefully", () => { const rawData = [ ["UID", {}, "text", "event-4"], @@ -184,7 +228,7 @@ describe("calendarEventToJCal", () => { ], }; - const result = calendarEventToJCal(mockEvent); + const result = calendarEventToJCal(mockEvent as CalendarEvent); expect(result[0]).toBe("vcalendar"); const [vevent, vtimezone] = result[2]; @@ -266,6 +310,53 @@ describe("calendarEventToJCal", () => { ]) ); }); + + it("converts with alarm included", () => { + const mockEvent: any = { + uid: "event-10", + title: "Alarm Event", + start: new Date("2025-07-20T09:00:00"), + end: new Date("2025-07-20T10:00:00"), + timezone: "Europe/Paris", + allday: false, + alarm: { trigger: "-PT10M", action: "DISPLAY" }, + attendee: [], + }; + + const result = calendarEventToJCal(mockEvent, "owner@example.com"); + const vevent = result[2][0]; + + expect(vevent[2][0][0]).toBe("valarm"); + expect(vevent[2][0][1]).toEqual( + expect.arrayContaining([ + ["trigger", {}, "duration", "-PT10M"], + ["action", {}, "text", "DISPLAY"], + ]) + ); + }); + + it("converts all-day events adjusting dtend", () => { + const mockEvent: any = { + uid: "event-11", + title: "All Day", + start: new Date("2025-07-21"), + end: new Date("2025-07-21"), + timezone: "Europe/Paris", + allday: true, + attendee: [], + }; + + const result = calendarEventToJCal(mockEvent); + const veventProps = result[2][0][1]; + + expect(veventProps).toEqual( + expect.arrayContaining([ + ["dtstart", { tzid: "Europe/Paris" }, "date", "2025-07-21"], + ["dtend", { tzid: "Europe/Paris" }, "date", "2025-07-22"], + ]) + ); + }); + it("should convert a CalendarEvent to JCal format, with all day activated", () => { const mockEvent = { uid: "event-123", @@ -297,7 +388,7 @@ describe("calendarEventToJCal", () => { ], }; - const result = calendarEventToJCal(mockEvent); + const result = calendarEventToJCal(mockEvent as CalendarEvent); expect(result[0]).toBe("vcalendar"); const [vevent, vtimezone] = result[2]; diff --git a/src/features/Events/EventDisplay.tsx b/src/features/Events/EventDisplay.tsx index e9f9472..9b0027d 100644 --- a/src/features/Events/EventDisplay.tsx +++ b/src/features/Events/EventDisplay.tsx @@ -91,7 +91,7 @@ export default function EventDisplayModal({ const [repetition, setRepetition] = useState( event.repetition ?? ({} as RepetitionObject) ); - const [alarm, setAlarm] = useState(event.alarm.trigger); + const [alarm, setAlarm] = useState(event?.alarm?.trigger ?? ""); const [busy, setBusy] = useState(""); const [eventClass, setEventClass] = useState(event?.class ?? "PUBLIC"); const [timezone, setTimezone] = useState(event?.timezone ?? "UTC"); diff --git a/src/features/Events/eventUtils.ts b/src/features/Events/eventUtils.ts index d48dd89..7326688 100644 --- a/src/features/Events/eventUtils.ts +++ b/src/features/Events/eventUtils.ts @@ -105,7 +105,6 @@ export function parseCalendarEvent( } if (valarm) { - console.log(valarm); event.alarm = {} as AlarmObject; for (const [key, params, type, value] of valarm[1]) { switch (key.toLowerCase()) { From 3d48cd7fd7a5d7999e066a673b9e4b5d0c57334b Mon Sep 17 00:00:00 2001 From: Camille Moussu Date: Tue, 9 Sep 2025 11:04:10 +0200 Subject: [PATCH 3/4] [#78] fixed f5 update --- src/features/Calendars/CalendarSlice.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/Calendars/CalendarSlice.ts b/src/features/Calendars/CalendarSlice.ts index 3c64aac..84b4e52 100644 --- a/src/features/Calendars/CalendarSlice.ts +++ b/src/features/Calendars/CalendarSlice.ts @@ -89,7 +89,7 @@ export const putEventAsync = createAsyncThunk< (eventdata: any) => { const vevents = eventdata.data[2] as any[][]; const eventURL = eventdata._links.self.href; - const valarm = eventdata.data[3][0][1]; + const valarm = eventdata.data[2][0][2][0]; return vevents.map((vevent: any[]) => { return parseCalendarEvent( vevent[1], From 3e9530a0a594d32666177dab787de3ff45f837b5 Mon Sep 17 00:00:00 2001 From: Camille Moussu Date: Tue, 9 Sep 2025 14:52:51 +0200 Subject: [PATCH 4/4] [#78] fixed undefined email --- src/features/Calendars/CalendarSlice.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/features/Calendars/CalendarSlice.ts b/src/features/Calendars/CalendarSlice.ts index 84b4e52..635e73b 100644 --- a/src/features/Calendars/CalendarSlice.ts +++ b/src/features/Calendars/CalendarSlice.ts @@ -71,7 +71,10 @@ export const putEventAsync = createAsyncThunk< { calId: string; events: CalendarEvent[] }, // Return type { cal: Calendars; newEvent: CalendarEvent } // Arg type >("calendars/putEvent", async ({ cal, newEvent }) => { - const response = await putEvent(newEvent); + const response = await putEvent( + newEvent, + cal.ownerEmails ? cal.ownerEmails[0] : undefined + ); const eventDate = new Date(newEvent.start); const weekStart = new Date(eventDate);