- Add SEQUENCE property to ICS generation (starts at 1 for new events) - Increment SEQUENCE on event updates (modal, drag & drop, resize) - Handle SEQUENCE independently for recurring event instances - Add comprehensive tests for SEQUENCE generation and incrementation - Ensure proper event ordering for remote calendar participants 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
@@ -733,6 +733,7 @@ function EventPopover({
|
||||
},
|
||||
],
|
||||
transp: busy,
|
||||
sequence: 1,
|
||||
color: targetCalendar?.color,
|
||||
alarm: { trigger: alarm, action: "EMAIL" },
|
||||
x_openpass_videoconference: meetingLink || undefined,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
{},
|
||||
|
||||
Reference in New Issue
Block a user