- 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(),
|
end: new Date("2025-11-14T11:31:00.000Z").toISOString(),
|
||||||
class: "PUBLIC",
|
class: "PUBLIC",
|
||||||
partstat: "ACCEPTED",
|
partstat: "ACCEPTED",
|
||||||
|
sequence: 2,
|
||||||
organizer: {
|
organizer: {
|
||||||
cn: "Alice",
|
cn: "Alice",
|
||||||
cal_address: "alice@example.com",
|
cal_address: "alice@example.com",
|
||||||
@@ -380,6 +381,9 @@ describe("CalendarApp integration", () => {
|
|||||||
expect(normalAttendee).toBeTruthy();
|
expect(normalAttendee).toBeTruthy();
|
||||||
expect(normalAttendee?.partstat).toBe("ACCEPTED");
|
expect(normalAttendee?.partstat).toBe("ACCEPTED");
|
||||||
expect(normalAttendee?.role).toBe("REQ-PARTICIPANT");
|
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 () => {
|
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).toBeTruthy();
|
||||||
expect(normalAttendee?.partstat).toBe("NEEDS-ACTION");
|
expect(normalAttendee?.partstat).toBe("NEEDS-ACTION");
|
||||||
expect(normalAttendee?.role).toBe("REQ-PARTICIPANT");
|
expect(normalAttendee?.role).toBe("REQ-PARTICIPANT");
|
||||||
|
|
||||||
|
// Verify SEQUENCE is incremented
|
||||||
|
expect(updatedEvent?.sequence).toBe(3); // 2 + 1
|
||||||
});
|
});
|
||||||
it("update event attendees on drag", async () => {
|
it("update event attendees on drag", async () => {
|
||||||
const mockDispatch = jest.fn();
|
const mockDispatch = jest.fn();
|
||||||
@@ -490,6 +497,9 @@ describe("CalendarApp integration", () => {
|
|||||||
(a: any) => a.cal_address === "bob@example.com"
|
(a: any) => a.cal_address === "bob@example.com"
|
||||||
);
|
);
|
||||||
expect(normal?.partstat).toBe("NEEDS-ACTION");
|
expect(normal?.partstat).toBe("NEEDS-ACTION");
|
||||||
|
|
||||||
|
// Verify SEQUENCE is incremented
|
||||||
|
expect(updatedEvent?.sequence).toBe(3); // 2 + 1
|
||||||
});
|
});
|
||||||
|
|
||||||
it("update event attendees on resize", async () => {
|
it("update event attendees on resize", async () => {
|
||||||
@@ -549,6 +559,9 @@ describe("CalendarApp integration", () => {
|
|||||||
(a: any) => a.cal_address === "bob@example.com"
|
(a: any) => a.cal_address === "bob@example.com"
|
||||||
);
|
);
|
||||||
expect(normal?.partstat).toBe("NEEDS-ACTION");
|
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", () => {
|
describe("combineMasterDateWithFormTime", () => {
|
||||||
|
|||||||
@@ -177,6 +177,7 @@ export const createEventHandlers = (props: EventHandlersProps) => {
|
|||||||
...event,
|
...event,
|
||||||
start: computedNewStart.toISOString(),
|
start: computedNewStart.toISOString(),
|
||||||
end: computedNewEnd.toISOString(),
|
end: computedNewEnd.toISOString(),
|
||||||
|
sequence: (event.sequence ?? 1) + 1,
|
||||||
} as CalendarEvent,
|
} as CalendarEvent,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
@@ -202,6 +203,7 @@ export const createEventHandlers = (props: EventHandlersProps) => {
|
|||||||
...master,
|
...master,
|
||||||
start: computedNewStart.toISOString(),
|
start: computedNewStart.toISOString(),
|
||||||
end: computedNewEnd.toISOString(),
|
end: computedNewEnd.toISOString(),
|
||||||
|
sequence: (master.sequence ?? 1) + 1,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -254,6 +256,7 @@ export const createEventHandlers = (props: EventHandlersProps) => {
|
|||||||
...event,
|
...event,
|
||||||
start: computedNewStart.toISOString(),
|
start: computedNewStart.toISOString(),
|
||||||
end: computedNewEnd.toISOString(),
|
end: computedNewEnd.toISOString(),
|
||||||
|
sequence: (event.sequence ?? 1) + 1,
|
||||||
} as CalendarEvent,
|
} as CalendarEvent,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
@@ -280,6 +283,7 @@ export const createEventHandlers = (props: EventHandlersProps) => {
|
|||||||
...master,
|
...master,
|
||||||
start: computedNewStart.toISOString(),
|
start: computedNewStart.toISOString(),
|
||||||
end: computedNewEnd.toISOString(),
|
end: computedNewEnd.toISOString(),
|
||||||
|
sequence: (master.sequence ?? 1) + 1,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -733,6 +733,7 @@ function EventPopover({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
transp: busy,
|
transp: busy,
|
||||||
|
sequence: 1,
|
||||||
color: targetCalendar?.color,
|
color: targetCalendar?.color,
|
||||||
alarm: { trigger: alarm, action: "EMAIL" },
|
alarm: { trigger: alarm, action: "EMAIL" },
|
||||||
x_openpass_videoconference: meetingLink || undefined,
|
x_openpass_videoconference: meetingLink || undefined,
|
||||||
|
|||||||
@@ -604,6 +604,7 @@ function EventUpdateModal({
|
|||||||
organizer: organizer,
|
organizer: organizer,
|
||||||
timezone,
|
timezone,
|
||||||
transp: busy,
|
transp: busy,
|
||||||
|
sequence: (event.sequence ?? 1) + 1,
|
||||||
color: targetCalendar?.color,
|
color: targetCalendar?.color,
|
||||||
alarm: { trigger: alarm, action: "EMAIL" },
|
alarm: { trigger: alarm, action: "EMAIL" },
|
||||||
x_openpass_videoconference: meetingLink || undefined,
|
x_openpass_videoconference: meetingLink || undefined,
|
||||||
@@ -700,6 +701,7 @@ function EventUpdateModal({
|
|||||||
...newEvent,
|
...newEvent,
|
||||||
uid: newEventUID,
|
uid: newEventUID,
|
||||||
URL: `/calendars/${newCalId || calId}/${newEventUID}.ics`,
|
URL: `/calendars/${newCalId || calId}/${newEventUID}.ics`,
|
||||||
|
sequence: 1, // New event with new UID starts at sequence 1
|
||||||
};
|
};
|
||||||
|
|
||||||
// STEP 3: Persist new event to server
|
// STEP 3: Persist new event to server
|
||||||
|
|||||||
@@ -297,6 +297,7 @@ export function makeVevent(
|
|||||||
formatDateToICal(new Date(event.start), event.allday ?? false, tzid),
|
formatDateToICal(new Date(event.start), event.allday ?? false, tzid),
|
||||||
],
|
],
|
||||||
["class", {}, "text", event.class ?? "PUBLIC"],
|
["class", {}, "text", event.class ?? "PUBLIC"],
|
||||||
|
["sequence", {}, "integer", event.sequence ?? 1],
|
||||||
[
|
[
|
||||||
"x-openpaas-videoconference",
|
"x-openpaas-videoconference",
|
||||||
{},
|
{},
|
||||||
|
|||||||
Reference in New Issue
Block a user