foundary-test-1770784989/.sdlc/features/task-management-ui/design.md
rdev-worker ca6916c786
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
build: /design-feature task-management-ui
2026-02-11 05:49:09 +00:00

20 KiB

Design: Task Management UI

Architecture Approach

This feature is a frontend-only addition to the studio-ui React application. No backend changes are required — the feature consumes the REST endpoints provided by the data-models feature.

What's new:

  • 1 new npm dependency: @dnd-kit/core + @dnd-kit/sortable for drag-and-drop
  • 1 new npm dependency: @foundary-test-1770784989/api-client (already in monorepo, needs adding to studio-ui)
  • React Router setup in App.tsx (currently no routing)
  • ~10 new component files under src/features/board/
  • ~3 new hook files under src/features/board/hooks/
  • ~2 new type files under src/features/board/types/

What's modified:

  • apps/studio-ui/package.json — adds @dnd-kit/core, @dnd-kit/sortable, @foundary-test-1770784989/api-client
  • apps/studio-ui/src/App.tsx — adds React Router with route definitions and updated navigation
  • apps/studio-ui/src/main.tsx — wraps app in BrowserRouter

What's preserved:

  • All existing UI components and layout patterns
  • CSS variable system — no hardcoded colors
  • DashboardShell / Sidebar / Header layout wrapper

Data Model Changes

No backend data model changes. The frontend consumes existing API types.

Frontend TypeScript Types

// src/features/board/types/task.ts

interface Task {
  id: string;
  project_id: string;
  title: string;
  description: string;
  status: TaskStatus;
  priority: TaskPriority;
  created_at: string;
  updated_at: string;
}

type TaskStatus = 'open' | 'in_progress' | 'done' | 'cancelled';
type TaskPriority = 'low' | 'medium' | 'high' | 'critical';

interface Label {
  id: string;
  name: string;
  color: string;
  created_at: string;
}

interface Assignment {
  id: string;
  task_id: string;
  user_id: string;
  assigned_at: string;
}

// Enriched task with resolved associations for board rendering
interface BoardTask extends Task {
  labels: Label[];
  assignments: Assignment[];
}

These types mirror the {data} envelope response shapes from the data-models API.

API Response Envelope

All API responses use the {data, meta} envelope:

interface ApiResponse<T> {
  data: T;
  meta?: Record<string, unknown>;
}

API Changes

No new API endpoints. The feature consumes these existing endpoints from data-models:

Method Endpoint Usage
GET /api/studio-api/projects/{projectId}/tasks Fetch all tasks for the board
POST /api/studio-api/projects/{projectId}/tasks Create a new task
PUT /api/studio-api/tasks/{id} Update task (status change on drag, edit form)
DELETE /api/studio-api/tasks/{id} Delete task from edit modal
GET /api/studio-api/labels Fetch labels for filter dropdown and task forms
GET /api/studio-api/tasks/{taskId}/labels Fetch labels attached to a task
POST /api/studio-api/tasks/{taskId}/labels/{labelId} Attach label to task
DELETE /api/studio-api/tasks/{taskId}/labels/{labelId} Detach label from task
GET /api/studio-api/tasks/{taskId}/assignments Fetch assignments for a task
POST /api/studio-api/tasks/{taskId}/assignments Assign user to task
DELETE /api/studio-api/tasks/{taskId}/assignments/{id} Remove assignment
GET /api/studio-api/projects Fetch projects for project selection

Data Loading Strategy

On board mount, the UI makes parallel requests:

  1. GET /projects/{projectId}/tasks — all tasks for the project
  2. GET /labels — all labels (for filter and form dropdowns)
  3. For each task: GET /tasks/{taskId}/labels and GET /tasks/{taskId}/assignments

To avoid N+1 requests per task, the board will:

  • Fetch all tasks first
  • Then batch-fetch labels and assignments for all tasks in parallel using Promise.all
  • Merge into BoardTask[] enriched objects

This is acceptable for v1 since the spec states "no pagination" and data volumes are expected to be small. If performance becomes an issue, the backend can add a /tasks?include=labels,assignments query parameter in a future iteration.

Component Diagram

