fix(11-05): CR-01 surface reminderIsCustom to preserve custom VALARMs on edit

- expand.ts: add reminderIsCustom:boolean to CalendarOccurrence interface;
  derived from classifyValarms kind==='custom'; propagated to both
  non-recurring and recurring occurrence branches
- client.ts: mirror reminderIsCustom on CalendarOccurrence (atomic mirror)
- EventForm.tsx: extend deriveReminderValue to accept isCustom flag;
  returns '__custom__' when true, making the existing D-08 preserve branch
  live — editing a custom-alarm event now omits reminderLeadMinutes from
  the payload so outboxWorker extractValarms keeps the original VALARM
- Fix existing test fixtures (EventForm.test.tsx, EventDetailPopover.test.tsx)
  to include reminderIsCustom:false on all CalendarOccurrence literals

Fixes CAL-14 Pitfall 1: Apple Calendar absolute DATE-TIME / multi-VALARM
alarms no longer silently stripped on any edit round-trip from the PWA.
This commit is contained in:
Lucas Berger
2026-06-14 08:11:46 -04:00
parent 5d6cb47191
commit f6b47ebf1e
5 changed files with 54 additions and 10 deletions
+14
View File
@@ -80,6 +80,13 @@ export interface CalendarOccurrence {
* positive — N minutes before event start (timed) or N/1440 days before (all-day)
*/
reminderLeadMinutes: number | null;
/**
* True when the master event's alarm is a custom/absolute/multi-VALARM not reducible
* to a single before-event lead (CAL-14, CR-01, Phase 11 Plan 05).
* When true, reminderLeadMinutes is always null and the edit form must initialize the
* picker to '__custom__' to emit an absent payload field and preserve the original VALARM.
*/
reminderIsCustom: boolean;
}
/**
@@ -241,9 +248,14 @@ export function expandOccurrences(
// event's VALARM via classifyValarms. All occurrences inherit this value (series-level, D-10).
// preset/offlist → specific leadMinutes; custom/none → null (D-07/NOTIF-05).
// classifyValarms wraps ICAL.parse in try/catch (T-11-07 safe); safe on parse failure → null.
//
// Phase 11 Plan 05 (CR-01): reminderIsCustom — surface the 'custom' classification so the
// edit form can initialize the picker to '__custom__' and emit an absent payload field,
// preserving the original VALARM via the outboxWorker D-08 preserve path.
const alarmClass = classifyValarms(rawVevent);
const reminderLeadMinutes: number | null =
alarmClass.kind === 'preset' || alarmClass.kind === 'offlist' ? alarmClass.leadMinutes : null;
const reminderIsCustom: boolean = alarmClass.kind === 'custom';
// --- 4. Non-recurring event: single occurrence check ---
if (!isRecurring) {
@@ -281,6 +293,7 @@ export function expandOccurrences(
description: event.description ?? null,
hasRrule: isRecurring, // always false in the non-recurring branch
reminderLeadMinutes, // series-level (D-10)
reminderIsCustom, // CR-01 (Plan 05): true when alarm is absolute/multi-VALARM
});
}
return occurrences;
@@ -329,6 +342,7 @@ export function expandOccurrences(
description: event.description ?? null,
hasRrule: isRecurring, // always true in the recurring branch
reminderLeadMinutes, // series-level — all occurrences inherit the master's value (D-10)
reminderIsCustom, // CR-01 (Plan 05): true when alarm is absolute/multi-VALARM
});
}