docs(03-05): complete event write UI plan — EventForm + client calls + FAB
This commit is contained in:
@@ -107,7 +107,7 @@ Plans:
|
||||
**Wave 3** *(blocked on Wave 2)*
|
||||
|
||||
- [x] 03-04-PLAN.md — TDD: outbox worker state machine (D-05/06/07/08 retry/backoff/dead/conflict, edit-as-move ordering) + index.ts wiring
|
||||
- [ ] 03-05-PLAN.md — Frontend create/edit slice: write client calls + Zustand keys + EventForm (D-01/02/11) + New Event FAB
|
||||
- [x] 03-05-PLAN.md — Frontend create/edit slice: write client calls + Zustand keys + EventForm (D-01/02/11) + New Event FAB
|
||||
- [x] 03-07-PLAN.md — PWA install: VitePWA manifest + auth-safe SW denylist + icons + InstallPrompt (iOS walkthrough + Android prompt) (PWA-01/02)
|
||||
|
||||
**Wave 4** *(blocked on Wave 3)*
|
||||
@@ -161,7 +161,7 @@ Note: Phase 4 depends only on Phase 1 and can begin as soon as Phase 1 is comple
|
||||
|-------|----------------|--------|-----------|
|
||||
| 1. Foundation + Broker Spike | 4/4 | Complete | 2026-06-04 |
|
||||
| 2. Calendar Display | 5/5 | Complete | 2026-06-05 |
|
||||
| 3. Event Write-Back + PWA Install | 5/8 | In Progress| |
|
||||
| 3. Event Write-Back + PWA Install | 6/8 | In Progress| |
|
||||
| 4. Shared Lists + Live Sync | 0/? | Not started | - |
|
||||
| 5. Web Push Notifications | 0/? | Not started | - |
|
||||
|
||||
@@ -171,7 +171,7 @@ Note: Phase 4 depends only on Phase 1 and can begin as soon as Phase 1 is comple
|
||||
|
||||
**Goal:** [Captured for future planning] Abstract the calendar backend behind a provider interface so Fastmail/CalDAV is one implementation among potentially many. Shipping with a single provider is fine, but the broker, sync, and event-expansion layers should be structured so additional providers (e.g. other CalDAV hosts, Google Calendar, generic ICS feeds) can be added without rework. Captures the "provider" seam as an explicit architectural concern.
|
||||
**Requirements:** TBD
|
||||
**Plans:** 5/8 plans executed
|
||||
**Plans:** 6/8 plans executed
|
||||
|
||||
Plans:
|
||||
|
||||
|
||||
+10
-6
@@ -3,14 +3,14 @@ gsd_state_version: 1.0
|
||||
milestone: v1.0
|
||||
milestone_name: milestone
|
||||
status: executing
|
||||
stopped_at: Completed 03-04-PLAN.md
|
||||
last_updated: "2026-06-05T22:22:47.716Z"
|
||||
stopped_at: Completed 03-05-PLAN.md
|
||||
last_updated: "2026-06-05T22:32:25.900Z"
|
||||
last_activity: 2026-06-05
|
||||
progress:
|
||||
total_phases: 6
|
||||
completed_phases: 2
|
||||
total_plans: 17
|
||||
completed_plans: 14
|
||||
completed_plans: 15
|
||||
percent: 33
|
||||
---
|
||||
|
||||
@@ -26,7 +26,7 @@ See: .planning/PROJECT.md (updated 2026-06-03)
|
||||
## Current Position
|
||||
|
||||
Phase: 03 (event-write-back-pwa-install) — EXECUTING
|
||||
Plan: 6 of 8
|
||||
Plan: 7 of 8
|
||||
Status: Ready to execute
|
||||
Last activity: 2026-06-05
|
||||
|
||||
@@ -57,6 +57,7 @@ Progress: [███████░░░] 65%
|
||||
| Phase 03 P03-03 | 7 | 3 tasks | 2 files |
|
||||
| Phase 03 P03-07 | 5 | 2 tasks | 7 files |
|
||||
| Phase 03 P03-04 | 15 | 2 tasks | 3 files |
|
||||
| Phase 03 P03-05 | 6 | 3 tasks | 6 files |
|
||||
|
||||
## Accumulated Context
|
||||
|
||||
@@ -81,6 +82,9 @@ Recent decisions affecting current work:
|
||||
- [Phase ?]: D-03-07-layout: InstallPrompt desktop layout wraps CalendarContent in flex-column div so banner appears above calendar grid
|
||||
- [Phase ?]: D-03-04-hoisting: vi.hoisted() required for vi.mock() factory variables when test file has static import of module under test (TDZ issue)
|
||||
- [Phase ?]: D-03-04-where: Drizzle types disallow chained .where().where(); use and() in single .where() call
|
||||
- [Phase ?]: D-01 calendar default: last-used URL from localStorage (eventForm.lastCalendarUrl), first writable calendar as fallback
|
||||
- [Phase ?]: D-02 calendar picker: hidden when writableCalendars.length === 1, shown when >1 — authoritative from GET /api/events/writable-calendars
|
||||
- [Phase ?]: T-03-15 XSS: EventForm renders all values as plain-text JSX children; no dangerouslySetInnerHTML in code
|
||||
|
||||
### Pending Todos
|
||||
|
||||
@@ -109,6 +113,6 @@ Recent decisions affecting current work:
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-06-05T22:22:47.710Z
|
||||
Stopped at: Completed 03-04-PLAN.md
|
||||
Last session: 2026-06-05T22:32:25.896Z
|
||||
Stopped at: Completed 03-05-PLAN.md
|
||||
Resume file: None
|
||||
|
||||
@@ -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