apps/studio-ui/src/
├── main.tsx                           # BrowserRouter wrapper
├── App.tsx                            # Routes + DashboardShell + Sidebar
├── features/
│   └── board/
│       ├── types/
│       │   └── task.ts                # Task, Label, Assignment, BoardTask types
│       ├── hooks/
│       │   ├── useTasks.ts            # Fetch/mutate tasks, labels, assignments
│       │   ├── useBoardFilters.ts     # Filter state (label, assignee)
│       │   └── useOptimisticUpdate.ts # Optimistic status change with rollback
│       ├── api.ts                     # API client calls (thin wrapper)
│       ├── BoardPage.tsx              # Page component (route target)
│       ├── BoardView.tsx              # Kanban layout with DnD context
│       ├── BoardColumn.tsx            # Single status column (droppable)
│       ├── TaskCard.tsx               # Draggable task card
│       ├── TaskCreateDialog.tsx       # Create task modal
│       ├── TaskEditDialog.tsx         # Edit task modal (with delete)
│       ├── FilterBar.tsx              # Label + assignee filter controls
│       └── BoardSkeleton.tsx          # Loading skeleton

Component Interaction Flow

┌─────────────────────────────────────────────────────────────┐
│ App.tsx (React Router)                                       │
│  Route: /projects/:projectId/board → <BoardPage>            │
└──────────────────────────┬──────────────────────────────────┘
                           │
┌──────────────────────────▼──────────────────────────────────┐
│ BoardPage.tsx                                                │
│  - Extracts projectId from URL params                        │
│  - Calls useTasks(projectId) hook                            │
│  - Calls useBoardFilters() hook                              │
│  - Renders Header, FilterBar, BoardView, Dialogs            │
│  - Manages dialog open/close state                           │
└──────┬──────────┬───────────┬────────────┬──────────────────┘
       │          │           │            │
  ┌────▼────┐ ┌──▼───────┐ ┌▼─────────┐ ┌▼────────────────┐
  │FilterBar│ │ BoardView │ │TaskCreate│ │  TaskEditDialog  │
  │         │ │ (DnD)    │ │Dialog    │ │  (with Delete)   │
  └─────────┘ └──┬───────┘ └──────────┘ └─────────────────┘
                  │
        ┌─────────┼─────────┐
        │         │         │
   ┌────▼───┐ ┌──▼────┐ ┌──▼────┐
   │Column  │ │Column │ │Column │
   │"To Do" │ │"In    │ │"Done" │
   │(open)  │ │Prog"  │ │       │
   └──┬─────┘ └──┬────┘ └──┬────┘
      │           │         │
   ┌──▼──┐     ┌──▼──┐   ┌──▼──┐
   │Task │     │Task │   │Task │
   │Card │     │Card │   │Card │
   └─────┘     └─────┘   └─────┘

Data Flow

useTasks hook (React state)
  │
  ├── tasks: BoardTask[]          ← fetched from API, enriched with labels/assignments
  ├── labels: Label[]             ← fetched from GET /labels
  ├── isLoading: boolean
  ├── error: string | null
  ├── createTask(input)           → POST /projects/{id}/tasks + refresh
  ├── updateTask(id, fields)      → PUT /tasks/{id} + optimistic update
  ├── deleteTask(id)              → DELETE /tasks/{id} + remove from state
  ├── attachLabel(taskId, labelId)→ POST /tasks/{taskId}/labels/{labelId}
  ├── detachLabel(taskId, labelId)→ DELETE /tasks/{taskId}/labels/{labelId}
  ├── assignUser(taskId, userId)  → POST /tasks/{taskId}/assignments
  └── unassignUser(taskId, asgId) → DELETE /tasks/{taskId}/assignments/{id}

useBoardFilters hook (React state)
  │
  ├── selectedLabels: string[]    ← label IDs to filter by
  ├── selectedAssignee: string    ← user ID to filter by (or empty)
  ├── setLabelFilter(ids)
  ├── setAssigneeFilter(userId)
  └── clearFilters()

Filtering is applied client-side:
  filteredTasks = tasks
    .filter(t => t.status !== 'cancelled')
    .filter(t => selectedLabels.length === 0 || t.labels.some(l => selectedLabels.includes(l.id)))
    .filter(t => !selectedAssignee || t.assignments.some(a => a.user_id === selectedAssignee))

Error Handling Strategy

API Call Failures

Operation Failure Mode Handling
Initial fetch (tasks, labels) Network error, 500 Show error alert in board area with "Retry" button. Board is non-functional without data.
Create task 400 (validation) Show inline validation errors in the create dialog (title required, length). Do not close dialog.
Create task 500 Show toast/alert: "Failed to create task. Please try again." Do not close dialog.
Update task (drag) Any error Revert optimistic update — move card back to original column. Show toast: "Failed to update task status."
Update task (edit form) 400 Show inline validation errors in edit dialog.
Update task (edit form) 500 Show toast: "Failed to update task." Do not close dialog.
Delete task Any error Show toast: "Failed to delete task." Keep task on board.
Label attach/detach Any error Show toast: "Failed to update labels." Revert label state on task.
Assignment add/remove Any error Show toast: "Failed to update assignments." Revert assignment state on task.

