# Design: Task Management UI ## Architecture Approach This feature transforms `studio-ui` from a static placeholder dashboard into a functional task management application. It adds client-side routing, API integration, state management, and CRUD pages that consume the REST APIs defined by the `data-models` feature. | Layer | What Changes | |-------|-------------| | **Routing** (`src/App.tsx`) | Replace static content with React Router; add route definitions | | **Pages** (`src/pages/`) | New page components: ProjectList, ProjectDetail | | **Components** (`src/components/`) | New feature components: ProjectCard, TaskBoard, TaskColumn, TaskCard, dialogs | | **Hooks** (`src/hooks/`) | Custom hooks for API data fetching and mutation | | **API** (`src/api/`) | API client instance and typed request functions | | **Types** (`src/types/`) | TypeScript interfaces matching backend response shapes | | **Navigation** (`src/App.tsx`) | Update sidebar items to include Projects route | The existing shared packages (`ui`, `layout`, `auth`, `api-client`) are consumed as-is. No shared package changes are required. ## Data Model (Frontend Types) TypeScript interfaces mirroring the backend API response shapes: ```typescript // src/types/project.ts interface Project { id: string; name: string; description: string; status: "active" | "archived"; created_at: string; updated_at: string; } interface CreateProjectRequest { name: string; description?: string; } interface UpdateProjectRequest { name: string; description?: string; status?: "active" | "archived"; } // src/types/task.ts type TaskStatus = "todo" | "in_progress" | "done"; type TaskPriority = "low" | "medium" | "high"; interface Task { id: string; project_id: string; title: string; description: string; status: TaskStatus; priority: TaskPriority; position: number; created_at: string; updated_at: string; } interface CreateTaskRequest { title: string; description?: string; priority?: TaskPriority; } interface UpdateTaskRequest { title?: string; description?: string; status?: TaskStatus; priority?: TaskPriority; position?: number; } // src/types/label.ts interface Label { id: string; project_id: string; name: string; color: string; created_at: string; updated_at: string; } interface CreateLabelRequest { name: string; color: string; } // src/types/assignment.ts interface Assignment { id: string; task_id: string; assignee: string; created_at: string; } interface CreateAssignmentRequest { assignee: string; } ``` All responses from the API are wrapped in the `{data, meta}` envelope: ```typescript interface APIResponse { data: T; meta?: { request_id: string; timestamp: string; }; } ``` ## API Integration Layer ### Client Setup ```typescript // src/api/client.ts import { createClient } from '@foundary-test-1770773605/api-client'; const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8001'; export const api = createClient({ baseUrl: `${API_BASE}/api/studio-api`, }); ``` ### API Functions Typed wrappers around the API client, one file per resource: ```typescript // src/api/projects.ts export async function listProjects(): Promise { const res = await api.get>('/projects'); return res.data; } export async function getProject(id: string): Promise { const res = await api.get>(`/projects/${id}`); return res.data; } export async function createProject(req: CreateProjectRequest): Promise { const res = await api.post>('/projects', req); return res.data; } export async function updateProject(id: string, req: UpdateProjectRequest): Promise { const res = await api.put>(`/projects/${id}`, req); return res.data; } export async function deleteProject(id: string): Promise { await api.delete(`/projects/${id}`); } ``` Similar patterns for `src/api/tasks.ts`, `src/api/labels.ts`, `src/api/assignments.ts`. Task and label create/list endpoints use the nested project path (`/projects/{projectId}/tasks`), while get/update/delete use flat paths (`/tasks/{id}`). ## State Management ### Approach: React hooks + local component state No global state library (Redux, Zustand) is needed. Each page manages its own data via custom hooks that encapsulate fetch/mutate/loading/error state. This matches the simplicity of the current codebase. ### Custom Hooks ```typescript // src/hooks/useProjects.ts function useProjects() { const [projects, setProjects] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const refresh = useCallback(async () => { ... }, []); useEffect(() => { refresh(); }, [refresh]); return { projects, loading, error, refresh }; } // src/hooks/useProject.ts function useProject(id: string) { // Fetches single project + its tasks + labels const [project, setProject] = useState(null); const [tasks, setTasks] = useState([]); const [labels, setLabels] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const refresh = useCallback(async () => { // Parallel fetch: project, tasks, labels const [proj, taskList, labelList] = await Promise.all([ getProject(id), listTasks(id), listLabels(id), ]); setProject(proj); setTasks(taskList); setLabels(labelList); }, [id]); return { project, tasks, labels, loading, error, refresh }; } ``` After a mutation (create/update/delete), the hook's `refresh()` is called to re-fetch from the server. This avoids stale state and keeps the implementation simple. ## Component Diagram ``` src/ ├── App.tsx # Router + DashboardShell layout ├── api/ │ ├── client.ts # API client instance │ ├── projects.ts # Project API functions │ ├── tasks.ts # Task API functions │ ├── labels.ts # Label API functions │ └── assignments.ts # Assignment API functions ├── types/ │ ├── project.ts # Project types │ ├── task.ts # Task types │ ├── label.ts # Label types │ └── assignment.ts # Assignment types ├── hooks/ │ ├── useProjects.ts # Project list data │ └── useProject.ts # Single project + tasks + labels ├── pages/ │ ├── ProjectListPage.tsx # /projects route │ └── ProjectDetailPage.tsx # /projects/:id route ├── components/ │ ├── ProjectCard.tsx # Card for project list │ ├── CreateProjectDialog.tsx # Dialog for creating/editing projects │ ├── DeleteConfirmDialog.tsx # Reusable delete confirmation │ ├── TaskBoard.tsx # Kanban-style board with columns │ ├── TaskColumn.tsx # Single status column │ ├── TaskCard.tsx # Individual task card │ ├── CreateTaskDialog.tsx # Dialog for creating/editing tasks │ ├── LabelList.tsx # Label management section │ ├── CreateLabelDialog.tsx # Dialog for creating labels │ └── AssignmentList.tsx # Assignment display in task detail ├── index.css ├── main.tsx └── lib/ └── logger.ts ``` ### Component Interaction Flow ``` App.tsx ├── DashboardShell (from @layout) │ ├── Sidebar → nav items including /projects │ ├── Header │ └── │ ├── /projects → ProjectListPage │ │ ├── useProjects() hook │ │ ├── ProjectCard[] (grid) │ │ └── CreateProjectDialog │ │ │ └── /projects/:id → ProjectDetailPage │ ├── useProject(id) hook │ ├── Project header (name, status, actions) │ ├── TaskBoard │ │ ├── TaskColumn (todo) │ │ │ └── TaskCard[] │ │ ├── TaskColumn (in_progress) │ │ │ └── TaskCard[] │ │ └── TaskColumn (done) │ │ └── TaskCard[] │ ├── CreateTaskDialog │ ├── LabelList │ │ └── CreateLabelDialog │ └── DeleteConfirmDialog ``` ## Page Designs ### Project List Page (`/projects`) ``` ┌─────────────────────────────────────────────────┐ │ Header: "Projects" [+ New Project] │ ├─────────────────────────────────────────────────┤ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ Project A │ │ Project B │ │ Project C │ │ │ │ │ │ │ │ │ │ │ │ Desc... │ │ Desc... │ │ Desc... │ │ │ │ │ │ │ │ │ │ │ │ ●Active │ │ ●Active │ │ ○Archived │ │ │ │ 5 tasks │ │ 12 tasks │ │ 3 tasks │ │ │ └──────────┘ └──────────┘ └──────────┘ │ │ │ │ ┌──────────────────────────────────────────┐ │ │ │ No projects yet? Create your first one! │ │ │ │ [+ Create Project] │ │ │ └──────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────┘ ``` - Grid layout: `grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4` - Empty state card when no projects exist - Project cards link to `/projects/:id` - Badge shows status (success for active, secondary for archived) ### Project Detail Page (`/projects/:id`) ``` ┌─────────────────────────────────────────────────┐ │ Projects > Project A [Edit] [Del] │ ├─────────────────────────────────────────────────┤ │ │ │ Labels: [Bug] [Feature] [Docs] [+ Add Label] │ │ │ │ ┌─────────────┬──────────────┬───────────────┐ │ │ │ Todo (3) │ In Progress │ Done (5) │ │ │ │ │ (2) │ │ │ │ │ ┌─────────┐│ ┌──────────┐ │ ┌───────────┐ │ │ │ │ │ Task 1 ││ │ Task 4 │ │ │ Task 6 │ │ │ │ │ │ ▲ High ││ │ ● Medium │ │ │ ▼ Low │ │ │ │ │ │ @alice ││ │ @bob │ │ │ @alice │ │ │ │ │ └─────────┘│ └──────────┘ │ └───────────┘ │ │ │ │ ┌─────────┐│ │ ┌───────────┐ │ │ │ │ │ Task 2 ││ │ │ Task 7 │ │ │ │ │ │ ● Med ││ │ │ ● Medium │ │ │ │ │ └─────────┘│ │ └───────────┘ │ │ │ │ │ │ │ │ │ │ [+ Add] │ │ │ │ │ └─────────────┴──────────────┴───────────────┘ │ │ │ └─────────────────────────────────────────────────┘ ``` - Kanban board with three fixed columns: Todo, In Progress, Done - Each column has a header with count badge - Task cards show title, priority badge, assignee(s) - Clicking a task card opens an edit sheet/dialog - "Add Task" button at the bottom of the Todo column (or a floating button) - Labels displayed as a horizontal row of badges above the board ## Key Component Specifications ### ProjectCard ```tsx // Uses: Card, CardHeader, CardTitle, CardDescription, CardContent, Badge // Props: project: Project, taskCount?: number, onClick: () => void // Behavior: Click navigates to /projects/:id ``` ### TaskBoard ```tsx // Props: tasks: Task[], onCreateTask, onUpdateTask, onDeleteTask // Groups tasks by status into three TaskColumn components // Horizontal scroll on narrow viewports: flex with overflow-x-auto ``` ### TaskCard ```tsx // Uses: Card, Badge // Props: task: Task, assignments: Assignment[], onClick: () => void // Shows: title, priority badge (color-coded), assignee avatars/names // Click opens task edit dialog ``` ### Priority Badge Colors | Priority | Badge Variant | Color | |----------|--------------|-------| | High | `error` | Red | | Medium | `warning` | Yellow/Amber | | Low | `info` | Blue | ### Status Column Colors | Status | Label | |--------|-------| | `todo` | "Todo" | | `in_progress` | "In Progress" | | `done` | "Done" | ### CreateProjectDialog ```tsx // Uses: Dialog, Input, Textarea, Button, Label // Fields: name (required, max 200), description (optional, max 1000) // Mode: create (POST) or edit (PUT) based on whether project prop is passed // On submit: calls API, then refresh() from hook ``` ### CreateTaskDialog ```tsx // Uses: Dialog, Input, Textarea, Select, Button, Label // Fields: title (required, max 300), description (optional, max 5000), // priority (select: low/medium/high), status (select: todo/in_progress/done) // Mode: create (POST, status defaults to todo) or edit (PUT) ``` ### DeleteConfirmDialog ```tsx // Uses: Dialog, Button // Props: title, message, onConfirm, onCancel // "Are you sure?" with destructive-variant confirm button // Reusable for projects, tasks, labels ``` ## Routing Setup ```tsx // src/App.tsx import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; function App() { return ( } header={...}> } /> } /> } /> ); } ``` The sidebar `navItems` will be updated: ```tsx const navItems: NavItem[] = [ { label: 'Projects', href: '/projects', icon: FolderKanban, active: true }, { label: 'Settings', href: '/settings', icon: Settings }, ]; ``` Active state should be derived from current route via `useLocation()`. ## Error Handling Strategy ### API Error Handling All API functions handle the `{data, meta}` envelope and throw on non-2xx responses: ```typescript // Errors propagated from api-client try { await createProject(req); } catch (err) { // err.status: HTTP status code // err.message: error message from server // err.body: full error response } ``` ### Per-Component Error States | Scenario | UI Behavior | |----------|-------------| | Page load failure | Alert banner: "Failed to load projects. Try again." + retry button | | Create/Update validation (400/422) | Field-level error messages in dialog | | Duplicate name conflict (409) | Inline error: "A project with this name already exists" | | Not found (404) | Redirect to project list with toast/alert | | Network error | Alert banner: "Network error. Check your connection." | | Delete failure | Toast/alert: "Failed to delete. Try again." | ### Error Display Components Errors are shown using the existing `Alert` component: ```tsx Error {error.message} ``` ## Security Considerations ### Input Validation (Client-Side) Client-side validation mirrors backend constraints to provide immediate feedback: - Project name: required, 1-200 chars - Project description: max 1000 chars - Task title: required, 1-300 chars - Task description: max 5000 chars - Label name: required, 1-50 chars - Label color: hex format validation (#RRGGBB) - Assignee: required, 1-200 chars Client-side validation is **defense in depth** — the backend always re-validates. ### XSS Prevention - React's JSX auto-escapes rendered content — no `dangerouslySetInnerHTML` used - User-provided strings (names, descriptions) rendered as text nodes - Label colors rendered only as CSS `background-color` values, validated as hex ### Authentication - Auth is out of scope per spec — no login UI or token management in this feature - The API client supports bearer token injection when auth is later enabled - Write operations (create/update/delete) work without auth in development (matching backend's `AUTH_ENABLED=false` default) ### CORS - Dev mode: Vite proxy or CORS headers configured for `localhost:3001` → `localhost:8001` - Production: Same-origin or configured CORS in the backend ## Performance Considerations ### Data Fetching - **Project list:** Single API call (`GET /projects`). No pagination needed for initial scale. - **Project detail:** Three parallel API calls (`GET /projects/:id`, `GET /projects/:id/tasks`, `GET /projects/:id/labels`). Uses `Promise.all` for concurrent loading. - **Assignments:** Fetched lazily when a task card is expanded/clicked, not on page load. This avoids N+1 API calls. ### Rendering - Task board: Three columns rendered as simple lists. No virtualization needed at current scale (< 100 tasks per project). - React key optimization: All lists keyed by entity `id` (UUID). - Dialog components: Rendered conditionally (not mounted until triggered). ### Bundle Size - No additional large dependencies. Uses existing React, React Router, and shared packages. - lucide-react icons are tree-shaken per import. - No chart libraries or heavy visualization for this feature. ### Caching - No client-side cache layer in v1. Each page navigation re-fetches. - Future optimization: add SWR or React Query for caching, deduplication, and background refetching. ## Migration / Rollout Plan ### Prerequisites 1. **Backend:** The `data-models` feature must be implemented and deployed first. The API endpoints (`/projects`, `/tasks`, etc.) must be available. 2. **Database:** PostgreSQL with migrations applied (handled by `data-models` feature). ### Rollout Steps **Step 1: Add routing and page structure** - Install `react-router-dom` (if not already present) - Replace static `App.tsx` content with router setup - Create empty page components **Step 2: Add API integration layer** - Create `src/api/client.ts` with base configuration - Create typed API function files per resource - Create TypeScript type definitions **Step 3: Build project list page** - `ProjectListPage` with `useProjects` hook - `ProjectCard` component - `CreateProjectDialog` (create + edit modes) - `DeleteConfirmDialog` (reusable) **Step 4: Build project detail page** - `ProjectDetailPage` with `useProject` hook - `TaskBoard` + `TaskColumn` + `TaskCard` - `CreateTaskDialog` (create + edit modes) - `LabelList` + `CreateLabelDialog` - `AssignmentList` within task edit **Step 5: Polish and integration** - Loading states, error states, empty states - Navigation active states from route - Environment variable for API URL ### Backward Compatibility - The static dashboard content is **replaced**, not preserved alongside - No existing user data to migrate — this is a new feature - The sidebar navigation changes (removes placeholder items, adds Projects) - No API contract changes — this is a pure frontend addition ### Environment Variables | Variable | Default | Purpose | |----------|---------|---------| | `VITE_API_URL` | `http://localhost:8001` | Backend API base URL | ### Testing Strategy - **Manual testing:** All CRUD operations through the UI against the running backend - **Component tests:** Optional unit tests for complex components (TaskBoard grouping logic) - **E2E tests:** Out of scope for v1; can be added later with Playwright/Cypress ## Design Decisions 1. **React hooks over global state:** The app is simple enough that local state per page is sufficient. No Redux/Zustand complexity. Custom hooks encapsulate fetch logic and make it reusable. 2. **Kanban board over table view:** Task management benefits from visual status columns. The three-column board (Todo, In Progress, Done) provides immediate status overview. Table view can be added as an alternative later. 3. **Dialogs over separate pages for CRUD:** Create/edit operations use modal dialogs rather than navigating to separate form pages. This keeps the user in context and matches the design system's Dialog component. 4. **Lazy assignment loading:** Assignments are fetched per-task when clicked, not upfront. This avoids N+1 queries on page load and keeps initial render fast. 5. **No drag-and-drop in v1:** The backend has a `position` field, but implementing drag-and-drop adds significant complexity (library dependency, position recalculation, optimistic updates). Deferred to a follow-up feature. 6. **Shared DeleteConfirmDialog:** One reusable confirmation dialog rather than per-entity delete dialogs. Reduces code duplication. 7. **Environment-based API URL:** Uses `VITE_API_URL` for the backend base URL. In production, this can point to the deployed API. In development, it defaults to `localhost:8001`. 8. **No client-side routing library beyond React Router:** The sidebar's `NavItem` pattern already supports `href` strings. Active state detection uses `useLocation()` from React Router.