# 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 ```typescript // 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: ```typescript interface ApiResponse { data: T; meta?: Record; } ``` ## 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.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 context** — `useTasks` 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 variable** — `VITE_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. |