build: /design-feature task-management-ui
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

This commit is contained in:
rdev-worker 2026-02-11 05:49:09 +00:00
parent 8135e05596
commit ca6916c786
2 changed files with 391 additions and 1 deletions

View File

@ -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<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 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. |

View File

@ -13,7 +13,7 @@ artifacts:
status: pending
path: audit.md
design:
status: pending
status: draft
path: design.md
qa_plan:
status: pending