9.8 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-event-write-back-pwa-install | 06 | pwa-frontend |
|
|
|
|
|
|
Phase 03 Plan 06: Edit/Delete + SyncStateToast Summary
One-liner: Polled sync-state toast (D-05/D-06/D-08/D-09) + two-tap delete confirmation wired to EventDetailPopover footer, completing the edit/delete write-back vertical slices for CAL-05 and CAL-06.
Tasks Completed
| Task | Name | Commit | Files |
|---|---|---|---|
| 1 | deleteEvent + fetchSyncStatus + Zustand delete/sync keys | 8aeacc8 |
client.ts, calendarStore.ts |
| 2 | SyncStateToast with polled sync-status (D-05/D-06/D-08/D-09) | aa7c4c3 |
SyncStateToast.tsx, CalendarShell.tsx, EventForm.tsx |
| 3 | EventDetailPopover Edit/Delete footer + DeleteConfirmationDialog | 40322e1 |
EventDetailPopover.tsx, DeleteConfirmationDialog.tsx, CalendarShell.tsx |
What Was Built
Task 1 — Client calls + Zustand keys (RED: 8357cf9, GREEN: 8aeacc8)
apps/pwa/src/api/client.ts:
deleteEvent(uid): Promise<void>— DELETE/api/events/:uid, credentials:include, throws on !okfetchSyncStatus(uid): Promise<SyncStatus>— GET/api/events/sync-status?uid=- Exported types:
SyncStatusValue,SyncStatus
apps/pwa/src/store/calendarStore.ts:
deleteDialogOpen: boolean— default falsedeleteDialogUid: string | null— default nulllastSyncedUid: string | null— drives SyncStateToast pollingsetDeleteDialog(open, uid?)— settersetLastSyncedUid(uid)— setter (null to dismiss toast)
Task 2 — SyncStateToast (RED: 6874e1a, GREEN: aa7c4c3)
apps/pwa/src/components/SyncStateToast.tsx (210 lines):
useQuery(['syncStatus', lastSyncedUid], fetchSyncStatus)withrefetchIntervalcallback — 3000ms while pending, disabled on terminal status- States per UI-SPEC: pending (Loader2 spinner, "Syncing…"), done (Check, "Saved"), failed generic (AlertCircle, "Didn't save. Try again."), failed conflict/412 (conflict copy), dead ("Not saved. Check your connection.")
role="status"for pending/done;role="alert"for failed/deaddoneauto-dismisses after 2s viasetTimeout+setLastSyncedUid(null)failed/deadpersist until user taps dismiss (X button, 44px touch target)done+ 412 conflict both callqueryClient.invalidateQueries({ queryKey: ['events'] })(D-06/D-08)- No EventSource / SSE (D-09: polling only)
apps/pwa/src/components/EventForm.tsx: onSuccess now calls setLastSyncedUid(data.uid) instead of self-invalidating. SyncStateToast owns cache invalidation on done/conflict.
apps/pwa/src/components/CalendarShell.tsx: <SyncStateToast /> mounted in both phone and tablet/desktop layouts.
Task 3 — EventDetailPopover footer + DeleteConfirmationDialog (RED: 2fbeffe, GREEN: 40322e1)
apps/pwa/src/components/EventDetailPopover.tsx:
- Replaced
aria-hidden="true"reserved footer placeholder with a live flex row - Left: "Edit" ghost button (Edit2 icon,
--color-text-primary) — callssetEventForm(true, 'edit', uid)+ closes popover - Right: "Delete" ghost button (Trash2 icon,
--color-destructive) — callssetDeleteDialog(true, uid) - Both buttons: 44px touch targets, plain-text label children
apps/pwa/src/components/DeleteConfirmationDialog.tsx (208 lines):
- Centered modal, max-width 320px,
--color-overlaybackdrop, focus trap role="dialog",aria-modal="true", Escape to cancel- Heading "Delete event?" (18px/600), body "This will be removed from your Fastmail calendar."
- Cancel (ghost, 44px) closes without deleting; Delete (filled
--color-destructive, 48px, Trash2) firesdeleteEventmutation onSuccess:setLastSyncedUid(uid)→ SyncStateToast tracks it; closes dialog (setDeleteDialog(false)) and popover (setOpenEventId(null))- T-03-17: mandatory two-tap; no single-tap delete; no "don't ask again"
apps/pwa/src/components/CalendarShell.tsx: <DeleteConfirmationDialog /> mounted unconditionally in both layouts.
Verification
pnpm --filter @familysync/pwa test
Test Files 10 passed (10)
Tests 120 passed (120)
pnpm --filter @familysync/pwa exec tsc --noEmit
(no output — clean)
grep -c "EventSource" apps/pwa/src/components/SyncStateToast.tsx → 0
grep -q "refetchInterval" apps/pwa/src/components/SyncStateToast.tsx → PASS
grep -q "invalidateQueries" apps/pwa/src/components/SyncStateToast.tsx → PASS
grep -q "Delete event?" apps/pwa/src/components/DeleteConfirmationDialog.tsx → PASS
Deviations from Plan
Auto-fixed Issues
1. [Rule 1 - Bug] EventForm.test.tsx mock lacked setLastSyncedUid
- Found during: Task 2 GREEN
- Issue: EventForm calls
useCalendarStore((s) => s.setLastSyncedUid)(selector form). The existing test mock returned a static object regardless of selector, so the selector call returned the full mock object instead of the setter function. - Fix: Updated both the
vi.mockfactory and therenderFormhelper'smockImplementationto support the selector call pattern —if (typeof selector === 'function') return selector(state). - Files modified:
apps/pwa/src/components/EventForm.test.tsx - Commit:
aa7c4c3
2. [Rule 2 - Missing] SyncStateToast test used @testing-library/user-event not installed
- Found during: Task 2 RED
- Issue: Test imported
userEventbut only@testing-library/reactis installed. - Fix: Replaced with
fireEvent.clickfrom@testing-library/react(already in project). - Files modified:
apps/pwa/src/components/SyncStateToast.test.tsx
3. [Rule 1 - Bug] Fake timers blocked waitFor in SyncStateToast tests
- Found during: Task 2 GREEN
- Issue:
vi.useFakeTimers()inbeforeEachcaused allwaitForcalls to timeout becausewaitForusessetTimeoutinternally. - Fix: Use
vi.useFakeTimers({ shouldAdvanceTime: true })only for the specific auto-dismiss and refetch interval tests; use real timers for all query-resolution tests.afterEachcallsvi.useRealTimers(). - Files modified:
apps/pwa/src/components/SyncStateToast.test.tsx
4. [Rule 1 - Bug] SyncStateToast JSDoc comment contained "EventSource"
- Found during: Task 2 verify
- Issue: Acceptance criteria
grep -c "EventSource" ... returns 0would have failed due to a comment mentioning "No SSE / EventSource". - Fix: Changed comment to "No SSE — polling only (D-09)".
- Files modified:
apps/pwa/src/components/SyncStateToast.tsx
5. [Rule 3 - Required] EventDetailPopover.test.tsx needed selector-aware mock
- Found during: Task 3 GREEN (during mock update for new selector calls)
- Issue: EventDetailPopover now calls
useCalendarStorein selector form forsetEventFormandsetDeleteDialog. Old mock was not selector-aware. - Fix: Updated all mock implementations in
EventDetailPopover.test.tsxto support both selector and non-selector call patterns. - Files modified:
apps/pwa/src/components/EventDetailPopover.test.tsx
Known Stubs
None — all data is wired from real TanStack Query + Zustand state. No placeholder text or hardcoded empty values flow to UI rendering.
Threat Flags
No new network endpoints, auth paths, or schema changes introduced. All threats in plan's threat register are mitigated:
- T-03-17: Two-tap DeleteConfirmationDialog enforced; no inline single-tap delete
- T-03-18: failed/dead toast persists until dismissed; server refetch restores event on conflict
- T-03-19: fetchSyncStatus is member-scoped server-side (Plan 03-03 T-03-07); client queries current member's uid only
Self-Check: PASSED
Files exist:
- apps/pwa/src/components/SyncStateToast.tsx — FOUND
- apps/pwa/src/components/DeleteConfirmationDialog.tsx — FOUND
Commits exist: