Skip to content

Commit ea2fa61

Browse files
fix(web): keep earlier recurrence instances when opening an occurrence (#2739)
* fix(web): keep earlier recurrence instances when opening an occurrence Opening a later weekday occurrence rebuilt the RRULE with default INTERVAL=1, which falsely flipped preserve→series and hid earlier siblings. Treat INTERVAL=1 as equivalent to an omitted interval. Co-authored-by: Tyler Dane <tyler-dane@users.noreply.github.com> * test(web): harden recurrence open regression and INTERVAL normalize Apply draft updates in the regression so suppression stays null after mount, and strip INTERVAL=1 in either RRULE position. Co-authored-by: Tyler Dane <tyler-dane@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Tyler Dane <tyler-dane@users.noreply.github.com>
1 parent 2d021fd commit ea2fa61

2 files changed

Lines changed: 64 additions & 1 deletion

File tree

packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/useRecurrence/useRecurrence.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
createGridEventDraft,
1111
editGridEventDraft,
1212
resolveDraftRecurrenceRules,
13+
suppressedSeriesIdForDraft,
1314
} from "@web/events/grid-event-draft.adapter";
1415
import { useRecurrence } from "./useRecurrence";
1516
import { afterEach, describe, expect, it, mock, spyOn } from "bun:test";
@@ -214,6 +215,59 @@ describe("useRecurrence hook", () => {
214215
expect(nextSetDraft).not.toHaveBeenCalled();
215216
});
216217

218+
// Regression: opening a later weekday occurrence of a Google-style series
219+
// (BYDAY without INTERVAL=1) used to rewrite the rule on mount, flip
220+
// preserve→series, suppress earlier siblings, and leave only the clicked
221+
// day + forward previews visible.
222+
it("does not rewrite a preserve occurrence draft whose series omits INTERVAL=1", () => {
223+
const seriesRules = ["RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR"];
224+
const source = createMockEvent({
225+
schedule: SCHEDULE,
226+
recurrence: {
227+
kind: "occurrence",
228+
seriesId: EventIdSchema.parse("0123456789abcdefaaaaaaaa"),
229+
},
230+
});
231+
const editedDraft = editGridEventDraft(source);
232+
if (!editedDraft) throw new Error("expected edit draft");
233+
234+
expect(editedDraft.values.recurrence).toEqual({ kind: "preserve" });
235+
expect(suppressedSeriesIdForDraft(editedDraft)).toBeNull();
236+
237+
let draft: GridEventDraft = editedDraft;
238+
let setDraftCalls = 0;
239+
const setDraft: Dispatch<SetStateAction<GridEventDraft | null>> = (
240+
updater,
241+
) => {
242+
setDraftCalls++;
243+
const next = typeof updater === "function" ? updater(draft) : updater;
244+
if (next) draft = next;
245+
};
246+
247+
const { result, rerender } = renderHook(() =>
248+
useRecurrence(draft, { setDraft }, seriesRules),
249+
);
250+
251+
expect(result.current.hasRecurrence).toBe(true);
252+
expect(result.current.freq).toBe(Frequency.WEEKLY);
253+
expect(result.current.weekDays).toEqual([
254+
"monday",
255+
"tuesday",
256+
"wednesday",
257+
"thursday",
258+
"friday",
259+
]);
260+
expect(setDraftCalls).toBe(0);
261+
expect(draft.values.recurrence).toEqual({ kind: "preserve" });
262+
expect(suppressedSeriesIdForDraft(draft)).toBeNull();
263+
264+
rerender();
265+
266+
expect(setDraftCalls).toBe(0);
267+
expect(draft.values.recurrence).toEqual({ kind: "preserve" });
268+
expect(suppressedSeriesIdForDraft(draft)).toBeNull();
269+
});
270+
217271
// Regression for React error #185 (max update depth exceeded): a timed
218272
// UNTIL rule crashed the whole app for any non-UTC user. `options.until`
219273
// off a parsed CompassEventRRule is in the floating frame used for

packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/useRecurrence/useRecurrence.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,17 @@ const WEEKDAY_MAP: Record<
5656
{} as Record<number | string | keyof typeof WEEKDAY_RRULE_MAP, Weekday>,
5757
);
5858

59+
// Strip RRULE defaults the rrule library re-emits on rebuild (INTERVAL=1,
60+
// WKST) so open-for-edit doesn't treat serialization drift as a real edit.
61+
// Without this, Google-style rules like FREQ=WEEKLY;BYDAY=MO..FR flip the
62+
// draft from preserve→series and hide earlier sibling instances.
5963
const normalizeRecurrenceRule = (rule: string[] | null | undefined): string[] =>
60-
(rule ?? []).map((entry) => entry.replace(/;WKST=[A-Z]{2}/, ""));
64+
(rule ?? []).map((entry) =>
65+
entry
66+
.replace(/;INTERVAL=1(?=;|$)/g, "")
67+
.replace(/:INTERVAL=1;/g, ":")
68+
.replace(/;WKST=[A-Z]{2}/g, ""),
69+
);
6170

6271
const weekdayKeyFromByweekday = (
6372
day: number | Weekday,

0 commit comments

Comments
 (0)