feat(06-06): add whole-series edit confirmation prompt

- Create SeriesEditPrompt.tsx: bottom-sheet (phone) / dialog (desktop) matching
  DeleteConfirmationDialog pattern; focus trap, Escape=cancel, role=dialog/aria-modal
- UI-SPEC Surface 6 copy: 'Edit recurring series' heading, 'This will update all
  occurrences of this event.' body, 'Update series' accent-filled CTA, 'Cancel' ghost
- Import SeriesEditPrompt + add seriesEditPromptOpen state to EventForm
- handleSubmit gates on occurrence.hasRrule: opens prompt for recurring edits;
  executeSubmit() fires the existing whole-series PATCH on confirmation
- Save CTA label becomes 'Update series' for recurring edits (UI-SPEC primary CTAs)
- Non-recurring and create-mode Save behavior unchanged (no prompt)
This commit is contained in:
Lucas Berger
2026-06-10 11:40:33 -04:00
parent cbf5f98eb9
commit 96ef0b45b9
2 changed files with 264 additions and 6 deletions
+41 -6
View File
@@ -45,6 +45,7 @@ import {
computeNewTimedEnd,
computeNewAllDayEnd,
} from '../lib/eventDateTime.js'
import { SeriesEditPrompt } from './SeriesEditPrompt.js'
// ── Constants ─────────────────────────────────────────────────────────────────
@@ -276,6 +277,8 @@ export function EventForm() {
// ── Validation state ────────────────────────────────────────────────────────
const [errors, setErrors] = useState<{ title?: string; endTime?: string; recurrenceBound?: string }>({})
// D-08/D-09: series-edit confirmation prompt state
const [seriesEditPromptOpen, setSeriesEditPromptOpen] = useState(false)
// ── Mutations ───────────────────────────────────────────────────────────────
@@ -360,9 +363,13 @@ export function EventForm() {
return Object.keys(newErrors).length === 0
}
const handleSubmit = () => {
if (!validate()) return
/**
* Build the payload and fire the mutation. Called directly for non-recurring edits
* and create mode; called from the SeriesEditPrompt onConfirm for recurring edits
* (D-08/D-09). The whole-series PATCH reuses the existing /api/events/:uid/edit route
* which PUTs the master VEVENT — no RECURRENCE-ID, per D-08.
*/
const executeSubmit = () => {
// BUG A fix: serialize timed events to an unambiguous UTC instant here in
// the browser (operator's zone is known) instead of sending a naive local
// wall-clock string. The API container is UTC; a naive string was being read
@@ -403,6 +410,20 @@ export function EventForm() {
mutation.mutate(payload)
}
const handleSubmit = () => {
if (!validate()) return
const isEdit = eventFormMode === 'edit' && !!eventFormUid
// D-08/D-09: gate recurring-series edits behind the confirmation prompt
if (isEdit && occurrence?.hasRrule === true) {
setSeriesEditPromptOpen(true)
return
}
executeSubmit()
}
// ── Keyboard: Escape to close ───────────────────────────────────────────────
useEffect(() => {
@@ -466,11 +487,15 @@ export function EventForm() {
const isPhone = isPhoneBreakpoint()
const label = eventFormMode === 'edit' ? 'Edit Event' : 'New Event'
// D-08/D-09: CTA label is "Update series" for recurring edits (UI-SPEC "EventForm primary CTAs")
const isEditRecurring = eventFormMode === 'edit' && occurrence?.hasRrule === true
const saveLabel = mutation.isPending
? 'Saving…'
: eventFormMode === 'edit'
? 'Save Changes'
: 'Create Event'
: isEditRecurring
? 'Update series'
: eventFormMode === 'edit'
? 'Save Changes'
: 'Create Event'
const dialogStyle: React.CSSProperties = isPhone
? {
@@ -1014,6 +1039,16 @@ export function EventForm() {
</button>
</div>
</div>
{/* D-08/D-09: Series-edit confirmation prompt — shown when editing a recurring occurrence */}
<SeriesEditPrompt
open={seriesEditPromptOpen}
onConfirm={() => {
setSeriesEditPromptOpen(false)
executeSubmit()
}}
onCancel={() => setSeriesEditPromptOpen(false)}
/>
</>
)
}