Optimistic Update Pattern (Drag & Drop)

1. User drops card in new column
2. UI immediately moves card to new column (optimistic)
3. PUT /tasks/{id} fires with new status
4. On success: no action needed (already in correct position)
5. On failure: revert card to previous column + show error toast

This is implemented via useOptimisticUpdate hook that stores the pre-mutation state and provides a rollback() function.

Error Display Components

  • Full-page error: Alert component from @foundary-test-1770784989/ui with AlertTitle and AlertDescription, plus a retry Button
  • Toast notifications: Lightweight inline alert positioned at the top of the board (auto-dismiss after 5 seconds). Uses the existing Alert component styled as a floating notification. No need for a toast library in v1 — a simple state-driven alert that auto-clears is sufficient.
  • Form validation errors: Inline text below form fields using text-[var(--error)] color

Security Considerations

Authentication

  • The board reads data via public GET endpoints (matching existing backend pattern where reads are unauthenticated)
  • Write operations (create, update, delete, label/assignment mutations) call authenticated endpoints
  • The @foundary-test-1770784989/api-client must be configured with a bearer token for write operations
  • If auth is disabled (AUTH_ENABLED=false), write endpoints work without a token (same as backend behavior)

Input Validation

  • Client-side: Title required, 1-200 chars (enforced before API call). Description max 2000 chars.
  • Server-side: Backend validates independently via app.BindAndValidate() — client validation is for UX only
  • XSS prevention: React's JSX escapes all interpolated strings by default. No dangerouslySetInnerHTML used anywhere. Label colors rendered via style={{ backgroundColor: label.color }} are safe since CSS color values cannot execute scripts.

Data Boundaries

  • Board is scoped to a single projectId from the URL — no cross-project data leakage
  • User IDs in assignments are opaque strings (no PII exposure beyond what the user enters)
  • No local storage or cookies used for task data

URL Parameter Safety

  • projectId from URL params is passed directly to API calls as a path segment — the API client handles URL encoding
  • No user-controlled values are used in DOM attributes or event handlers unsafely

Performance Considerations

Expected Load

  • Per spec: no pagination, all tasks returned. Suitable for < 500 tasks per project.
  • Labels and assignments are small collections (< 100 each typically).
  • All filtering is client-side — no additional API calls after initial load.

Rendering Performance

  • React key strategy: Each TaskCard keyed by task.id for stable reconciliation during drag
  • Column rendering: Tasks grouped by status into 3 arrays once, not re-computed on every render. Use useMemo for the grouping.
  • DnD performance: @dnd-kit uses CSS transforms (not layout recalculation) for drag animations, which is GPU-accelerated and performs well with hundreds of items.

Network Performance

  • Initial load: 1 request for tasks + 1 for labels + N requests for task labels/assignments (parallelized)
  • For a board with 50 tasks, this is ~102 parallel requests for associations. This is acceptable for v1 but should be noted as a future optimization point (backend could support ?include=labels,assignments on the task list endpoint).
  • Mutations: single request per action, no debouncing needed (user-initiated discrete actions)

Caching

  • No client-side caching layer in v1 (per spec: no state management library)
  • Data is refetched when navigating to the board page
  • After mutations (create, update, delete), affected data is updated in local state without a full refetch (except for create, which refreshes the full list to get server-generated fields)

Migration / Rollout Plan

Prerequisites

  1. The data-models feature must be fully implemented and deployed (provides all API endpoints)
  2. A project must exist in the database (board requires a projectId)

Phase 1: Project Setup

  1. Add dependencies to apps/studio-ui/package.json:
    • @dnd-kit/core
    • @dnd-kit/sortable
    • @foundary-test-1770784989/api-client (workspace dependency)
  2. Run pnpm install from monorepo root

Phase 2: Routing Infrastructure

  1. Wrap app in BrowserRouter in main.tsx
  2. Add Routes / Route definitions in App.tsx
  3. Move existing dashboard content to a DashboardPage component at route /
  4. Add board route: /projects/:projectId/board
  5. Update sidebar navigation to include "Board" link

Phase 3: Types and API Layer

  1. Create src/features/board/types/task.ts with TypeScript interfaces
  2. Create src/features/board/api.ts with API client wrapper functions
  3. Wire API client with base URL from environment variable (VITE_API_URL)

Phase 4: Core Board Components

  1. BoardPage.tsx — page wrapper, data fetching via hooks
  2. useTasks.ts — data fetching, mutation functions, state management
  3. BoardView.tsx — DnD context provider, column layout
  4. BoardColumn.tsx — droppable column with task count
  5. TaskCard.tsx — draggable card with title, priority badge, labels, assignee
  6. BoardSkeleton.tsx — loading state

Phase 5: Task Dialogs

  1. TaskCreateDialog.tsx — form with title, description, labels, assignee
  2. TaskEditDialog.tsx — pre-populated form, status/priority dropdowns, delete action

Phase 6: Filtering

  1. useBoardFilters.ts — filter state management
  2. FilterBar.tsx — label multi-select, assignee filter, clear button
  3. Wire filters into BoardPage to filter the displayed tasks

Rollout Checklist

  • pnpm install succeeds with new dependencies
  • pnpm build succeeds in apps/studio-ui
  • Board route renders empty board when no tasks exist
  • Tasks display in correct columns based on status
  • Drag and drop changes task status with optimistic update
  • Create task dialog creates task and adds to board
  • Edit task dialog updates task and reflects changes
  • Delete task removes from board after confirmation
  • Label and assignee filters work correctly
  • Error states display appropriately for failed API calls
  • Loading skeleton shows during data fetch
  • All UI uses CSS variables, no hardcoded colors
  • Board renders correctly inside DashboardShell layout

Key Design Decisions

  1. Feature folder structure (src/features/board/) — Groups all board-related components, hooks, types, and API calls together rather than scattering across src/components/, src/hooks/, etc. This is standard for feature-scoped React code and keeps the feature self-contained.

  2. Custom hooks over contextuseTasks and useBoardFilters are plain hooks that manage state locally in BoardPage. No React Context needed since the board is a single page with a flat component tree. Props are passed at most 2 levels deep (BoardPage → BoardView → TaskCard), which doesn't warrant context.

  3. @dnd-kit over react-beautiful-dnd@dnd-kit is actively maintained, has better accessibility support (keyboard DnD), smaller bundle size, and a more composable API. react-beautiful-dnd is in maintenance mode. The spec recommends @dnd-kit.

  4. Optimistic updates for drag, refetch for create — Drag-and-drop must feel instant, so status changes use optimistic updates with rollback. Task creation refreshes the task list because the server generates id, created_at, etc. that the client doesn't know upfront.

  5. N+1 association fetching in v1 — The board fetches labels and assignments per-task via parallel requests. This is a known tradeoff: it avoids backend changes and is acceptable for small task counts. A future backend ?include= parameter would eliminate this.

  6. Client-side filtering — Filters apply to the already-fetched task list in memory. No additional API calls. This is simpler and faster for the expected data volumes (< 500 tasks). Server-side filtering via query params is already supported by the backend but isn't needed for v1.

  7. No toast library — Error notifications use a simple state-driven Alert component that auto-dismisses. Adding a toast library (e.g., react-hot-toast) is unnecessary overhead for v1. If multiple features need toasts later, a shared toast system can be extracted.

  8. Assignee as free-text input — Since there is no user directory (per spec), the assignee field is a text input for user ID strings. The dropdown is populated by deriving unique user IDs from existing assignments on tasks, providing a "recent assignees" suggestion list.

  9. Route structure: /projects/:projectId/board — Uses path params (not query params) for projectId since it's the primary resource scope. Matches the RESTful URL convention used by the backend API.

  10. API base URL via environment variableVITE_API_URL configures the API client base URL. Defaults to '' (same origin) for production behind a reverse proxy. Set to http://localhost:8001 for local development.

Open Question Resolutions

Question Resolution
Project context Route uses /projects/:projectId/board. Sidebar shows "Board" link. For MVP, the user navigates to a project-specific URL. A project list/selector page is out of scope per spec.
Assignee data source Free-text input with suggestions derived from existing task assignments. No user directory dependency.
DnD library @dnd-kit/core + @dnd-kit/sortable per spec recommendation.
Optimistic vs. refetch Optimistic for drag-and-drop status changes (with rollback on error). Refetch after create. Direct state update after edit/delete.
State management Plain React hooks (useState, useMemo, useCallback). No Context, no external library.
Board URL /projects/:projectId/board using React Router path params.