shipped

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

MethodPathAuth/RoleRequestResponseErrors
POST/api/deals/{id}/move/org member{ stage_id: string, position?: number }updated deal403 if not member, 404 if not found
POST/api/tasks/{id}/reorder/org member{ position: number }updated task403 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 of react-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>. On onDragEnd, call dealsApi.move(id, { stage_id, position }) then queryClient.invalidateQueries
  • Tasks list: TasksPage.tsx — the list container is a <Droppable>, each task row is a <Draggable>. On onDragEnd, call tasksApi.reorder(id, { position }) then queryClient.invalidateQueries
  • Accessibility: @hello-pangea/dnd supports 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 onDragEnd shows a brief loading indicator on the moved card; optimistic update preferred (move immediately, revert on error)
    • Error: if move/reorder API fails, revert the card to its original position + toast “Không thể cập nhật thứ tự”
  • i18n: No new keys — error toast uses existing common.error_load or deals.error_load

7. Test Plan

#CaseTypeWhere
1Deals: drag deal to new stage → API called with correct stagefrontendfrontend/src/test/crm/DealsCrud.test.tsx
2Deals: failed move → card reverts to original positionfrontendfrontend/src/test/DealsCrud.test.tsx (same — DealsPage.test.tsx does not exist)
3Tasks: drag task to new position → API called with correct positionfrontendfrontend/src/test/TasksPage.test.tsx
4Drag-drop works via keyboardfrontendfrontend/src/test/dnd.test.ts (file renamed from DragDrop.test.tsx to dnd.test.ts)
5Deals: drag deal to new stage in E2Ee2elumi.spec.ts existing Deals describe
6Backend: deal move endpoint scopes to orgbackendbackend/tests/work/test_f68_drag_and_drop.py::TestDealMove
7Backend: task reorder endpoint scopes to orgbackendbackend/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 isDropDisabled blocks it)
  • Optimistic update conflicts: not a concern for v1 (single-user CEO context)

10. Diagrams & Docs Touched

  • frontend/AGENTS.md — add drag-drop convention
  • frontend/DIAGRAMS.md — no structural changes

11. Implementation Audit

#ClaimStatusEvidence
1@hello-pangea/dnd@18.0.1 installed✅ matchpackage.json
2Shared DnD wrapper at frontend/src/components/dnd/✅ matchDragDropContext.tsx + index.ts (barrel: reorderItems() + crossListMove())
3DealsPage.tsx kanban with handleDragEnd → dealsApi.move✅ matchDealsPage.tsx
4POST /api/deals/{id}/move/ backend endpoint✅ matchbackend/apps/crm/views.py (move action)
5TasksPage.tsx list with GripVertical drag handle + handleDragEnd✅ matchTasksPage.tsx
6POST /api/tasks/{id}/reorder/ backend endpoint✅ matchbackend/apps/work/views.py (reorder action)
7Tenant isolation on move/reorder endpoints✅ matchBoth endpoints scope to organization=request.tenant
810 unit tests (dnd.test.ts)✅ matchfrontend/src/test/dnd.test.ts
9Optimistic reorder with rollback on task error⚠️ partialTasksPage.tsx::handleDragEnd optimistically updates and shows an error toast, but no rollback assignment exists
10PATCH /api/deals/{id}/reorder/ for intra-stage position (not in §5 API)✅ matchbackend/apps/crm/views.py (reorder action, PATCH, accepts before_id)

12. Open Questions / Decisions

  • @hello-pangea/dnd vs @dnd-kit/core? Decision: @hello-pangea/dnd. It’s a drop-in replacement for react-beautiful-dnd (which many devs already know), handles horizontal (kanban) and vertical (list) dragging equally well, and has built-in keyboard accessibility. @dnd-kit is 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 in frontend/src/test/DealsCrud.test.tsx. Row 4 and §11 row 8 referenced frontend/src/test/DragDrop.test.tsx — the actual file is frontend/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.