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, * Provides data for the projects list page with filtering,
* sorting, and pagination. * 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 * @see Issue #54
*/ */
import { useQuery } from '@tanstack/react-query'; 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'; import type { ProjectStatus } from '@/components/projects/types';
// ============================================================================ // ============================================================================
@@ -19,6 +21,7 @@ import type { ProjectStatus } from '@/components/projects/types';
export interface ProjectListItem { export interface ProjectListItem {
id: string; id: string;
name: string; name: string;
slug: string;
description?: string; description?: string;
status: ProjectStatus; status: ProjectStatus;
complexity: 'low' | 'medium' | 'high'; complexity: 'low' | 'medium' | 'high';
@@ -33,6 +36,7 @@ export interface ProjectListItem {
name: string; name: string;
}; };
tags?: string[]; tags?: string[];
autonomyLevel: string;
} }
export interface ProjectsListParams { export interface ProjectsListParams {
@@ -56,134 +60,64 @@ export interface ProjectsListResponse {
} }
// ============================================================================ // ============================================================================
// Mock Data // Helpers
// ============================================================================ // ============================================================================
const mockProjects: ProjectListItem[] = [ /**
{ * Maps API ProjectResponse to our ProjectListItem format
id: 'proj-001', */
name: 'E-Commerce Platform Redesign', function mapProjectResponse(project: ProjectResponse & Record<string, unknown>): ProjectListItem {
description: // Map complexity from mock data format to UI format
'Complete redesign of the e-commerce platform with modern UI/UX and improved checkout flow', const rawComplexity = project.complexity as string;
status: 'active', let complexity: 'low' | 'medium' | 'high' = 'medium';
complexity: 'high', if (rawComplexity === 'script' || rawComplexity === 'simple' || rawComplexity === 'low') {
progress: 67, complexity = 'low';
openIssues: 12, } else if (rawComplexity === 'complex' || rawComplexity === 'high') {
activeAgents: 4, complexity = 'high';
currentSprint: 'Sprint 3', }
lastActivity: '2 minutes ago',
createdAt: '2025-11-15T10:00:00Z', return {
owner: { id: 'user-001', name: 'Felipe Cardoso' }, id: project.id,
tags: ['e-commerce', 'frontend', 'ux'], name: project.name,
}, slug: project.slug || project.name.toLowerCase().replace(/\s+/g, '-'),
{ description: project.description || undefined,
id: 'proj-002', status: project.status as ProjectStatus,
name: 'Mobile Banking App', complexity,
description: progress: (project.progress as number) || 0,
'Native mobile app for banking services with biometric authentication and real-time notifications', openIssues: (project.openIssues as number) || project.issue_count || 0,
status: 'active', activeAgents: (project.activeAgents as number) || project.agent_count || 0,
complexity: 'high', currentSprint: project.active_sprint_name || undefined,
progress: 45, lastActivity: (project.lastActivity as string) || formatRelativeTime(project.updated_at),
openIssues: 8, createdAt: project.created_at,
activeAgents: 5, owner: {
currentSprint: 'Sprint 2', id: project.owner_id || 'unknown',
lastActivity: '15 minutes ago', name: (project.ownerName as string) || 'Unknown',
createdAt: '2025-11-20T09:00:00Z', },
owner: { id: 'user-001', name: 'Felipe Cardoso' }, tags: (project.tags as string[]) || [],
tags: ['mobile', 'fintech', 'security'], autonomyLevel: project.autonomy_level || 'milestone',
}, };
{ }
id: 'proj-003',
name: 'Internal HR Portal', /**
description: * Format a date string as relative time (e.g., "2 minutes ago")
'Employee self-service portal for HR operations including leave requests and performance reviews', */
status: 'paused', function formatRelativeTime(dateStr: string): string {
complexity: 'medium', const date = new Date(dateStr);
progress: 23, const now = new Date();
openIssues: 5, const diffMs = now.getTime() - date.getTime();
activeAgents: 0, const diffMins = Math.floor(diffMs / 60000);
currentSprint: 'Sprint 1', const diffHours = Math.floor(diffMins / 60);
lastActivity: '2 days ago', const diffDays = Math.floor(diffHours / 24);
createdAt: '2025-10-01T08:00:00Z', const diffWeeks = Math.floor(diffDays / 7);
owner: { id: 'user-002', name: 'Maria Santos' }, const diffMonths = Math.floor(diffDays / 30);
tags: ['internal', 'hr', 'portal'],
}, if (diffMins < 1) return 'Just now';
{ if (diffMins < 60) return `${diffMins} minute${diffMins > 1 ? 's' : ''} ago`;
id: 'proj-004', if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
name: 'API Gateway Modernization', if (diffDays < 7) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
description: if (diffWeeks < 4) return `${diffWeeks} week${diffWeeks > 1 ? 's' : ''} ago`;
'Migrate legacy API gateway to cloud-native architecture with improved rate limiting and caching', return `${diffMonths} month${diffMonths > 1 ? 's' : ''} ago`;
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'],
},
];
// ============================================================================ // ============================================================================
// Hook // Hook
@@ -206,35 +140,33 @@ export function useProjects(params: ProjectsListParams = {}) {
return useQuery<ProjectsListResponse>({ return useQuery<ProjectsListResponse>({
queryKey: ['projects', { search, status, complexity, sortBy, sortOrder, page, limit }], queryKey: ['projects', { search, status, complexity, sortBy, sortOrder, page, limit }],
queryFn: async () => { queryFn: async () => {
// Simulate network delay // Call API via SDK (MSW intercepts in demo mode)
await new Promise((resolve) => setTimeout(resolve, 400)); const response = await listProjectsApi({
query: {
status: status !== 'all' ? (status as ApiProjectStatus) : undefined,
search: search || undefined,
page,
limit,
},
});
// Filter projects if (response.error) {
let filtered = [...mockProjects]; throw new Error('Failed to fetch projects');
// 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))
);
} }
// Status filter // Get raw response data
if (status !== 'all') { const apiData = response.data;
filtered = filtered.filter((p) => p.status === status); 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') { if (complexity !== 'all') {
filtered = filtered.filter((p) => p.complexity === complexity); projects = projects.filter((p) => p.complexity === complexity);
} }
// Sort // Client-side sorting
filtered.sort((a, b) => { projects.sort((a, b) => {
let comparison = 0; let comparison = 0;
switch (sortBy) { switch (sortBy) {
case 'name': case 'name':
@@ -254,15 +186,12 @@ export function useProjects(params: ProjectsListParams = {}) {
return sortOrder === 'asc' ? comparison : -comparison; return sortOrder === 'asc' ? comparison : -comparison;
}); });
// Pagination // Calculate pagination
const total = filtered.length; const total = apiData.pagination.total;
const totalPages = Math.ceil(total / limit); const totalPages = Math.ceil(total / limit);
const start = (page - 1) * limit;
const end = start + limit;
const paginatedData = filtered.slice(start, end);
return { return {
data: paginatedData, data: projects,
pagination: { pagination: {
page, page,
limit, 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 * 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'; import { http, HttpResponse, delay } from 'msw';
@@ -603,4 +603,544 @@ export const generatedHandlers = [
message: 'Operation successful' 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 { http, HttpResponse, delay } from 'msw';
import { generateMockToken } from '../utils/tokens'; import { generateMockToken } from '../utils/tokens';
import { validateCredentials, setCurrentUser, currentUser } from '../data/users'; import { validateCredentials, setCurrentUser, currentUser } from '../data/users';
import {
getProjects,
getProjectById,
getProjectBySlug,
createProject,
updateProject,
deleteProject,
} from '../data/projects';
import config from '@/config/app.config'; import config from '@/config/app.config';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000'; const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000';
@@ -182,4 +190,164 @@ export const overrideHandlers = [
accounts: [], 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 = { const mockProject: ProjectListItem = {
id: 'proj-1', id: 'proj-1',
name: 'Test Project', name: 'Test Project',
slug: 'test-project',
description: 'This is a test project description', description: 'This is a test project description',
status: 'active', status: 'active',
complexity: 'medium', complexity: 'medium',
@@ -21,6 +22,7 @@ describe('ProjectCard', () => {
createdAt: '2025-01-01T00:00:00Z', createdAt: '2025-01-01T00:00:00Z',
owner: { id: 'user-1', name: 'Test User' }, owner: { id: 'user-1', name: 'Test User' },
tags: ['frontend', 'react', 'typescript'], tags: ['frontend', 'react', 'typescript'],
autonomyLevel: 'milestone',
}; };
it('renders project name', () => { it('renders project name', () => {

View File

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