7.4 KiB
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 |
|
|
|
|
|
|
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:
CreateEventPayloadinterface (title, allDay, start, end, recurrence, optional location/description/calendarUrl)CreateEventResponseinterface ({ uid })WritableCalendarinterface ({ url, displayName, color, isShared }) — D-03 server-authoritative shapeRecurrencePresettype ('none'|'daily'|'weekly'|'monthly'|'yearly')createEvent(payload)— POST /api/events/create, credentials:include, returns {uid}updateEvent(uid, payload)— PATCH /api/events/:uid/editfetchWritableCalendars()— GET /api/events/writable-calendars, parses{ calendars }envelope, returnsWritableCalendar[]
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 usingfetchWritableCalendars() - 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-destructivestyling useMutationfrom TanStack Query: callscreateEventin create mode,updateEventin 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
dangerouslySetInnerHTMLanywhere
Task 3: Mount EventForm + "New Event" FAB/toolbar in CalendarShell
Updated apps/pwa/src/components/CalendarShell.tsx:
- Added
setEventFormandeventFormOpenper-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 setterapps/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 capturedmockCreateEventetc. before initialization (TDZ), causingReferenceError: 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 |