From 04fa2d06295ea455115ba4139d36b534448e6a86 Mon Sep 17 00:00:00 2001 From: lenhanphung Date: Fri, 3 Oct 2025 11:32:42 +0700 Subject: [PATCH] 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 --- __test__/features/Events/EventApi.test.tsx | 52 ++++++++++++++++++++++ src/features/Events/EventsTypes.ts | 1 + src/features/Events/eventUtils.ts | 26 +++++++---- 3 files changed, 70 insertions(+), 9 deletions(-) diff --git a/__test__/features/Events/EventApi.test.tsx b/__test__/features/Events/EventApi.test.tsx index d6662fe..f1553d9 100644 --- a/__test__/features/Events/EventApi.test.tsx +++ b/__test__/features/Events/EventApi.test.tsx @@ -104,4 +104,56 @@ describe("eventApi", () => { 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), + }) + ); + }); }); diff --git a/src/features/Events/EventsTypes.ts b/src/features/Events/EventsTypes.ts index 13105a3..42263b0 100644 --- a/src/features/Events/EventsTypes.ts +++ b/src/features/Events/EventsTypes.ts @@ -29,6 +29,7 @@ export interface RepetitionObject { freq: string; interval?: number; selectedDays?: string[]; + byday?: string[] | null; occurrences?: number; endDate?: string; } diff --git a/src/features/Events/eventUtils.ts b/src/features/Events/eventUtils.ts index 0fcd71f..6415a65 100644 --- a/src/features/Events/eventUtils.ts +++ b/src/features/Events/eventUtils.ts @@ -1,4 +1,3 @@ -import { Calendars } from "../Calendars/CalendarTypes"; import { userAttendee } from "../User/userDataTypes"; import { AlarmObject, CalendarEvent } from "./EventsTypes"; import ICAL from "ical.js"; @@ -16,7 +15,7 @@ export function parseCalendarEvent( 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, , value] of data) { switch (key.toLowerCase()) { case "uid": event.uid = value; @@ -86,7 +85,7 @@ export function parseCalendarEvent( case "rrule": event.repetition = { freq: value.freq.toLowerCase() }; if (value.byday) { - event.repetition.selectedDays = value.byday; + event.repetition.byday = value.byday; } if (value.until) { event.repetition.endDate = value.until; @@ -106,7 +105,7 @@ export function parseCalendarEvent( if (valarm) { event.alarm = {} as AlarmObject; - for (const [key, params, type, value] of valarm[1]) { + for (const [key, , , value] of valarm[1]) { switch (key.toLowerCase()) { case "action": event.alarm.action = value; @@ -175,14 +174,20 @@ export function calendarEventToJCal( vevent.push([]); if (event.end) { - if (event.allday && event.end.getTime() === event.start.getTime()) { - event.end.setDate(event.start.getDate() + 1); + const startDate = new Date(event.start); + 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([ "dtend", { tzid }, event.allday ? "date" : "date-time", - formatDateToICal(new Date(event.end), event.allday ?? false), + formatDateToICal(finalEndDate, event.allday ?? false), ]); } if (event.organizer) { @@ -210,8 +215,11 @@ export function calendarEventToJCal( if (event.repetition.endDate) { repetitionRule["until"] = event.repetition.endDate; } - if (event.repetition.selectedDays) { - repetitionRule["byday"] = event.repetition.selectedDays; + if ( + event.repetition.byday !== null && + event.repetition.byday !== undefined + ) { + repetitionRule["byday"] = event.repetition.byday; } vevent[1].push(["rrule", {}, "recur", repetitionRule]); }