Ask the recurrenceId, which is the part that survives expansion
Rules alone were still wrong, in the other direction. A live 0.16.19 was asked to expand a real weekly series: the occurrences come back carrying no rule at all — only the master has one — and Stalwart spells that master's rule "recurrenceRule", singular, not the RFC 8984 array ihasmail looks for. So a genuine occurrence would have read as a one-off, and the delete dialog would have offered to delete "this event" while deleting the series. What an occurrence does carry is a recurrenceId, which a one-off never has. Master by its rule under either name, occurrence by its recurrenceId. The tests carry the shapes the live server returned.
This commit is contained in:
@@ -679,6 +679,8 @@ export interface JSCalendarEvent {
|
|||||||
recurrenceId?: LocalDate;
|
recurrenceId?: LocalDate;
|
||||||
recurrenceIdTimeZone?: string;
|
recurrenceIdTimeZone?: string;
|
||||||
recurrenceRules?: JSCalendarRecurrenceRule[];
|
recurrenceRules?: JSCalendarRecurrenceRule[];
|
||||||
|
/** Stalwart 0.16 stores a single rule under this name instead of the array above. */
|
||||||
|
recurrenceRule?: JSCalendarRecurrenceRule;
|
||||||
excludedRecurrenceRules?: JSCalendarRecurrenceRule[];
|
excludedRecurrenceRules?: JSCalendarRecurrenceRule[];
|
||||||
recurrenceOverrides?: Record<LocalDate, Record<string, unknown> | null>;
|
recurrenceOverrides?: Record<LocalDate, Record<string, unknown> | null>;
|
||||||
excluded?: boolean;
|
excluded?: boolean;
|
||||||
|
|||||||
@@ -19,9 +19,15 @@ describe("isRecurring", () => {
|
|||||||
// and a base that is a different id. Neither makes it a series.
|
// and a base that is a different id. Neither makes it a series.
|
||||||
expect(isRecurring(ev({ id: "eaaaaai", baseEventId: "i" }))).toBe(false);
|
expect(isRecurring(ev({ id: "eaaaaai", baseEventId: "i" }))).toBe(false);
|
||||||
});
|
});
|
||||||
it("recognises a series by its recurrence rules", () => {
|
it("recognises a series by its rule, under either name", () => {
|
||||||
expect(isRecurring(ev({ recurrenceRules: [{ "@type": "RecurrenceRule", frequency: "weekly" }] }))).toBe(true);
|
expect(isRecurring(ev({ recurrenceRules: [{ "@type": "RecurrenceRule", frequency: "weekly" }] }))).toBe(true);
|
||||||
expect(isRecurring(ev({ baseEventId: "ev1", recurrenceRules: [{ "@type": "RecurrenceRule", frequency: "daily" }] }))).toBe(true);
|
|
||||||
expect(isRecurring(ev({ excludedRecurrenceRules: [{ "@type": "RecurrenceRule", frequency: "monthly" }] }))).toBe(true);
|
expect(isRecurring(ev({ excludedRecurrenceRules: [{ "@type": "RecurrenceRule", frequency: "monthly" }] }))).toBe(true);
|
||||||
|
// Stalwart 0.16 keeps a single rule under the singular name.
|
||||||
|
expect(isRecurring(ev({ recurrenceRule: { "@type": "RecurrenceRule", frequency: "weekly", count: 3 } }))).toBe(true);
|
||||||
|
});
|
||||||
|
it("recognises an occurrence, which arrives with no rule of its own", () => {
|
||||||
|
// A live 0.16.19 expands a weekly series into instances like this: an id
|
||||||
|
// per occurrence, a recurrenceId, and no rule attached.
|
||||||
|
expect(isRecurring(ev({ id: "iaaaaas", recurrenceId: "2030-03-11T10:00:00" }))).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -300,15 +300,22 @@ export const useCalendar = create<CalendarState>((set, get) => ({
|
|||||||
/**
|
/**
|
||||||
* Whether an event is part of a series.
|
* Whether an event is part of a series.
|
||||||
*
|
*
|
||||||
* Not the same question as "does it have a baseEventId", nor "is that base some
|
* Three things had to be checked against a live 0.16.19 to get this right, none
|
||||||
* other event". `CalendarEvent/query` runs with `expandRecurrences`, and Stalwart
|
* of which the mock reproduces:
|
||||||
* hands back an instance id for everything it returns that way — a one-off event
|
*
|
||||||
* included, whose own id (`eaaaaai`) differs from its base (`i`), verified
|
* - `baseEventId` says nothing. `CalendarEvent/query` runs with
|
||||||
* against a live 0.16.19. Recurrence rules are what make a series, so those are
|
* `expandRecurrences`, and a one-off comes back as id `eaaaaai` over base
|
||||||
* what we ask about.
|
* `i` — an instance id of its own, and a base that is a different id.
|
||||||
|
* - The rules say nothing on an instance. An occurrence of a weekly series
|
||||||
|
* arrives with no rule attached at all; only the master carries one.
|
||||||
|
* - Stalwart names that rule `recurrenceRule`, singular, not the RFC 8984
|
||||||
|
* `recurrenceRules` array.
|
||||||
|
*
|
||||||
|
* What an occurrence does carry is a `recurrenceId`, and a one-off never has
|
||||||
|
* one. Master or occurrence, that is what makes this a series.
|
||||||
*/
|
*/
|
||||||
export function isRecurring(ev: CalendarEvent): boolean {
|
export function isRecurring(ev: CalendarEvent): boolean {
|
||||||
return Boolean(ev.recurrenceRules?.length || ev.excludedRecurrenceRules?.length);
|
return Boolean(ev.recurrenceRule || ev.recurrenceRules?.length || ev.excludedRecurrenceRules?.length || ev.recurrenceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toInstance(e: CalendarEvent, calendars: Record<Id, Calendar>): EventInstance | null {
|
export function toInstance(e: CalendarEvent, calendars: Record<Id, Calendar>): EventInstance | null {
|
||||||
|
|||||||
Reference in New Issue
Block a user