feat: add byday field support for event repetition

- Add byday field to RepetitionObject interface in EventsTypes.ts
- Update calendarEventToJCal to handle byday field (array or null)
- Update parseCalendarEvent to parse byday from JCal format
- Fix all-day event dtend logic to increment by 1 day when start equals end
- Add comprehensive test cases for byday field handling
This commit is contained in:
lenhanphung
2025-10-03 11:32:42 +07:00
parent cea576e5e8
commit 04fa2d0629
3 changed files with 70 additions and 9 deletions
@@ -104,4 +104,56 @@ describe("eventApi", () => {
method: "DELETE", method: "DELETE",
}); });
}); });
test("putEvent handles byday field correctly", async () => {
const mockResponse = { status: 201, url: "/dav/cals/test.ics" };
(api as unknown as jest.Mock).mockReturnValue(mockResponse);
const eventWithByday = {
...mockEvent,
repetition: {
freq: "weekly",
interval: 1,
byday: ["MO", "WE", "FR"],
},
};
await putEvent(eventWithByday);
const expectedResult = calendarEventToJCal(eventWithByday);
expect(api).toHaveBeenCalledWith(
"dav/calendars/667037022b752d0026472254/667037022b752d0026472254/cal1.ics",
expect.objectContaining({
method: "PUT",
headers: { "content-type": "text/calendar; charset=utf-8" },
body: JSON.stringify(expectedResult),
})
);
});
test("putEvent handles null byday field correctly", async () => {
const mockResponse = { status: 201, url: "/dav/cals/test.ics" };
(api as unknown as jest.Mock).mockReturnValue(mockResponse);
const eventWithNullByday = {
...mockEvent,
repetition: {
freq: "daily",
interval: 1,
byday: null,
},
};
await putEvent(eventWithNullByday);
const expectedResult = calendarEventToJCal(eventWithNullByday);
expect(api).toHaveBeenCalledWith(
"dav/calendars/667037022b752d0026472254/667037022b752d0026472254/cal1.ics",
expect.objectContaining({
method: "PUT",
headers: { "content-type": "text/calendar; charset=utf-8" },
body: JSON.stringify(expectedResult),
})
);
});
}); });
+1
View File
@@ -29,6 +29,7 @@ export interface RepetitionObject {
freq: string; freq: string;
interval?: number; interval?: number;
selectedDays?: string[]; selectedDays?: string[];
byday?: string[] | null;
occurrences?: number; occurrences?: number;
endDate?: string; endDate?: string;
} }
+17 -9
View File
@@ -1,4 +1,3 @@
import { Calendars } from "../Calendars/CalendarTypes";
import { userAttendee } from "../User/userDataTypes"; import { userAttendee } from "../User/userDataTypes";
import { AlarmObject, CalendarEvent } from "./EventsTypes"; import { AlarmObject, CalendarEvent } from "./EventsTypes";
import ICAL from "ical.js"; import ICAL from "ical.js";
@@ -16,7 +15,7 @@ export function parseCalendarEvent(
let recurrenceId; let recurrenceId;
const dateRegex = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/; 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, , value] of data) {
switch (key.toLowerCase()) { switch (key.toLowerCase()) {
case "uid": case "uid":
event.uid = value; event.uid = value;
@@ -86,7 +85,7 @@ export function parseCalendarEvent(
case "rrule": case "rrule":
event.repetition = { freq: value.freq.toLowerCase() }; event.repetition = { freq: value.freq.toLowerCase() };
if (value.byday) { if (value.byday) {
event.repetition.selectedDays = value.byday; event.repetition.byday = value.byday;
} }
if (value.until) { if (value.until) {
event.repetition.endDate = value.until; event.repetition.endDate = value.until;
@@ -106,7 +105,7 @@ export function parseCalendarEvent(
if (valarm) { if (valarm) {
event.alarm = {} as AlarmObject; event.alarm = {} as AlarmObject;
for (const [key, params, type, value] of valarm[1]) { for (const [key, , , value] of valarm[1]) {
switch (key.toLowerCase()) { switch (key.toLowerCase()) {
case "action": case "action":
event.alarm.action = value; event.alarm.action = value;
@@ -175,14 +174,20 @@ export function calendarEventToJCal(
vevent.push([]); vevent.push([]);
if (event.end) { if (event.end) {
if (event.allday && event.end.getTime() === event.start.getTime()) { const startDate = new Date(event.start);
event.end.setDate(event.start.getDate() + 1); const endDate = new Date(event.end);
let finalEndDate = endDate;
if (event.allday && endDate.getTime() === startDate.getTime()) {
finalEndDate = new Date(endDate);
finalEndDate.setDate(startDate.getDate() + 1);
} }
vevent[1].push([ vevent[1].push([
"dtend", "dtend",
{ tzid }, { tzid },
event.allday ? "date" : "date-time", event.allday ? "date" : "date-time",
formatDateToICal(new Date(event.end), event.allday ?? false), formatDateToICal(finalEndDate, event.allday ?? false),
]); ]);
} }
if (event.organizer) { if (event.organizer) {
@@ -210,8 +215,11 @@ export function calendarEventToJCal(
if (event.repetition.endDate) { if (event.repetition.endDate) {
repetitionRule["until"] = event.repetition.endDate; repetitionRule["until"] = event.repetition.endDate;
} }
if (event.repetition.selectedDays) { if (
repetitionRule["byday"] = event.repetition.selectedDays; event.repetition.byday !== null &&
event.repetition.byday !== undefined
) {
repetitionRule["byday"] = event.repetition.byday;
} }
vevent[1].push(["rrule", {}, "recur", repetitionRule]); vevent[1].push(["rrule", {}, "recur", repetitionRule]);
} }