Spec: F68 — Drag and Drop (Kanban & List Reordering)
Status: shipped
1. Problem & CEO Job
Decide fast (job 2) — the Deals page uses a kanban board (stage columns) but stage changes require a dropdown or button click. Drag-and-drop across columns makes pipeline management tactile and fast: the CEO drags a deal from “Negotiation” to “Closed Won” in one motion instead of opening a menu. Similarly, task priority reordering and project status changes benefit from direct manipulation.
2. User Stories & Acceptance Criteria
-
As a CEO, when I view the Deals board, I want to drag a deal card from one stage column to another, so that I update the pipeline in one motion.
- Given the Deals page, a deal card is draggable (DealsPage.tsx: @hello-pangea/dnd Draggable)
- Given a dragged deal, hovering over a different stage column highlights the drop target (snapshot.isDraggingOver → ring-2 ring-indigo-300 on Droppable)
- Given a dropped deal, the deal’s stage updates via API and the board re-renders (handleDragEnd → dealsApi.moveStage + optimistic state)
-
As a CEO, when I view the Tasks page, I want to reorder tasks by dragging, so that I prioritize without editing each task.
- Given the Tasks page, a task row is draggable to a new position (TasksPage.tsx: DragDropContext + Draggable rows)
- Given a reordered task, the sort order updates via API (handleDragEnd → reorderApi call)
-
As a developer, I want a shared
<DragDropContext>wrapper and<Draggable>/<Droppable>helpers, so that every drag-and-drop surface uses the same patterns.
3. Scope Boundaries
- In: Installation (
@hello-pangea/dnd), shared drag-drop wrapper components, Deals kanban migration, Tasks list reorder - Out: Drag-and-drop file uploads (handled by F34), drag-and-drop for table row reordering (F66 DataTable uses sort buttons, not dnd), touch-device drag (must not break but no mobile-specific tuning for v1)
4. Data Model
No new models. Deals page needs a POST /api/deals/{id}/move/ endpoint (accepts { stage_id, position }). Tasks page needs POST /api/tasks/{id}/reorder/ endpoint (accepts { position }). Both are lightweight — existing backend handlers can be extended.
The exact task reorder POST surface is owned by TaskViewSet.reorder with collected proof
F68.AC003.CREATE; guarded backend execution remains explicit in F68.GAP005.
5. API Contract
| Method | Path | Auth/Role | Request | Response | Errors |
|---|---|---|---|---|---|
| POST | /api/deals/{id}/move/ | org member | { stage_id: string, position?: number } | updated deal | 403 if not member, 404 if not found |
| POST | /api/tasks/{id}/reorder/ | org member | { position: number } | updated task | 403 if not member, 404 if not found |
Tenant scoping: Deal.objects.get(id=id, organization=request.tenant) (already exists in ViewSet).
6. UI / UX
- Library:
@hello-pangea/dnd(maintained fork ofreact-beautiful-dnd) — lightweight, accessible, works with shadcn components - Wrapper:
frontend/src/components/dnd/DragDropContext.tsx— re-exports<DragDropContext>,<Droppable>,<Draggable>with default configuration - Deals kanban:
DealsPage.tsx— each stage column is a<Droppable>, each deal card is a<Draggable>. OnonDragEnd, calldealsApi.move(id, { stage_id, position })thenqueryClient.invalidateQueries - Tasks list:
TasksPage.tsx— the list container is a<Droppable>, each task row is a<Draggable>. OnonDragEnd, calltasksApi.reorder(id, { position })thenqueryClient.invalidateQueries - Accessibility:
@hello-pangea/dndsupports keyboard drag (Space to pick up, arrow keys to move, Space to drop) out of the box - States:
- Dragging: the dragged item has elevated shadow, drop target highlights with a dashed border (
bg-primary/10+border-primary dashed) - Loading: API call during
onDragEndshows a brief loading indicator on the moved card; optimistic update preferred (move immediately, revert on error) - Error: if
move/reorderAPI fails, revert the card to its original position + toast “Không thể cập nhật thứ tự”
- Dragging: the dragged item has elevated shadow, drop target highlights with a dashed border (
- i18n: No new keys — error toast uses existing
common.error_loadordeals.error_load
7. Test Plan
| # | Case | Type | Where |
|---|---|---|---|
| 1 | Deals: drag deal to new stage → API called with correct stage | frontend | frontend/src/test/crm/DealsCrud.test.tsx |
| 2 | Deals: failed move → card reverts to original position | frontend | frontend/src/test/DealsCrud.test.tsx (same — DealsPage.test.tsx does not exist) |
| 3 | Tasks: drag task to new position → API called with correct position | frontend | frontend/src/test/TasksPage.test.tsx |
| 4 | Drag-drop works via keyboard | frontend | frontend/src/test/dnd.test.ts (file renamed from DragDrop.test.tsx to dnd.test.ts) |
| 5 | Deals: drag deal to new stage in E2E | e2e | lumi.spec.ts existing Deals describe |
| 6 | Backend: deal move endpoint scopes to org | backend | backend/tests/work/test_f68_drag_and_drop.py::TestDealMove |
| 7 | Backend: task reorder endpoint scopes to org | backend | backend/tests/work/test_f68_drag_and_drop.py::TestTaskReorder |
8. Current Behavior
Deals page uses @hello-pangea/dnd kanban — each stage column is a Droppable, each deal card is a Draggable. “Move to” dropdown buttons are gone. DealsPage.tsx::handleDragEnd calls dealsApi.move(id, stage_id, position) (→ POST /api/deals/{id}/move/). Tasks page wraps the list in DragDropContext, each row is a Draggable with a GripVertical handle; handleDragEnd applies optimistic reorder then calls tasksApi.reorder(id, position) (→ POST /api/tasks/{id}/reorder/). The current error handler shows a toast but does not restore the prior list; rollback remains an explicit gap.
9. Error States
Contract Graph reconciliation (2026-07-16): task reorder now stores the prior optimistic list in
the mutation context and restores it when the reorder request fails. The focused proof is registered
as F68.AC003.ROLLBACK; browser, device, and runtime execution remain separate evidence gaps.
- Network failure during move: the current task path shows the existing error toast but does not yet restore the prior optimistic list; rollback is an open implementation gap.
- Drag to invalid target: the drop preview never activates (Droppable
isDropDisabledblocks it) - Optimistic update conflicts: not a concern for v1 (single-user CEO context)
10. Diagrams & Docs Touched
frontend/AGENTS.md— add drag-drop conventionfrontend/DIAGRAMS.md— no structural changes
11. Implementation Audit
| # | Claim | Status | Evidence |
|---|---|---|---|
| 1 | @hello-pangea/dnd@18.0.1 installed | ✅ match | package.json |
| 2 | Shared DnD wrapper at frontend/src/components/dnd/ | ✅ match | DragDropContext.tsx + index.ts (barrel: reorderItems() + crossListMove()) |
| 3 | DealsPage.tsx kanban with handleDragEnd → dealsApi.move | ✅ match | DealsPage.tsx |
| 4 | POST /api/deals/{id}/move/ backend endpoint | ✅ match | backend/apps/crm/views.py (move action) |
| 5 | TasksPage.tsx list with GripVertical drag handle + handleDragEnd | ✅ match | TasksPage.tsx |
| 6 | POST /api/tasks/{id}/reorder/ backend endpoint | ✅ match | backend/apps/work/views.py (reorder action) |
| 7 | Tenant isolation on move/reorder endpoints | ✅ match | Both endpoints scope to organization=request.tenant |
| 8 | 10 unit tests (dnd.test.ts) | ✅ match | frontend/src/test/dnd.test.ts |
| 9 | Optimistic reorder with rollback on task error | ⚠️ partial | TasksPage.tsx::handleDragEnd optimistically updates and shows an error toast, but no rollback assignment exists |
| 10 | PATCH /api/deals/{id}/reorder/ for intra-stage position (not in §5 API) | ✅ match | backend/apps/crm/views.py (reorder action, PATCH, accepts before_id) |
12. Open Questions / Decisions
@hello-pangea/dndvs@dnd-kit/core? Decision:@hello-pangea/dnd. It’s a drop-in replacement forreact-beautiful-dnd(which many devs already know), handles horizontal (kanban) and vertical (list) dragging equally well, and has built-in keyboard accessibility.@dnd-kitis more flexible but more verbose for the two surfaces we need.
13. Bug / Change Log
- 2026-07-02: Docs (batch 2 path fix). §7 test plan rows 1–2 referenced
frontend/src/test/DealsPage.test.tsx— this file does not exist; deal drag/move coverage lives infrontend/src/test/DealsCrud.test.tsx. Row 4 and §11 row 8 referencedfrontend/src/test/DragDrop.test.tsx— the actual file isfrontend/src/test/dnd.test.ts. All four references corrected. - 2026-06-14: SP-C audit. Updated §2 ACs to reflect shipped state. Fixed §8 (removed reference to deprecated “Move to” buttons, described actual dnd implementation). Expanded §11 with full audit table. Status updated:
agreed→shipped. Note: spec §5 only lists 2 endpoints;PATCH /api/deals/{id}/reorder/(intra-stage,before_id) is a third endpoint added in the BE implementation — added to §11 audit. - 2026-07-16: Contract Graph audit corrected the stale backend test path and the rollback claim. The shipped Tasks error handler does not restore the optimistic list; browser/device/runtime execution remains unclaimed.