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