diff --git a/__test__/components/EventModifications.test.tsx b/__test__/components/EventModifications.test.tsx index cc46570..69d13ac 100644 --- a/__test__/components/EventModifications.test.tsx +++ b/__test__/components/EventModifications.test.tsx @@ -302,6 +302,7 @@ describe("CalendarApp integration", () => { end: new Date("2025-11-14T11:31:00.000Z").toISOString(), class: "PUBLIC", partstat: "ACCEPTED", + sequence: 2, organizer: { cn: "Alice", cal_address: "alice@example.com", @@ -380,6 +381,9 @@ describe("CalendarApp integration", () => { expect(normalAttendee).toBeTruthy(); expect(normalAttendee?.partstat).toBe("ACCEPTED"); expect(normalAttendee?.role).toBe("REQ-PARTICIPANT"); + + // Verify SEQUENCE is incremented + expect(updatedEvent?.sequence).toBe(3); // 2 + 1 }); it("changes normal attendee to need action on time update and no organizer changes", async () => { @@ -432,6 +436,9 @@ describe("CalendarApp integration", () => { expect(normalAttendee).toBeTruthy(); expect(normalAttendee?.partstat).toBe("NEEDS-ACTION"); expect(normalAttendee?.role).toBe("REQ-PARTICIPANT"); + + // Verify SEQUENCE is incremented + expect(updatedEvent?.sequence).toBe(3); // 2 + 1 }); it("update event attendees on drag", async () => { const mockDispatch = jest.fn(); @@ -490,6 +497,9 @@ describe("CalendarApp integration", () => { (a: any) => a.cal_address === "bob@example.com" ); expect(normal?.partstat).toBe("NEEDS-ACTION"); + + // Verify SEQUENCE is incremented + expect(updatedEvent?.sequence).toBe(3); // 2 + 1 }); it("update event attendees on resize", async () => { @@ -549,6 +559,9 @@ describe("CalendarApp integration", () => { (a: any) => a.cal_address === "bob@example.com" ); expect(normal?.partstat).toBe("NEEDS-ACTION"); + + // Verify SEQUENCE is incremented + expect(updatedEvent?.sequence).toBe(3); // 2 + 1 }); }); }); diff --git a/__test__/features/Events/eventUtils.test.ts b/__test__/features/Events/eventUtils.test.ts index cd51779..6b89148 100644 --- a/__test__/features/Events/eventUtils.test.ts +++ b/__test__/features/Events/eventUtils.test.ts @@ -837,6 +837,99 @@ describe("calendarEventToJCal", () => { ]) ); }); + + it("includes SEQUENCE field when present in event", () => { + const mockEvent = { + uid: "event-with-sequence", + title: "Event with Sequence", + start: "2025-07-23T08:00:00.000Z", + end: "2025-07-23T09:00:00.000Z", + timezone: "Europe/Paris", + allday: false, + sequence: 3, + attendee: [], + }; + + const result = calendarEventToJCal(mockEvent as unknown as CalendarEvent); + + const [vevent] = result[2]; + const props = vevent[1]; + + const sequenceProp = props.find((p: any[]) => p[0] === "sequence"); + expect(sequenceProp).toBeDefined(); + expect(sequenceProp).toEqual(["sequence", {}, "integer", 3]); + }); + + it("defaults SEQUENCE to 1 when not present in event", () => { + const mockEvent = { + uid: "event-without-sequence", + title: "Event without Sequence", + start: "2025-07-23T08:00:00.000Z", + end: "2025-07-23T09:00:00.000Z", + timezone: "Europe/Paris", + allday: false, + attendee: [], + }; + + const result = calendarEventToJCal(mockEvent as unknown as CalendarEvent); + + const [vevent] = result[2]; + const props = vevent[1]; + + const sequenceProp = props.find((p: any[]) => p[0] === "sequence"); + expect(sequenceProp).toBeDefined(); + expect(sequenceProp).toEqual(["sequence", {}, "integer", 1]); + }); + + it("includes SEQUENCE in recurring event", () => { + const mockEvent = { + uid: "recurring-event-sequence", + title: "Recurring Event with Sequence", + start: "2025-07-23T08:00:00.000Z", + end: "2025-07-23T09:00:00.000Z", + timezone: "Europe/Paris", + allday: false, + sequence: 2, + repetition: { freq: "DAILY", interval: 1 }, + attendee: [], + }; + + const result = calendarEventToJCal(mockEvent as unknown as CalendarEvent); + + const [vevent] = result[2]; + const props = vevent[1]; + + const sequenceProp = props.find((p: any[]) => p[0] === "sequence"); + expect(sequenceProp).toBeDefined(); + expect(sequenceProp).toEqual(["sequence", {}, "integer", 2]); + }); + + it("includes SEQUENCE in event exception (with recurrence-id)", () => { + const mockEvent = { + uid: "event-exception-sequence", + title: "Event Exception with Sequence", + start: "2025-07-23T08:00:00.000Z", + end: "2025-07-23T09:00:00.000Z", + timezone: "Europe/Paris", + allday: false, + sequence: 5, + recurrenceId: "2025-07-23T08:00:00Z", + attendee: [], + }; + + const result = calendarEventToJCal(mockEvent as unknown as CalendarEvent); + + const [vevent] = result[2]; + const props = vevent[1]; + + const sequenceProp = props.find((p: any[]) => p[0] === "sequence"); + expect(sequenceProp).toBeDefined(); + expect(sequenceProp).toEqual(["sequence", {}, "integer", 5]); + + // Also verify recurrence-id is present + const recurrenceIdProp = props.find((p: any[]) => p[0] === "recurrence-id"); + expect(recurrenceIdProp).toBeDefined(); + }); }); describe("combineMasterDateWithFormTime", () => { diff --git a/src/components/Calendar/handlers/eventHandlers.ts b/src/components/Calendar/handlers/eventHandlers.ts index fd71445..3a6ac22 100644 --- a/src/components/Calendar/handlers/eventHandlers.ts +++ b/src/components/Calendar/handlers/eventHandlers.ts @@ -177,6 +177,7 @@ export const createEventHandlers = (props: EventHandlersProps) => { ...event, start: computedNewStart.toISOString(), end: computedNewEnd.toISOString(), + sequence: (event.sequence ?? 1) + 1, } as CalendarEvent, true ); @@ -202,6 +203,7 @@ export const createEventHandlers = (props: EventHandlersProps) => { ...master, start: computedNewStart.toISOString(), end: computedNewEnd.toISOString(), + sequence: (master.sequence ?? 1) + 1, }, }) ); @@ -254,6 +256,7 @@ export const createEventHandlers = (props: EventHandlersProps) => { ...event, start: computedNewStart.toISOString(), end: computedNewEnd.toISOString(), + sequence: (event.sequence ?? 1) + 1, } as CalendarEvent, true ); @@ -280,6 +283,7 @@ export const createEventHandlers = (props: EventHandlersProps) => { ...master, start: computedNewStart.toISOString(), end: computedNewEnd.toISOString(), + sequence: (master.sequence ?? 1) + 1, }, }) ); diff --git a/src/features/Events/EventModal.tsx b/src/features/Events/EventModal.tsx index 2de392a..7d53109 100644 --- a/src/features/Events/EventModal.tsx +++ b/src/features/Events/EventModal.tsx @@ -733,6 +733,7 @@ function EventPopover({ }, ], transp: busy, + sequence: 1, color: targetCalendar?.color, alarm: { trigger: alarm, action: "EMAIL" }, x_openpass_videoconference: meetingLink || undefined, diff --git a/src/features/Events/EventUpdateModal.tsx b/src/features/Events/EventUpdateModal.tsx index 583e80d..6d0275c 100644 --- a/src/features/Events/EventUpdateModal.tsx +++ b/src/features/Events/EventUpdateModal.tsx @@ -604,6 +604,7 @@ function EventUpdateModal({ organizer: organizer, timezone, transp: busy, + sequence: (event.sequence ?? 1) + 1, color: targetCalendar?.color, alarm: { trigger: alarm, action: "EMAIL" }, x_openpass_videoconference: meetingLink || undefined, @@ -700,6 +701,7 @@ function EventUpdateModal({ ...newEvent, uid: newEventUID, URL: `/calendars/${newCalId || calId}/${newEventUID}.ics`, + sequence: 1, // New event with new UID starts at sequence 1 }; // STEP 3: Persist new event to server diff --git a/src/features/Events/eventUtils.ts b/src/features/Events/eventUtils.ts index 7ee2d35..ce46ef1 100644 --- a/src/features/Events/eventUtils.ts +++ b/src/features/Events/eventUtils.ts @@ -297,6 +297,7 @@ export function makeVevent( formatDateToICal(new Date(event.start), event.allday ?? false, tzid), ], ["class", {}, "text", event.class ?? "PUBLIC"], + ["sequence", {}, "integer", event.sequence ?? 1], [ "x-openpaas-videoconference", {},