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

7.4 KiB
Raw Blame History

phase, plan, subsystem, tags, dependency_graph, tech_stack, key_files, decisions, metrics
phase plan subsystem tags dependency_graph tech_stack key_files decisions metrics
03 05 pwa-event-write-ui
react
tanstack-query
zustand
eventform
caldav-write
pwa
requires provides affects
03-03
EventForm component
createEvent/updateEvent/fetchWritableCalendars client calls
eventFormOpen/eventFormMode/eventFormUid Zustand keys
CalendarShell
EventDetailPopover (future edit trigger)
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
created modified
apps/pwa/src/components/EventForm.tsx
apps/pwa/src/api/client.test.ts
apps/pwa/src/components/EventForm.test.tsx
apps/pwa/src/api/client.ts
apps/pwa/src/store/calendarStore.ts
apps/pwa/src/components/CalendarShell.tsx
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)
duration_minutes completed_date tasks_completed files_created files_modified
6 2026-06-05 3 3 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