docs(03-05): complete event write UI plan — EventForm + client calls + FAB
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
---
|
||||
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 |
|
||||
Reference in New Issue
Block a user