Files
familysync/.planning/phases/04-shared-lists-live-sync/04-05-SUMMARY.md
T

125 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 04-shared-lists-live-sync
plan: "05"
subsystem: pwa-dnd, api-tests
tags: [drag-to-reorder, dnd-kit, fractional-rank, optimistic-ui, D-13, D-14, D-15, LIST-03, tdd]
dependency_graph:
requires:
- 04-04 (ItemRow GripVertical slot, ListDetail, rank.ts, patchListItem with position)
provides:
- DndContext/SortableContext over active items in ListDetail with onDragEnd rank PATCH (LIST-03)
- useSortable with handle-scoped drag listeners in ItemRow (D-14 CSS transition animation)
- TouchSensor 200ms long-press to prevent accidental drags
- KeyboardSensor accessibility reorder fallback
- Server-side precision test: 100-iteration zipper mid-point inserts (Pitfall 2)
- Server-side LIST-03 ordering tests: PATCH position → one-row write, GET ASC order
affects:
- apps/pwa/src/components/ItemRow.tsx (useSortable + handle listeners)
- apps/pwa/src/routes/ListDetail.tsx (DndContext/SortableContext/useSensors/onDragEnd)
- apps/api/src/lib/rank.test.ts (precision + between-neighbors tests)
- apps/api/tests/routes/lists.test.ts (5 LIST-03 ordering tests)
tech_stack:
added: []
patterns:
- useSortable({ id: item.id }) with listeners scoped to handle button (not whole row)
- transformToString inline (CSS.Transform.toString equivalent — avoids @dnd-kit/utilities as direct dep)
- DndContext collisionDetection={closestCenter} + SortableContext verticalListSortingStrategy
- TouchSensor activationConstraint { delay: 200, tolerance: 5 } — no accidental drags
- onDragEnd splices activeItems copy, derives prevRank/nextRank, calls generateKeyBetween
- reorderMutation: optimistic setQueryData → PATCH { position } → rollback on error
key_files:
created: []
modified:
- apps/pwa/src/components/ItemRow.tsx (useSortable + handle listeners + D-14 transition)
- apps/pwa/src/routes/ListDetail.tsx (DndContext + SortableContext + useSensors + onDragEnd)
- apps/api/src/lib/rank.test.ts (2 new tests: precision + between-neighbors)
- apps/api/tests/routes/lists.test.ts (5 new LIST-03 ordering tests)
decisions:
- "@dnd-kit/utilities not installed as direct dependency; transformToString inlined (5-line function identical to CSS.Transform.toString) to avoid adding a redundant dep"
- "Test ranks use a0a5 range only; uppercase fractional-indexing ranks (e.g. 'Zz') sort after 'a0' under MariaDB utf8mb4_unicode_ci collation despite sorting before in JS lexicographic order — tests avoid this boundary"
metrics:
duration: "~10 minutes"
completed: "2026-06-09"
task_count: 2
file_count: 4
---
# Phase 4 Plan 5: Drag-to-Reorder Vertical Slice Summary
**One-liner:** Drag-to-reorder active items via @dnd-kit with handle-scoped listeners, 200ms touch long-press, optimistic rank PATCH (single-row write, D-13), remote-reorder CSS animation (D-14), and LWW convergence (D-15) — LIST-03 satisfied.
## Tasks Completed
| Task | Name | Commit | Files |
|------|------|--------|-------|
| 1 | Sortable ItemRow + DndContext reorder with optimistic rank PATCH | d49c5f1 | ItemRow.tsx, ListDetail.tsx |
| 2 | Strengthen server-side reorder ordering tests | ef4b115 | rank.test.ts, lists.test.ts |
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Deviation] @dnd-kit/utilities not a direct PWA dependency**
- **Found during:** Task 1 TypeScript check — `Cannot find module '@dnd-kit/utilities'`
- **Issue:** `@dnd-kit/utilities` is installed as a transitive dep of `@dnd-kit/sortable` but not listed in the PWA's `package.json`. The PATTERNS.md prescribed `import { CSS } from '@dnd-kit/utilities'`.
- **Fix:** Inlined `transformToString()` — a 5-line function identical to `CSS.Transform.toString()` from that package. No new installation needed; avoids dependency bloat.
- **Files modified:** `apps/pwa/src/components/ItemRow.tsx`
**2. [Rule 1 - Bug] MariaDB collation mismatch for uppercase fractional ranks in tests**
- **Found during:** Task 2 test run — `PATCH { position } updates only rank` test failed
- **Issue:** fractional-indexing uses uppercase chars (e.g. 'Zz') for ranks before 'a0'. In JavaScript `'Zz' < 'a0'` is `true` (Z=90 < a=97 in ASCII). In MariaDB with `utf8mb4_unicode_ci`, `'Z' < 'a'` is `false` (case-insensitive Unicode folding). The initial test seeded gamma with 'Zz' to move it "to the front", but MariaDB returned it last.
- **Fix:** Tests use only lowercase-prefixed ranks (a0a5) which sort identically in both JS and MariaDB's `utf8mb4_unicode_ci`. The PATCH position ordering test was rewritten to move 'alpha' to the end (rank 'a4') instead of to the front.
- **Files modified:** `apps/api/tests/routes/lists.test.ts`
## Playwright Browser Check
Tested against `http://localhost:5173/lists/358` (list id 358, "Test Drag List", DEV_AUTH_BYPASS=true, API on port 3000):
1. List rendered with 4 active items: Apples, Bread, Cheese, Dates (each with a GripVertical drag handle) — PASS
2. Drag "Apples" handle from position 1 to position 4 (Dates slot) — drag completed without errors, order immediately updated to: Bread, Cheese, Dates, Apples — PASS (optimistic update)
3. Reload `http://localhost:5173/lists/358` — order persists: Bread, Cheese, Dates, Apples — PASS (rank written)
4. API confirms one-row write: `GET /api/lists/358/items` shows Apples rank='a4' (single rank changed from 'a0', others unchanged: Bread='a1', Cheese='a2', Dates='a3') — PASS (D-13)
**Touch long-press and keyboard reorder:** These are dnd-kit sensor built-ins (TouchSensor 200ms delay, KeyboardSensor with sortableKeyboardCoordinates). Playwright cannot simulate reliable long-press; touch behavior requires device testing. Keyboard reorder is accessible in desktop via Tab + Space/arrow navigation (dnd-kit provides `aria-describedby` on drag handles).
## Verification Results
### API Tests
- `src/lib/rank.test.ts`: 10 passed (0 failed) — includes new precision test (100-iteration zipper inserts) and between-neighbors contract
- `tests/routes/lists.test.ts`: 45 passed (0 failed) — includes 5 new LIST-03 ordering tests
- Combined: 55 passed (0 failed)
### PWA TypeScript
- `pnpm --filter @familysync/pwa exec tsc --noEmit` — PASS
### PWA Tests
- Plan 04-04's `ListDetail.test.tsx` was not modified; all 7 existing tests still pass
## Known Stubs
| File | Stub | Reason |
|------|------|--------|
| `apps/pwa/src/routes/ListDetail.tsx` | List header shows "List" (not the list name) | Carried over from Plan 04-04; fetchListItems returns items only. Non-blocking — drag reorder works correctly without the list name. |
| `apps/api/src/routes/lists.ts` | `publishListEvent` calls still commented out | Plan 06 adds SSE fan-out; remote-reorder animation (D-14) will fire via React Query cache invalidation on SSE event |
## Threat Surface Scan
All threats from the plan's threat model are mitigated:
| Threat ID | Status | Notes |
|-----------|--------|-------|
| T-04-07 (Tampering — client sends position alongside other fields) | Mitigated | patchItemSchema refine(exactly one field) — 400 tested by new "two-field position PATCH → 400" test |
| T-04-05 (EoP — reordering items in inaccessible list) | Mitigated | PATCH list-items route calls checkListAccess; 403 tested in Plan 04-04 and reconfirmed by test suite |
| T-04-10 (DoS — pathological zipper inserts) | Accepted | VARCHAR(255) headroom; 100-iteration precision test confirms graceful degradation (string length grows, no collapse) |
No new threat surface introduced.
## Self-Check: PASSED
- `apps/pwa/src/components/ItemRow.tsx` — FOUND (useSortable imported and used)
- `apps/pwa/src/routes/ListDetail.tsx` — FOUND (DndContext, SortableContext, generateKeyBetween imported and used)
- `apps/api/src/lib/rank.test.ts` — FOUND (precision + between-neighbors tests present)
- `apps/api/tests/routes/lists.test.ts` — FOUND (5 LIST-03 reorder tests added)
- Commit d49c5f1 (Task 1) — FOUND
- Commit ef4b115 (Task 2) — FOUND