Files
familysync/.planning/phases/03-event-write-back-pwa-install/03-05-SUMMARY.md
T

131 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: "03"
plan: "05"
subsystem: pwa-event-write-ui
tags: [react, tanstack-query, zustand, eventform, caldav-write, pwa]
dependency_graph:
requires: ["03-03"]
provides: ["EventForm component", "createEvent/updateEvent/fetchWritableCalendars client calls", "eventFormOpen/eventFormMode/eventFormUid Zustand keys"]
affects: ["CalendarShell", "EventDetailPopover (future edit trigger)"]
tech_stack:
added: []
patterns: ["TanStack Query useMutation + useQuery", "Zustand UI-only state", "bottom-sheet/dialog responsive overlay", "vi.hoisted() for mock TDZ", "D-01 last-used calendar localStorage", "D-02 conditional calendar picker", "D-11 whole-series recurrence presets", "T-03-15 plain-text JSX XSS guard"]
key_files:
created:
- apps/pwa/src/components/EventForm.tsx
- apps/pwa/src/api/client.test.ts
- apps/pwa/src/components/EventForm.test.tsx
modified:
- apps/pwa/src/api/client.ts
- apps/pwa/src/store/calendarStore.ts
- apps/pwa/src/components/CalendarShell.tsx
decisions:
- "D-01 calendar default: last-used URL from localStorage (eventForm.lastCalendarUrl), first writable calendar as fallback"
- "D-02 calendar picker: hidden when writableCalendars.length === 1, shown when >1 — driven by GET /api/events/writable-calendars"
- "T-03-15 XSS: all field values as plain-text JSX children in EventForm; no dangerouslySetInnerHTML in code"
- "vi.hoisted() required for mock factory variables to avoid TDZ (D-03-04-hoisting pattern)"
- "eventFormOpen selector added to CalendarShell per-field selector pattern (Bug B guard preserved)"
metrics:
duration_minutes: 6
completed_date: "2026-06-05"
tasks_completed: 3
files_created: 3
files_modified: 3
---
# Phase 03 Plan 05: Event Write UI (EventForm + Client Calls) Summary
**One-liner:** EventForm modal with timed/all-day/recurring fields, conditional calendar picker (D-02), and typed write client (createEvent/updateEvent/fetchWritableCalendars) wired to the Plan 03 write API via TanStack Query mutations.
## What Was Built
### Task 1: Typed write client calls + Zustand form-state keys
Extended `apps/pwa/src/api/client.ts` with:
- `CreateEventPayload` interface (title, allDay, start, end, recurrence, optional location/description/calendarUrl)
- `CreateEventResponse` interface ({ uid })
- `WritableCalendar` interface ({ url, displayName, color, isShared }) — D-03 server-authoritative shape
- `RecurrencePreset` type ('none'|'daily'|'weekly'|'monthly'|'yearly')
- `createEvent(payload)` — POST /api/events/create, credentials:include, returns {uid}
- `updateEvent(uid, payload)` — PATCH /api/events/:uid/edit
- `fetchWritableCalendars()` — GET /api/events/writable-calendars, parses `{ calendars }` envelope, returns `WritableCalendar[]`
Extended `apps/pwa/src/store/calendarStore.ts` with:
- `eventFormOpen: boolean` (default: false)
- `eventFormMode: 'create' | 'edit'` (default: 'create')
- `eventFormUid: string | null` (default: null)
- `setEventForm(open, mode?, uid?)` setter — no server data in Zustand
### Task 2: EventForm modal
New `apps/pwa/src/components/EventForm.tsx` (715 lines):
- Bottom sheet on phone (≤767px), centered 480px dialog on tablet/desktop — reuses EventDetailPopover pattern
- Fields per UI-SPEC §EventForm order: title, all-day toggle, start date/time, end date/time, calendar picker (conditional), recurrence, location, description
- All-day toggle (`role="switch"`, aria-checked): hides time inputs when on, restores 09:00/10:00 defaults when off
- Recurrence: `<select>` with None/Daily/Weekly/Monthly/Yearly (D-11 whole-series only)
- Calendar picker (D-02): hidden when `writableCalendars.length === 1`, shown when >1; populated from TanStack Query `['writableCalendars']` key using `fetchWritableCalendars()`
- D-01 default: last-used calendar URL from `localStorage.getItem('eventForm.lastCalendarUrl')`, falls back to first writable calendar
- Validation: "Title is required" + "End time must be after start" with `--color-destructive` styling
- `useMutation` from TanStack Query: calls `createEvent` in create mode, `updateEvent` in edit mode
- On success: `queryClient.invalidateQueries({ queryKey: ['events'] })`, writes last-used calendar to localStorage, `setEventForm(false)`
- Edit mode: pre-populates all fields from TanStack Query cache by eventFormUid
- `role="dialog"` `aria-modal="true"` `aria-label="New Event"/"Edit Event"`
- Focus moves to title input on open; Escape/backdrop/Cancel close without confirmation
- Save button: dark neutral fill (`--color-text-primary`), white label, shows Loader2 spinner + "Saving…" while pending
- T-03-15: all values as plain-text JSX children — no `dangerouslySetInnerHTML` anywhere
### Task 3: Mount EventForm + "New Event" FAB/toolbar in CalendarShell
Updated `apps/pwa/src/components/CalendarShell.tsx`:
- Added `setEventForm` and `eventFormOpen` per-field selectors (Bug B guard preserved)
- Phone layout: fixed FAB bottom-right (56×56px, dark neutral fill, Plus icon, 56px ≥ 44px touch target)
- Tablet/desktop layout: toolbar button above calendar content (dark neutral fill, Plus icon + "New Event" label)
- Both call `setEventForm(true, 'create')` via Zustand
- `{eventFormOpen && <EventForm />}` conditionally rendered in both phone and desktop layouts
## Test Coverage
- `apps/pwa/src/api/client.test.ts` (14 tests): write client calls POST/PATCH/GET, credentials, return shapes, error throws; Zustand form-state defaults and setEventForm setter
- `apps/pwa/src/components/EventForm.test.tsx` (23 tests): dialog role/aria, all required fields, all-day toggle, D-02 picker visibility, validation errors, create/edit mutations, close behaviors, edit mode pre-population
**Full suite: 81 tests, 8 test files — all green. TypeScript: tsc --noEmit passes.**
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 3 - Blocking] vi.hoisted() required for EventForm.test.tsx mock factory variables**
- **Found during:** Task 2 GREEN phase
- **Issue:** `vi.mock('../api/client.js', ...)` factory captured `mockCreateEvent` etc. before initialization (TDZ), causing `ReferenceError: Cannot access 'mockCreateEvent' before initialization`
- **Fix:** Moved mock function declarations into `vi.hoisted()` call per decision D-03-04-hoisting
- **Files modified:** `apps/pwa/src/components/EventForm.test.tsx`
- **Commit:** 86cefff
None — plan executed with one auto-fixed TDZ blocker.
## Threat Surface Scan
| Flag | File | Description |
|------|------|-------------|
| T-03-15 verified | apps/pwa/src/components/EventForm.tsx | All event field values rendered as plain-text JSX children; no `dangerouslySetInnerHTML` in code (appears only in JSDoc comment) |
| T-03-16 verified | apps/pwa/src/api/client.ts | `fetchWritableCalendars` reads server-authoritative D-03 set verbatim; no client-side writability derivation |
No new threat surface introduced beyond what was planned.
## Known Stubs
None. All API calls are wired to real Plan 03 endpoints. SyncStateToast feedback (post-202 sync polling) is intentionally deferred to Plan 03-06 per plan scope.
## Self-Check: PASSED
| Item | Status |
|------|--------|
| apps/pwa/src/components/EventForm.tsx | FOUND |
| apps/pwa/src/api/client.test.ts | FOUND |
| apps/pwa/src/components/EventForm.test.tsx | FOUND |
| 6400ce6 test(03-05): RED client calls | FOUND |
| 6ffcdcb feat(03-05): client calls GREEN | FOUND |
| df416a4 test(03-05): RED EventForm | FOUND |
| 86cefff feat(03-05): EventForm GREEN | FOUND |
| 69eac90 feat(03-05): CalendarShell wired | FOUND |