Merge pull request #99 from linagora/78-add-valarm-support
Add valarm support
This commit is contained in:
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -70,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);
|
||||
@@ -88,8 +92,15 @@ export const putEventAsync = createAsyncThunk<
|
||||
(eventdata: any) => {
|
||||
const vevents = eventdata.data[2] as any[][];
|
||||
const eventURL = eventdata._links.self.href;
|
||||
const valarm = eventdata.data[2][0][2][0];
|
||||
return vevents.map((vevent: any[]) => {
|
||||
return parseCalendarEvent(vevent[1], cal.color ?? "", cal.id, eventURL);
|
||||
return parseCalendarEvent(
|
||||
vevent[1],
|
||||
cal.color ?? "",
|
||||
cal.id,
|
||||
eventURL,
|
||||
valarm
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -91,7 +91,7 @@ export default function EventDisplayModal({
|
||||
const [repetition, setRepetition] = useState<RepetitionObject>(
|
||||
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}
|
||||
/>
|
||||
|
||||
<FormControl fullWidth margin="dense" size="small">
|
||||
<InputLabel id="alarm">Alarm</InputLabel>
|
||||
<Select
|
||||
labelId="alarm"
|
||||
value={alarm}
|
||||
disabled={!isOwn}
|
||||
onChange={(e: SelectChangeEvent) =>
|
||||
setAlarm(e.target.value)
|
||||
}
|
||||
>
|
||||
<MenuItem value={""}>No Alarm</MenuItem>
|
||||
<MenuItem value={"-PT1M"}>1 minute</MenuItem>
|
||||
<MenuItem value={"-PT5M"}>2 minutes</MenuItem>
|
||||
<MenuItem value={"-PT10M"}>10 minutes</MenuItem>
|
||||
<MenuItem value={"-PT15M"}>15 minutes</MenuItem>
|
||||
<MenuItem value={"-PT30M"}>30 minutes</MenuItem>
|
||||
<MenuItem value={"-PT1H"}>1 hours</MenuItem>
|
||||
<MenuItem value={"-PT2H"}>2 hours</MenuItem>
|
||||
<MenuItem value={"-PT5H"}>5 hours</MenuItem>
|
||||
<MenuItem value={"-PT12H"}>12 hours</MenuItem>
|
||||
<MenuItem value={"-PT1D"}>1 day</MenuItem>
|
||||
<MenuItem value={"-PT2D"}>2 days</MenuItem>
|
||||
<MenuItem value={"-PT1W"}>1 week</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl fullWidth margin="dense" size="small">
|
||||
<InputLabel id="alarm">Alarm</InputLabel>
|
||||
<Select
|
||||
labelId="alarm"
|
||||
label="Alarm"
|
||||
value={alarm}
|
||||
disabled={!isOwn}
|
||||
onChange={(e: SelectChangeEvent) => setAlarm(e.target.value)}
|
||||
>
|
||||
<MenuItem value={""}>No Alarm</MenuItem>
|
||||
<MenuItem value={"-PT1M"}>1 minute</MenuItem>
|
||||
<MenuItem value={"-PT5M"}>2 minutes</MenuItem>
|
||||
<MenuItem value={"-PT10M"}>10 minutes</MenuItem>
|
||||
<MenuItem value={"-PT15M"}>15 minutes</MenuItem>
|
||||
<MenuItem value={"-PT30M"}>30 minutes</MenuItem>
|
||||
<MenuItem value={"-PT1H"}>1 hours</MenuItem>
|
||||
<MenuItem value={"-PT2H"}>2 hours</MenuItem>
|
||||
<MenuItem value={"-PT5H"}>5 hours</MenuItem>
|
||||
<MenuItem value={"-PT12H"}>12 hours</MenuItem>
|
||||
<MenuItem value={"-PT1D"}>1 day</MenuItem>
|
||||
<MenuItem value={"-PT2D"}>2 days</MenuItem>
|
||||
<MenuItem value={"-PT1W"}>1 week</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth margin="dense" size="small">
|
||||
<InputLabel id="Visibility">Visibility</InputLabel>
|
||||
|
||||
@@ -117,6 +117,7 @@ function EventPopover({
|
||||
],
|
||||
transp: "OPAQUE",
|
||||
color: userPersonnalCalendars[calendarid]?.color,
|
||||
alarm: { trigger: alarm, action: "EMAIL" },
|
||||
};
|
||||
if (end) {
|
||||
newEvent.end = new Date(end);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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, string>, 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<CalendarEvent> = { color, attendee: [] };
|
||||
let recurrenceId;
|
||||
@@ -103,6 +104,20 @@ export function parseCalendarEvent(
|
||||
event.uid = `${event.uid}/${recurrenceId}`;
|
||||
}
|
||||
|
||||
if (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 +130,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 +156,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()) {
|
||||
|
||||
Reference in New Issue
Block a user