foundary-test-1770773605/.sdlc/features/task-management-ui/design.md
rdev-worker fd7949cb18
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
build: /design-feature task-management-ui
2026-02-11 02:25:45 +00:00

22 KiB

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:

// 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:

interface APIResponse<T> {
  data: T;
  meta?: {
    request_id: string;
    timestamp: string;
  };
}

API Integration Layer

Client Setup

// 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:

// src/api/projects.ts
export async function listProjects(): Promise<Project[]> {
  const res = await api.get<APIResponse<Project[]>>('/projects');
  return res.data;
}

export async function getProject(id: string): Promise<Project> {
  const res = await api.get<APIResponse<Project>>(`/projects/${id}`);
  return res.data;
}

export async function createProject(req: CreateProjectRequest): Promise<Project> {
  const res = await api.post<APIResponse<Project>>('/projects', req);
  return res.data;
}

export async function updateProject(id: string, req: UpdateProjectRequest): Promise<Project> {
  const res = await api.put<APIResponse<Project>>(`/projects/${id}`, req);
  return res.data;
}

export async function deleteProject(id: string): Promise<void> {
  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

// src/hooks/useProjects.ts
function useProjects() {
  const [projects, setProjects] = useState<Project[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(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<Project | null>(null);
  const [tasks, setTasks] = useState<Task[]>([]);
  const [labels, setLabels] = useState<Label[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(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
│   └── <Routes>
│       ├── /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

// Uses: Card, CardHeader, CardTitle, CardDescription, CardContent, Badge
// Props: project: Project, taskCount?: number, onClick: () => void
// Behavior: Click navigates to /projects/:id

TaskBoard

// 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

// 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

// 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

// 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

// Uses: Dialog, Button
// Props: title, message, onConfirm, onCancel
// "Are you sure?" with destructive-variant confirm button
// Reusable for projects, tasks, labels

Routing Setup

// src/App.tsx
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      <DashboardShell sidebar={<Sidebar items={navItems} />} header={...}>
        <Routes>
          <Route path="/" element={<Navigate to="/projects" replace />} />
          <Route path="/projects" element={<ProjectListPage />} />
          <Route path="/projects/:id" element={<ProjectDetailPage />} />
        </Routes>
      </DashboardShell>
    </BrowserRouter>
  );
}

The sidebar navItems will be updated:

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:

// 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:

<Alert variant="destructive">
  <AlertTitle>Error</AlertTitle>
  <AlertDescription>{error.message}</AlertDescription>
</Alert>

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:3001localhost: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.