feat(frontend): wire useProjects hook to SDK and enhance MSW handlers

- Regenerate API SDK with 77 endpoints (up from 61)
- Update useProjects hook to use SDK's listProjects function
- Add comprehensive project mock data for demo mode
- Add project CRUD handlers to MSW overrides
- Map API response to frontend ProjectListItem format
- Fix test files with required slug and autonomyLevel properties

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-03 02:22:44 +01:00
parent fe2104822e
commit 731a188a76
8 changed files with 4881 additions and 165 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -4,12 +4,14 @@
* Provides data for the projects list page with filtering,
* sorting, and pagination.
*
* Uses mock data until backend endpoints are available.
* Uses SDK to call API (intercepted by MSW in demo mode).
*
* @see Issue #54
*/
import { useQuery } from '@tanstack/react-query';
import { listProjects as listProjectsApi } from '@/lib/api/generated';
import type { ProjectStatus as ApiProjectStatus, ProjectResponse } from '@/lib/api/generated';
import type { ProjectStatus } from '@/components/projects/types';
// ============================================================================
@@ -19,6 +21,7 @@ import type { ProjectStatus } from '@/components/projects/types';
export interface ProjectListItem {
id: string;
name: string;
slug: string;
description?: string;
status: ProjectStatus;
complexity: 'low' | 'medium' | 'high';
@@ -33,6 +36,7 @@ export interface ProjectListItem {
name: string;
};
tags?: string[];
autonomyLevel: string;
}
export interface ProjectsListParams {
@@ -56,134 +60,64 @@ export interface ProjectsListResponse {
}
// ============================================================================
// Mock Data
// Helpers
// ============================================================================
const mockProjects: ProjectListItem[] = [
{
id: 'proj-001',
name: 'E-Commerce Platform Redesign',
description:
'Complete redesign of the e-commerce platform with modern UI/UX and improved checkout flow',
status: 'active',
complexity: 'high',
progress: 67,
openIssues: 12,
activeAgents: 4,
currentSprint: 'Sprint 3',
lastActivity: '2 minutes ago',
createdAt: '2025-11-15T10:00:00Z',
owner: { id: 'user-001', name: 'Felipe Cardoso' },
tags: ['e-commerce', 'frontend', 'ux'],
},
{
id: 'proj-002',
name: 'Mobile Banking App',
description:
'Native mobile app for banking services with biometric authentication and real-time notifications',
status: 'active',
complexity: 'high',
progress: 45,
openIssues: 8,
activeAgents: 5,
currentSprint: 'Sprint 2',
lastActivity: '15 minutes ago',
createdAt: '2025-11-20T09:00:00Z',
owner: { id: 'user-001', name: 'Felipe Cardoso' },
tags: ['mobile', 'fintech', 'security'],
},
{
id: 'proj-003',
name: 'Internal HR Portal',
description:
'Employee self-service portal for HR operations including leave requests and performance reviews',
status: 'paused',
complexity: 'medium',
progress: 23,
openIssues: 5,
activeAgents: 0,
currentSprint: 'Sprint 1',
lastActivity: '2 days ago',
createdAt: '2025-10-01T08:00:00Z',
owner: { id: 'user-002', name: 'Maria Santos' },
tags: ['internal', 'hr', 'portal'],
},
{
id: 'proj-004',
name: 'API Gateway Modernization',
description:
'Migrate legacy API gateway to cloud-native architecture with improved rate limiting and caching',
status: 'active',
complexity: 'high',
progress: 82,
openIssues: 3,
activeAgents: 2,
currentSprint: 'Sprint 4',
lastActivity: '1 hour ago',
createdAt: '2025-12-01T11:00:00Z',
owner: { id: 'user-001', name: 'Felipe Cardoso' },
tags: ['api', 'cloud', 'infrastructure'],
},
{
id: 'proj-005',
name: 'Customer Analytics Dashboard',
description:
'Real-time analytics dashboard for customer behavior insights with ML-powered predictions',
status: 'completed',
complexity: 'medium',
progress: 100,
openIssues: 0,
activeAgents: 0,
lastActivity: '2 weeks ago',
createdAt: '2025-09-01T10:00:00Z',
owner: { id: 'user-003', name: 'Alex Johnson' },
tags: ['analytics', 'ml', 'dashboard'],
},
{
id: 'proj-006',
name: 'DevOps Pipeline Automation',
description: 'Automate CI/CD pipelines with AI-assisted deployments and rollback capabilities',
status: 'active',
complexity: 'medium',
progress: 35,
openIssues: 6,
activeAgents: 3,
currentSprint: 'Sprint 1',
lastActivity: '30 minutes ago',
createdAt: '2025-12-10T14:00:00Z',
owner: { id: 'user-001', name: 'Felipe Cardoso' },
tags: ['devops', 'automation', 'ci-cd'],
},
{
id: 'proj-007',
name: 'Inventory Management System',
description: 'Warehouse inventory tracking with barcode scanning and automated reordering',
status: 'archived',
complexity: 'low',
progress: 100,
openIssues: 0,
activeAgents: 0,
lastActivity: '1 month ago',
createdAt: '2025-06-15T08:00:00Z',
owner: { id: 'user-002', name: 'Maria Santos' },
tags: ['inventory', 'warehouse', 'logistics'],
},
{
id: 'proj-008',
name: 'Customer Support Chatbot',
description: 'AI-powered chatbot for 24/7 customer support with sentiment analysis',
status: 'active',
complexity: 'medium',
progress: 58,
openIssues: 4,
activeAgents: 2,
currentSprint: 'Sprint 2',
lastActivity: '45 minutes ago',
createdAt: '2025-12-05T09:00:00Z',
owner: { id: 'user-003', name: 'Alex Johnson' },
tags: ['ai', 'chatbot', 'support'],
},
];
/**
* Maps API ProjectResponse to our ProjectListItem format
*/
function mapProjectResponse(project: ProjectResponse & Record<string, unknown>): ProjectListItem {
// Map complexity from mock data format to UI format
const rawComplexity = project.complexity as string;
let complexity: 'low' | 'medium' | 'high' = 'medium';
if (rawComplexity === 'script' || rawComplexity === 'simple' || rawComplexity === 'low') {
complexity = 'low';
} else if (rawComplexity === 'complex' || rawComplexity === 'high') {
complexity = 'high';
}
return {
id: project.id,
name: project.name,
slug: project.slug || project.name.toLowerCase().replace(/\s+/g, '-'),
description: project.description || undefined,
status: project.status as ProjectStatus,
complexity,
progress: (project.progress as number) || 0,
openIssues: (project.openIssues as number) || project.issue_count || 0,
activeAgents: (project.activeAgents as number) || project.agent_count || 0,
currentSprint: project.active_sprint_name || undefined,
lastActivity: (project.lastActivity as string) || formatRelativeTime(project.updated_at),
createdAt: project.created_at,
owner: {
id: project.owner_id || 'unknown',
name: (project.ownerName as string) || 'Unknown',
},
tags: (project.tags as string[]) || [],
autonomyLevel: project.autonomy_level || 'milestone',
};
}
/**
* Format a date string as relative time (e.g., "2 minutes ago")
*/
function formatRelativeTime(dateStr: string): string {
const date = new Date(dateStr);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
const diffWeeks = Math.floor(diffDays / 7);
const diffMonths = Math.floor(diffDays / 30);
if (diffMins < 1) return 'Just now';
if (diffMins < 60) return `${diffMins} minute${diffMins > 1 ? 's' : ''} ago`;
if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
if (diffDays < 7) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
if (diffWeeks < 4) return `${diffWeeks} week${diffWeeks > 1 ? 's' : ''} ago`;
return `${diffMonths} month${diffMonths > 1 ? 's' : ''} ago`;
}
// ============================================================================
// Hook
@@ -206,35 +140,33 @@ export function useProjects(params: ProjectsListParams = {}) {
return useQuery<ProjectsListResponse>({
queryKey: ['projects', { search, status, complexity, sortBy, sortOrder, page, limit }],
queryFn: async () => {
// Simulate network delay
await new Promise((resolve) => setTimeout(resolve, 400));
// Call API via SDK (MSW intercepts in demo mode)
const response = await listProjectsApi({
query: {
status: status !== 'all' ? (status as ApiProjectStatus) : undefined,
search: search || undefined,
page,
limit,
},
});
// Filter projects
let filtered = [...mockProjects];
// Search filter
if (search) {
const searchLower = search.toLowerCase();
filtered = filtered.filter(
(p) =>
p.name.toLowerCase().includes(searchLower) ||
p.description?.toLowerCase().includes(searchLower) ||
p.tags?.some((t) => t.toLowerCase().includes(searchLower))
);
if (response.error) {
throw new Error('Failed to fetch projects');
}
// Status filter
if (status !== 'all') {
filtered = filtered.filter((p) => p.status === status);
}
// Get raw response data
const apiData = response.data;
let projects = apiData.data.map((p) =>
mapProjectResponse(p as ProjectResponse & Record<string, unknown>)
);
// Complexity filter
// Client-side complexity filter (not supported by API)
if (complexity !== 'all') {
filtered = filtered.filter((p) => p.complexity === complexity);
projects = projects.filter((p) => p.complexity === complexity);
}
// Sort
filtered.sort((a, b) => {
// Client-side sorting
projects.sort((a, b) => {
let comparison = 0;
switch (sortBy) {
case 'name':
@@ -254,15 +186,12 @@ export function useProjects(params: ProjectsListParams = {}) {
return sortOrder === 'asc' ? comparison : -comparison;
});
// Pagination
const total = filtered.length;
// Calculate pagination
const total = apiData.pagination.total;
const totalPages = Math.ceil(total / limit);
const start = (page - 1) * limit;
const end = start + limit;
const paginatedData = filtered.slice(start, end);
return {
data: paginatedData,
data: projects,
pagination: {
page,
limit,

View File

@@ -0,0 +1,266 @@
/**
* Mock Project Data for Demo Mode
*
* Sample projects used by MSW handlers in demo mode.
*/
import type { ProjectResponse, ProjectStatus } from '@/lib/api/generated';
export interface ProjectListItem extends ProjectResponse {
// Extended UI fields (computed/stored separately in real app)
complexity?: 'script' | 'simple' | 'medium' | 'complex';
progress?: number;
openIssues?: number;
activeAgents?: number;
lastActivity?: string;
tags?: string[];
ownerName?: string;
}
export const sampleProjects: ProjectListItem[] = [
{
id: 'proj-001',
name: 'E-Commerce Platform Redesign',
slug: 'ecommerce-redesign',
description: 'Complete redesign of the e-commerce platform with modern UI/UX',
autonomy_level: 'milestone',
status: 'active',
settings: {},
owner_id: 'user-001',
created_at: '2025-11-15T10:00:00Z',
updated_at: new Date(Date.now() - 2 * 60 * 1000).toISOString(),
agent_count: 5,
issue_count: 70,
active_sprint_name: 'Sprint 3',
// Extended fields
complexity: 'complex',
progress: 67,
openIssues: 12,
activeAgents: 4,
lastActivity: '2 minutes ago',
tags: ['e-commerce', 'frontend', 'ux'],
ownerName: 'Felipe Cardoso',
},
{
id: 'proj-002',
name: 'Mobile Banking App',
slug: 'mobile-banking',
description: 'Native mobile app for banking services with biometric authentication',
autonomy_level: 'full_control',
status: 'active',
settings: {},
owner_id: 'user-001',
created_at: '2025-11-20T09:00:00Z',
updated_at: new Date(Date.now() - 15 * 60 * 1000).toISOString(),
agent_count: 5,
issue_count: 45,
active_sprint_name: 'Sprint 2',
complexity: 'complex',
progress: 45,
openIssues: 8,
activeAgents: 5,
lastActivity: '15 minutes ago',
tags: ['mobile', 'fintech', 'security'],
ownerName: 'Felipe Cardoso',
},
{
id: 'proj-003',
name: 'Internal HR Portal',
slug: 'hr-portal',
description: 'Employee self-service portal for HR operations',
autonomy_level: 'milestone',
status: 'paused',
settings: {},
owner_id: 'user-002',
created_at: '2025-10-01T08:00:00Z',
updated_at: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
agent_count: 3,
issue_count: 25,
active_sprint_name: 'Sprint 1',
complexity: 'medium',
progress: 23,
openIssues: 5,
activeAgents: 0,
lastActivity: '2 days ago',
tags: ['internal', 'hr', 'portal'],
ownerName: 'Maria Santos',
},
{
id: 'proj-004',
name: 'API Gateway Modernization',
slug: 'api-gateway',
description: 'Migrate legacy API gateway to cloud-native architecture',
autonomy_level: 'autonomous',
status: 'active',
settings: {},
owner_id: 'user-001',
created_at: '2025-12-01T11:00:00Z',
updated_at: new Date(Date.now() - 60 * 60 * 1000).toISOString(),
agent_count: 3,
issue_count: 40,
active_sprint_name: 'Sprint 4',
complexity: 'complex',
progress: 82,
openIssues: 3,
activeAgents: 2,
lastActivity: '1 hour ago',
tags: ['api', 'cloud', 'infrastructure'],
ownerName: 'Felipe Cardoso',
},
{
id: 'proj-005',
name: 'Customer Analytics Dashboard',
slug: 'analytics-dashboard',
description: 'Real-time analytics dashboard for customer behavior insights',
autonomy_level: 'milestone',
status: 'completed',
settings: {},
owner_id: 'user-003',
created_at: '2025-09-01T10:00:00Z',
updated_at: new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString(),
agent_count: 0,
issue_count: 50,
active_sprint_name: null,
complexity: 'medium',
progress: 100,
openIssues: 0,
activeAgents: 0,
lastActivity: '2 weeks ago',
tags: ['analytics', 'ml', 'dashboard'],
ownerName: 'Alex Johnson',
},
{
id: 'proj-006',
name: 'DevOps Pipeline Automation',
slug: 'devops-automation',
description: 'Automate CI/CD pipelines with AI-assisted deployments',
autonomy_level: 'milestone',
status: 'active',
settings: {},
owner_id: 'user-001',
created_at: '2025-12-10T14:00:00Z',
updated_at: new Date(Date.now() - 30 * 60 * 1000).toISOString(),
agent_count: 4,
issue_count: 30,
active_sprint_name: 'Sprint 1',
complexity: 'medium',
progress: 35,
openIssues: 6,
activeAgents: 3,
lastActivity: '30 minutes ago',
tags: ['devops', 'automation', 'ci-cd'],
ownerName: 'Felipe Cardoso',
},
{
id: 'proj-007',
name: 'Inventory Management System',
slug: 'inventory-system',
description: 'Warehouse inventory tracking with barcode scanning',
autonomy_level: 'full_control',
status: 'archived',
settings: {},
owner_id: 'user-002',
created_at: '2025-06-15T08:00:00Z',
updated_at: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
agent_count: 0,
issue_count: 80,
active_sprint_name: null,
complexity: 'simple',
progress: 100,
openIssues: 0,
activeAgents: 0,
lastActivity: '1 month ago',
tags: ['inventory', 'warehouse', 'logistics'],
ownerName: 'Maria Santos',
},
{
id: 'proj-008',
name: 'Customer Support Chatbot',
slug: 'support-chatbot',
description: 'AI-powered chatbot for 24/7 customer support',
autonomy_level: 'autonomous',
status: 'active',
settings: {},
owner_id: 'user-003',
created_at: '2025-12-05T09:00:00Z',
updated_at: new Date(Date.now() - 45 * 60 * 1000).toISOString(),
agent_count: 3,
issue_count: 35,
active_sprint_name: 'Sprint 2',
complexity: 'medium',
progress: 58,
openIssues: 4,
activeAgents: 2,
lastActivity: '45 minutes ago',
tags: ['ai', 'chatbot', 'support'],
ownerName: 'Alex Johnson',
},
];
// In-memory store for demo mode (allows create/update/delete)
let projectsStore = [...sampleProjects];
export function getProjects(): ProjectListItem[] {
return projectsStore;
}
export function getProjectById(id: string): ProjectListItem | undefined {
return projectsStore.find((p) => p.id === id);
}
export function getProjectBySlug(slug: string): ProjectListItem | undefined {
return projectsStore.find((p) => p.slug === slug);
}
export function createProject(data: Partial<ProjectListItem>): ProjectListItem {
const newProject: ProjectListItem = {
id: `proj-${Date.now()}`,
name: data.name || 'New Project',
slug: data.slug || data.name?.toLowerCase().replace(/\s+/g, '-') || `project-${Date.now()}`,
description: data.description || null,
autonomy_level: data.autonomy_level || 'milestone',
status: 'active',
settings: data.settings || {},
owner_id: data.owner_id || 'user-001',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
agent_count: 0,
issue_count: 0,
active_sprint_name: null,
complexity: data.complexity || 'medium',
progress: 0,
openIssues: 0,
activeAgents: 0,
lastActivity: 'Just now',
tags: data.tags || [],
ownerName: 'Demo User',
};
projectsStore.unshift(newProject);
return newProject;
}
export function updateProject(
id: string,
data: Partial<ProjectListItem>
): ProjectListItem | undefined {
const index = projectsStore.findIndex((p) => p.id === id);
if (index === -1) return undefined;
projectsStore[index] = {
...projectsStore[index],
...data,
updated_at: new Date().toISOString(),
};
return projectsStore[index];
}
export function deleteProject(id: string): boolean {
const index = projectsStore.findIndex((p) => p.id === id);
if (index === -1) return false;
projectsStore.splice(index, 1);
return true;
}
export function resetProjects(): void {
projectsStore = [...sampleProjects];
}

View File

@@ -8,7 +8,7 @@
*
* For custom handler behavior, use src/mocks/handlers/overrides.ts
*
* Generated: 2025-12-30T02:14:59.598Z
* Generated: 2026-01-03T01:13:34.961Z
*/
import { http, HttpResponse, delay } from 'msw';
@@ -603,4 +603,544 @@ export const generatedHandlers = [
message: 'Operation successful'
});
}),
/**
* Create Project
*/
http.post(`${API_BASE_URL}/api/v1/projects`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* List Projects
*/
http.get(`${API_BASE_URL}/api/v1/projects`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Project
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Update Project
*/
http.patch(`${API_BASE_URL}/api/v1/projects/:project_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Archive Project
*/
http.delete(`${API_BASE_URL}/api/v1/projects/:project_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Project by Slug
*/
http.get(`${API_BASE_URL}/api/v1/projects/slug/:slug`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Pause Project
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/pause`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Resume Project
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/resume`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Create Agent Type
*/
http.post(`${API_BASE_URL}/api/v1/agent-types`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* List Agent Types
*/
http.get(`${API_BASE_URL}/api/v1/agent-types`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Update Agent Type
*/
http.patch(`${API_BASE_URL}/api/v1/agent-types/:agent_type_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Deactivate Agent Type
*/
http.delete(`${API_BASE_URL}/api/v1/agent-types/:agent_type_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Agent Type
*/
http.get(`${API_BASE_URL}/api/v1/agent-types/:agent_type_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Agent Type by Slug
*/
http.get(`${API_BASE_URL}/api/v1/agent-types/slug/:slug`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Create Issue
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/issues`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* List Issues
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/issues`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Issue Statistics
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/issues/stats`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Issue
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/issues/:issue_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Update Issue
*/
http.patch(`${API_BASE_URL}/api/v1/projects/:project_id/issues/:issue_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Delete Issue
*/
http.delete(`${API_BASE_URL}/api/v1/projects/:project_id/issues/:issue_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Assign Issue
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/issues/:issue_id/assign`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Unassign Issue
*/
http.delete(`${API_BASE_URL}/api/v1/projects/:project_id/issues/:issue_id/assignment`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Trigger Issue Sync
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/issues/:issue_id/sync`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Spawn Agent Instance
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/agents`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* List Project Agents
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/agents`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Project Agent Metrics
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/agents/metrics`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Agent Details
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/agents/:agent_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Update Agent
*/
http.patch(`${API_BASE_URL}/api/v1/projects/:project_id/agents/:agent_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Terminate Agent
*/
http.delete(`${API_BASE_URL}/api/v1/projects/:project_id/agents/:agent_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Pause Agent
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/agents/:agent_id/pause`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Resume Agent
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/agents/:agent_id/resume`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Agent Metrics
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/agents/:agent_id/metrics`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Create Sprint
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/sprints`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* List Sprints
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/sprints`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Active Sprint
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/sprints/active`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Project Velocity
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/sprints/velocity`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Sprint Details
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/sprints/:sprint_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Update Sprint
*/
http.patch(`${API_BASE_URL}/api/v1/projects/:project_id/sprints/:sprint_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Delete Sprint
*/
http.delete(`${API_BASE_URL}/api/v1/projects/:project_id/sprints/:sprint_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Start Sprint
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/sprints/:sprint_id/start`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Complete Sprint
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/sprints/:sprint_id/complete`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Cancel Sprint
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/sprints/:sprint_id/cancel`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Sprint Issues
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/sprints/:sprint_id/issues`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Add Issue to Sprint
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/sprints/:sprint_id/issues`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Remove Issue from Sprint
*/
http.delete(`${API_BASE_URL}/api/v1/projects/:project_id/sprints/:sprint_id/issues`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
];

View File

@@ -14,6 +14,14 @@
import { http, HttpResponse, delay } from 'msw';
import { generateMockToken } from '../utils/tokens';
import { validateCredentials, setCurrentUser, currentUser } from '../data/users';
import {
getProjects,
getProjectById,
getProjectBySlug,
createProject,
updateProject,
deleteProject,
} from '../data/projects';
import config from '@/config/app.config';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000';
@@ -182,4 +190,164 @@ export const overrideHandlers = [
accounts: [],
});
}),
// ============================================
// PROJECT HANDLERS
// ============================================
/**
* List Projects
* Returns paginated list of projects with filtering support
*/
http.get(`${API_BASE_URL}/api/v1/projects`, async ({ request }) => {
await delay(NETWORK_DELAY);
const url = new URL(request.url);
const status = url.searchParams.get('status');
const search = url.searchParams.get('search');
const skip = parseInt(url.searchParams.get('skip') || '0');
const limit = parseInt(url.searchParams.get('limit') || '20');
let projects = getProjects();
// Filter by status
if (status && status !== 'all') {
projects = projects.filter((p) => p.status === status);
}
// Filter by search term
if (search) {
const searchLower = search.toLowerCase();
projects = projects.filter(
(p) =>
p.name.toLowerCase().includes(searchLower) ||
p.description?.toLowerCase().includes(searchLower)
);
}
const total = projects.length;
const paginatedProjects = projects.slice(skip, skip + limit);
const totalPages = Math.ceil(total / limit);
return HttpResponse.json({
data: paginatedProjects,
pagination: {
total,
page: Math.floor(skip / limit) + 1,
limit,
total_pages: totalPages,
has_next: skip + limit < total,
has_prev: skip > 0,
},
});
}),
/**
* Create Project
*/
http.post(`${API_BASE_URL}/api/v1/projects`, async ({ request }) => {
await delay(NETWORK_DELAY);
const body = (await request.json()) as any;
const newProject = createProject(body);
return HttpResponse.json(newProject, { status: 201 });
}),
/**
* Get Project by ID
*/
http.get(`${API_BASE_URL}/api/v1/projects/:projectId`, async ({ params }) => {
await delay(NETWORK_DELAY);
const { projectId } = params;
const project = getProjectById(projectId as string);
if (!project) {
return HttpResponse.json({ detail: 'Project not found' }, { status: 404 });
}
return HttpResponse.json(project);
}),
/**
* Get Project by Slug
*/
http.get(`${API_BASE_URL}/api/v1/projects/slug/:slug`, async ({ params }) => {
await delay(NETWORK_DELAY);
const { slug } = params;
const project = getProjectBySlug(slug as string);
if (!project) {
return HttpResponse.json({ detail: 'Project not found' }, { status: 404 });
}
return HttpResponse.json(project);
}),
/**
* Update Project
*/
http.patch(`${API_BASE_URL}/api/v1/projects/:projectId`, async ({ params, request }) => {
await delay(NETWORK_DELAY);
const { projectId } = params;
const body = (await request.json()) as any;
const updated = updateProject(projectId as string, body);
if (!updated) {
return HttpResponse.json({ detail: 'Project not found' }, { status: 404 });
}
return HttpResponse.json(updated);
}),
/**
* Archive (Delete) Project
*/
http.delete(`${API_BASE_URL}/api/v1/projects/:projectId`, async ({ params }) => {
await delay(NETWORK_DELAY);
const { projectId } = params;
const deleted = deleteProject(projectId as string);
if (!deleted) {
return HttpResponse.json({ detail: 'Project not found' }, { status: 404 });
}
return HttpResponse.json({ message: 'Project archived successfully' });
}),
/**
* Pause Project
*/
http.post(`${API_BASE_URL}/api/v1/projects/:projectId/pause`, async ({ params }) => {
await delay(NETWORK_DELAY);
const { projectId } = params;
const updated = updateProject(projectId as string, { status: 'paused' as any });
if (!updated) {
return HttpResponse.json({ detail: 'Project not found' }, { status: 404 });
}
return HttpResponse.json(updated);
}),
/**
* Resume Project
*/
http.post(`${API_BASE_URL}/api/v1/projects/:projectId/resume`, async ({ params }) => {
await delay(NETWORK_DELAY);
const { projectId } = params;
const updated = updateProject(projectId as string, { status: 'active' as any });
if (!updated) {
return HttpResponse.json({ detail: 'Project not found' }, { status: 404 });
}
return HttpResponse.json(updated);
}),
];

View File

@@ -10,6 +10,7 @@ describe('ProjectCard', () => {
const mockProject: ProjectListItem = {
id: 'proj-1',
name: 'Test Project',
slug: 'test-project',
description: 'This is a test project description',
status: 'active',
complexity: 'medium',
@@ -21,6 +22,7 @@ describe('ProjectCard', () => {
createdAt: '2025-01-01T00:00:00Z',
owner: { id: 'user-1', name: 'Test User' },
tags: ['frontend', 'react', 'typescript'],
autonomyLevel: 'milestone',
};
it('renders project name', () => {

View File

@@ -18,6 +18,7 @@ describe('ProjectsGrid', () => {
{
id: 'proj-1',
name: 'Project One',
slug: 'project-one',
description: 'First project',
status: 'active',
complexity: 'medium',
@@ -27,10 +28,12 @@ describe('ProjectsGrid', () => {
lastActivity: '5 min ago',
createdAt: '2025-01-01T00:00:00Z',
owner: { id: 'user-1', name: 'User One' },
autonomyLevel: 'milestone',
},
{
id: 'proj-2',
name: 'Project Two',
slug: 'project-two',
description: 'Second project',
status: 'paused',
complexity: 'high',
@@ -40,6 +43,7 @@ describe('ProjectsGrid', () => {
lastActivity: '1 day ago',
createdAt: '2025-01-02T00:00:00Z',
owner: { id: 'user-2', name: 'User Two' },
autonomyLevel: 'full_control',
},
];