diff --git a/.sdlc/features/task-management-ui/design.md b/.sdlc/features/task-management-ui/design.md new file mode 100644 index 0000000..3eb3c52 --- /dev/null +++ b/.sdlc/features/task-management-ui/design.md @@ -0,0 +1,390 @@ +# 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. | diff --git a/.sdlc/features/task-management-ui/manifest.yaml b/.sdlc/features/task-management-ui/manifest.yaml index a677bf8..39191d0 100644 --- a/.sdlc/features/task-management-ui/manifest.yaml +++ b/.sdlc/features/task-management-ui/manifest.yaml @@ -13,7 +13,7 @@ artifacts: status: pending path: audit.md design: - status: pending + status: draft path: design.md qa_plan: status: pending