forked from cardosofelipe/fast-next-template
refactor(frontend): clean up code by consolidating multi-line JSX into single lines where feasible
- Refactored JSX elements to improve readability by collapsing multi-line props and attributes into single lines if their length permits. - Improved consistency in component imports by grouping and consolidating them. - No functional changes, purely restructuring for clarity and maintainability.
This commit is contained in:
@@ -199,13 +199,10 @@ export default function ActivityFeedPage() {
|
||||
<BellOff className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={reconnect}
|
||||
aria-label="Refresh connection"
|
||||
>
|
||||
<RefreshCw className={connectionState === 'connecting' ? 'h-4 w-4 animate-spin' : 'h-4 w-4'} />
|
||||
<Button variant="ghost" size="icon" onClick={reconnect} aria-label="Refresh connection">
|
||||
<RefreshCw
|
||||
className={connectionState === 'connecting' ? 'h-4 w-4 animate-spin' : 'h-4 w-4'}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -214,7 +211,8 @@ export default function ActivityFeedPage() {
|
||||
{(!isConnected || sseEvents.length === 0) && (
|
||||
<div className="rounded-lg border border-yellow-200 bg-yellow-50 p-4 dark:border-yellow-800 dark:bg-yellow-950">
|
||||
<p className="text-sm text-yellow-800 dark:text-yellow-200">
|
||||
<strong>Demo Mode:</strong> Showing sample events. Connect to a real project to see live updates.
|
||||
<strong>Demo Mode:</strong> Showing sample events. Connect to a real project to see
|
||||
live updates.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -32,11 +32,7 @@ export default function AgentTypeDetailPage() {
|
||||
const [viewMode, setViewMode] = useState<ViewMode>(isNew ? 'create' : 'detail');
|
||||
|
||||
// Fetch agent type data (skip if creating new)
|
||||
const {
|
||||
data: agentType,
|
||||
isLoading,
|
||||
error,
|
||||
} = useAgentType(isNew ? null : id);
|
||||
const { data: agentType, isLoading, error } = useAgentType(isNew ? null : id);
|
||||
|
||||
// Mutations
|
||||
const createMutation = useCreateAgentType();
|
||||
@@ -171,7 +167,7 @@ export default function AgentTypeDetailPage() {
|
||||
<div className="container mx-auto px-4 py-6">
|
||||
{(viewMode === 'create' || viewMode === 'edit') && (
|
||||
<AgentTypeForm
|
||||
agentType={viewMode === 'edit' ? agentType ?? undefined : undefined}
|
||||
agentType={viewMode === 'edit' ? (agentType ?? undefined) : undefined}
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={handleCancel}
|
||||
isSubmitting={createMutation.isPending || updateMutation.isPending}
|
||||
|
||||
@@ -10,13 +10,7 @@
|
||||
|
||||
import { use } from 'react';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
ArrowLeft,
|
||||
Calendar,
|
||||
Clock,
|
||||
ExternalLink,
|
||||
Edit,
|
||||
} from 'lucide-react';
|
||||
import { ArrowLeft, Calendar, Clock, ExternalLink, Edit } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
@@ -65,10 +59,7 @@ export default function IssueDetailPage({ params }: IssueDetailPageProps) {
|
||||
<Link href={`/${locale}/projects/${projectId}/issues`}>
|
||||
<Button variant="outline">Back to Issues</Button>
|
||||
</Link>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => window.location.reload()}
|
||||
>
|
||||
<Button variant="outline" onClick={() => window.location.reload()}>
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
@@ -171,9 +162,7 @@ export default function IssueDetailPage({ params }: IssueDetailPageProps) {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="prose prose-sm max-w-none dark:prose-invert">
|
||||
<pre className="whitespace-pre-wrap font-sans text-sm">
|
||||
{issue.description}
|
||||
</pre>
|
||||
<pre className="whitespace-pre-wrap font-sans text-sm">{issue.description}</pre>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -14,12 +14,7 @@ import { useRouter } from 'next/navigation';
|
||||
import { Plus, Upload } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import {
|
||||
IssueFilters,
|
||||
IssueTable,
|
||||
BulkActions,
|
||||
useIssues,
|
||||
} from '@/features/issues';
|
||||
import { IssueFilters, IssueTable, BulkActions, useIssues } from '@/features/issues';
|
||||
import type { IssueFiltersType, IssueSort } from '@/features/issues';
|
||||
|
||||
interface ProjectIssuesPageProps {
|
||||
@@ -95,11 +90,7 @@ export default function ProjectIssuesPage({ params }: ProjectIssuesPageProps) {
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Failed to load issues. Please try again later.
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="mt-4"
|
||||
onClick={() => window.location.reload()}
|
||||
>
|
||||
<Button variant="outline" className="mt-4" onClick={() => window.location.reload()}>
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
@@ -169,26 +160,15 @@ export default function ProjectIssuesPage({ params }: ProjectIssuesPageProps) {
|
||||
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
||||
<span>
|
||||
Showing {(data.pagination.page - 1) * data.pagination.page_size + 1} to{' '}
|
||||
{Math.min(
|
||||
data.pagination.page * data.pagination.page_size,
|
||||
data.pagination.total
|
||||
)}{' '}
|
||||
of {data.pagination.total} issues
|
||||
{Math.min(data.pagination.page * data.pagination.page_size, data.pagination.total)} of{' '}
|
||||
{data.pagination.total} issues
|
||||
</span>
|
||||
{data.pagination.total_pages > 1 && (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={!data.pagination.has_prev}
|
||||
>
|
||||
<Button variant="outline" size="sm" disabled={!data.pagination.has_prev}>
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={!data.pagination.has_next}
|
||||
>
|
||||
<Button variant="outline" size="sm" disabled={!data.pagination.has_next}>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -14,44 +14,52 @@ This document contains the comments to be added to each Gitea issue for the desi
|
||||
The Project Dashboard prototype has been created and is ready for review.
|
||||
|
||||
### How to View
|
||||
|
||||
1. Start the frontend dev server: `cd frontend && npm run dev`
|
||||
2. Navigate to: `http://localhost:3000/en/prototypes/project-dashboard`
|
||||
|
||||
### What's Included
|
||||
|
||||
**Header Section**
|
||||
|
||||
- Project name with status badge (In Progress, Completed, Paused, Blocked)
|
||||
- Autonomy level indicator (Full Control, Milestone, Autonomous)
|
||||
- Quick action buttons (Pause Project, Run Sprint)
|
||||
|
||||
**Agent Panel**
|
||||
|
||||
- List of all project agents with avatars
|
||||
- Real-time status indicators (active = green, idle = yellow, pending = gray)
|
||||
- Current task description for each agent
|
||||
- Last activity timestamp
|
||||
|
||||
**Sprint Overview**
|
||||
|
||||
- Current sprint progress bar
|
||||
- Issue statistics grid (Completed, In Progress, Blocked, To Do)
|
||||
- Visual burndown chart with ideal vs actual lines
|
||||
- Sprint selector dropdown
|
||||
|
||||
**Issue Summary Sidebar**
|
||||
|
||||
- Count of issues by status with color-coded icons
|
||||
- Quick links to view all issues
|
||||
|
||||
**Recent Activity Feed**
|
||||
|
||||
- Chronological event list with type icons
|
||||
- Agent attribution
|
||||
- Highlighted approval requests with action buttons
|
||||
|
||||
### Key Design Decisions
|
||||
|
||||
- Three-column layout on desktop (2/3 main, 1/3 sidebar)
|
||||
- Agent status uses traffic light colors for intuitive understanding
|
||||
- Burndown chart is simplified for quick scanning
|
||||
- Activity feed limited to 5 items with "View All" link
|
||||
|
||||
### Questions for Review
|
||||
|
||||
1. Is the burndown chart detailed enough?
|
||||
2. Should agent cards be expandable for more details?
|
||||
3. Is the 5-item activity feed sufficient?
|
||||
@@ -59,6 +67,7 @@ The Project Dashboard prototype has been created and is ready for review.
|
||||
**Please review and approve or provide feedback.**
|
||||
|
||||
Files:
|
||||
|
||||
- `/frontend/src/app/[locale]/prototypes/project-dashboard/page.tsx`
|
||||
- `/frontend/src/app/[locale]/prototypes/project-dashboard/README.md`
|
||||
```
|
||||
@@ -75,6 +84,7 @@ Files:
|
||||
The Agent Configuration UI prototype has been created and is ready for review.
|
||||
|
||||
### How to View
|
||||
|
||||
1. Start the frontend dev server: `cd frontend && npm run dev`
|
||||
2. Navigate to: `http://localhost:3000/en/prototypes/agent-configuration`
|
||||
|
||||
@@ -102,18 +112,21 @@ The Agent Configuration UI prototype has been created and is ready for review.
|
||||
- **Personality Tab**: Large textarea for personality prompt
|
||||
|
||||
### Key Design Decisions
|
||||
|
||||
- Separate views for browsing, viewing, and editing
|
||||
- Tabbed editor reduces cognitive load
|
||||
- MCP permissions show nested scopes when enabled
|
||||
- Model parameters have helpful descriptions
|
||||
|
||||
### User Flows to Test
|
||||
|
||||
1. Click any card to see detail view
|
||||
2. Click "Edit" to see editor view
|
||||
3. Click "Create Agent Type" for blank editor
|
||||
4. Navigate tabs in editor
|
||||
|
||||
### Questions for Review
|
||||
|
||||
1. Is the tabbed editor the right approach?
|
||||
2. Should expertise be free-form tags or predefined list?
|
||||
3. Should model parameters have "presets"?
|
||||
@@ -121,6 +134,7 @@ The Agent Configuration UI prototype has been created and is ready for review.
|
||||
**Please review and approve or provide feedback.**
|
||||
|
||||
Files:
|
||||
|
||||
- `/frontend/src/app/[locale]/prototypes/agent-configuration/page.tsx`
|
||||
- `/frontend/src/app/[locale]/prototypes/agent-configuration/README.md`
|
||||
```
|
||||
@@ -137,12 +151,14 @@ Files:
|
||||
The Issue List and Detail Views prototype has been created and is ready for review.
|
||||
|
||||
### How to View
|
||||
|
||||
1. Start the frontend dev server: `cd frontend && npm run dev`
|
||||
2. Navigate to: `http://localhost:3000/en/prototypes/issue-management`
|
||||
|
||||
### What's Included
|
||||
|
||||
**List View**
|
||||
|
||||
- Filterable table with sortable columns
|
||||
- Quick status filter + expandable advanced filters
|
||||
- Bulk action bar (appears when selecting issues)
|
||||
@@ -150,6 +166,7 @@ The Issue List and Detail Views prototype has been created and is ready for revi
|
||||
- Labels displayed as badges
|
||||
|
||||
**Filter Options**
|
||||
|
||||
- Status: Open, In Progress, In Review, Blocked, Done
|
||||
- Priority: High, Medium, Low
|
||||
- Sprint: Current sprints, Backlog
|
||||
@@ -157,6 +174,7 @@ The Issue List and Detail Views prototype has been created and is ready for revi
|
||||
- Labels: Feature, Bug, Backend, Frontend, etc.
|
||||
|
||||
**Detail View**
|
||||
|
||||
- Full issue content (markdown-like display)
|
||||
- Status workflow panel (click to change status)
|
||||
- Assignment panel with agent avatar
|
||||
@@ -168,12 +186,14 @@ The Issue List and Detail Views prototype has been created and is ready for revi
|
||||
- Development section (branch, PR link)
|
||||
|
||||
### Key Design Decisions
|
||||
|
||||
- Table layout for density and scannability
|
||||
- Status workflow matches common issue tracker patterns
|
||||
- Sync status indicator shows data freshness
|
||||
- Activity timeline shows issue history
|
||||
|
||||
### User Flows to Test
|
||||
|
||||
1. Use search and filters
|
||||
2. Click checkboxes to see bulk actions
|
||||
3. Sort by clicking column headers
|
||||
@@ -181,6 +201,7 @@ The Issue List and Detail Views prototype has been created and is ready for revi
|
||||
5. Click status buttons in detail view
|
||||
|
||||
### Questions for Review
|
||||
|
||||
1. Should we add Kanban view as alternative?
|
||||
2. Is the sync indicator clear enough?
|
||||
3. Should there be inline editing?
|
||||
@@ -188,6 +209,7 @@ The Issue List and Detail Views prototype has been created and is ready for revi
|
||||
**Please review and approve or provide feedback.**
|
||||
|
||||
Files:
|
||||
|
||||
- `/frontend/src/app/[locale]/prototypes/issue-management/page.tsx`
|
||||
- `/frontend/src/app/[locale]/prototypes/issue-management/README.md`
|
||||
```
|
||||
@@ -204,12 +226,14 @@ Files:
|
||||
The Real-time Activity Feed prototype has been created and is ready for review.
|
||||
|
||||
### How to View
|
||||
|
||||
1. Start the frontend dev server: `cd frontend && npm run dev`
|
||||
2. Navigate to: `http://localhost:3000/en/prototypes/activity-feed`
|
||||
|
||||
### What's Included
|
||||
|
||||
**Event Types Displayed**
|
||||
|
||||
- Agent Status: Started, paused, resumed, stopped
|
||||
- Agent Message: Updates, questions, progress reports
|
||||
- Issue Update: Status changes, assignments, creation
|
||||
@@ -219,6 +243,7 @@ The Real-time Activity Feed prototype has been created and is ready for review.
|
||||
- Milestone: Goals achieved, completions
|
||||
|
||||
**Features**
|
||||
|
||||
- Real-time connection indicator (pulsing green when connected)
|
||||
- Time-based event grouping (New, Earlier Today, Yesterday, etc.)
|
||||
- Search functionality
|
||||
@@ -231,6 +256,7 @@ The Real-time Activity Feed prototype has been created and is ready for review.
|
||||
- Mark all read functionality
|
||||
|
||||
### Key Design Decisions
|
||||
|
||||
- Card-based layout for clear event separation
|
||||
- Orange left border highlights action-required items
|
||||
- Time grouping helps users orient in timeline
|
||||
@@ -238,6 +264,7 @@ The Real-time Activity Feed prototype has been created and is ready for review.
|
||||
- Real-time indicator builds trust in data freshness
|
||||
|
||||
### User Flows to Test
|
||||
|
||||
1. Scroll through the event feed
|
||||
2. Click events to expand details
|
||||
3. Open filter panel and select filters
|
||||
@@ -245,6 +272,7 @@ The Real-time Activity Feed prototype has been created and is ready for review.
|
||||
5. Click "Mark all read"
|
||||
|
||||
### Questions for Review
|
||||
|
||||
1. Should events be grouped by time or show flat?
|
||||
2. Should there be sound notifications for urgent items?
|
||||
3. Should users be able to "star" events?
|
||||
@@ -252,6 +280,7 @@ The Real-time Activity Feed prototype has been created and is ready for review.
|
||||
**Please review and approve or provide feedback.**
|
||||
|
||||
Files:
|
||||
|
||||
- `/frontend/src/app/[locale]/prototypes/activity-feed/page.tsx`
|
||||
- `/frontend/src/app/[locale]/prototypes/activity-feed/README.md`
|
||||
```
|
||||
@@ -269,6 +298,7 @@ The comments above should be added to the respective Gitea issues at:
|
||||
- Issue #39: Real-time Activity Feed
|
||||
|
||||
After the user reviews each prototype and provides feedback:
|
||||
|
||||
1. Iterate on the design based on feedback
|
||||
2. Get explicit approval
|
||||
3. Begin implementation
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
|
||||
@@ -248,12 +248,47 @@ const EVENT_TYPE_CONFIG: Record<
|
||||
};
|
||||
|
||||
const FILTER_CATEGORIES = [
|
||||
{ id: 'agent', label: 'Agent Actions', types: [EventType.AGENT_SPAWNED, EventType.AGENT_MESSAGE, EventType.AGENT_STATUS_CHANGED, EventType.AGENT_TERMINATED] },
|
||||
{ id: 'issue', label: 'Issues', types: [EventType.ISSUE_CREATED, EventType.ISSUE_UPDATED, EventType.ISSUE_ASSIGNED, EventType.ISSUE_CLOSED] },
|
||||
{
|
||||
id: 'agent',
|
||||
label: 'Agent Actions',
|
||||
types: [
|
||||
EventType.AGENT_SPAWNED,
|
||||
EventType.AGENT_MESSAGE,
|
||||
EventType.AGENT_STATUS_CHANGED,
|
||||
EventType.AGENT_TERMINATED,
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'issue',
|
||||
label: 'Issues',
|
||||
types: [
|
||||
EventType.ISSUE_CREATED,
|
||||
EventType.ISSUE_UPDATED,
|
||||
EventType.ISSUE_ASSIGNED,
|
||||
EventType.ISSUE_CLOSED,
|
||||
],
|
||||
},
|
||||
{ id: 'sprint', label: 'Sprints', types: [EventType.SPRINT_STARTED, EventType.SPRINT_COMPLETED] },
|
||||
{ id: 'approval', label: 'Approvals', types: [EventType.APPROVAL_REQUESTED, EventType.APPROVAL_GRANTED, EventType.APPROVAL_DENIED] },
|
||||
{ id: 'workflow', label: 'Workflows', types: [EventType.WORKFLOW_STARTED, EventType.WORKFLOW_STEP_COMPLETED, EventType.WORKFLOW_COMPLETED, EventType.WORKFLOW_FAILED] },
|
||||
{ id: 'project', label: 'Projects', types: [EventType.PROJECT_CREATED, EventType.PROJECT_UPDATED, EventType.PROJECT_ARCHIVED] },
|
||||
{
|
||||
id: 'approval',
|
||||
label: 'Approvals',
|
||||
types: [EventType.APPROVAL_REQUESTED, EventType.APPROVAL_GRANTED, EventType.APPROVAL_DENIED],
|
||||
},
|
||||
{
|
||||
id: 'workflow',
|
||||
label: 'Workflows',
|
||||
types: [
|
||||
EventType.WORKFLOW_STARTED,
|
||||
EventType.WORKFLOW_STEP_COMPLETED,
|
||||
EventType.WORKFLOW_COMPLETED,
|
||||
EventType.WORKFLOW_FAILED,
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'project',
|
||||
label: 'Projects',
|
||||
types: [EventType.PROJECT_CREATED, EventType.PROJECT_UPDATED, EventType.PROJECT_ARCHIVED],
|
||||
},
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
@@ -266,25 +301,60 @@ function getEventConfig(event: ProjectEvent) {
|
||||
|
||||
// Fallback based on event category
|
||||
if (isAgentEvent(event)) {
|
||||
return { icon: Bot, label: event.type, color: 'text-blue-500', bgColor: 'bg-blue-100 dark:bg-blue-900' };
|
||||
return {
|
||||
icon: Bot,
|
||||
label: event.type,
|
||||
color: 'text-blue-500',
|
||||
bgColor: 'bg-blue-100 dark:bg-blue-900',
|
||||
};
|
||||
}
|
||||
if (isIssueEvent(event)) {
|
||||
return { icon: FileText, label: event.type, color: 'text-green-500', bgColor: 'bg-green-100 dark:bg-green-900' };
|
||||
return {
|
||||
icon: FileText,
|
||||
label: event.type,
|
||||
color: 'text-green-500',
|
||||
bgColor: 'bg-green-100 dark:bg-green-900',
|
||||
};
|
||||
}
|
||||
if (isSprintEvent(event)) {
|
||||
return { icon: PlayCircle, label: event.type, color: 'text-indigo-500', bgColor: 'bg-indigo-100 dark:bg-indigo-900' };
|
||||
return {
|
||||
icon: PlayCircle,
|
||||
label: event.type,
|
||||
color: 'text-indigo-500',
|
||||
bgColor: 'bg-indigo-100 dark:bg-indigo-900',
|
||||
};
|
||||
}
|
||||
if (isApprovalEvent(event)) {
|
||||
return { icon: AlertTriangle, label: event.type, color: 'text-orange-500', bgColor: 'bg-orange-100 dark:bg-orange-900' };
|
||||
return {
|
||||
icon: AlertTriangle,
|
||||
label: event.type,
|
||||
color: 'text-orange-500',
|
||||
bgColor: 'bg-orange-100 dark:bg-orange-900',
|
||||
};
|
||||
}
|
||||
if (isWorkflowEvent(event)) {
|
||||
return { icon: Workflow, label: event.type, color: 'text-cyan-500', bgColor: 'bg-cyan-100 dark:bg-cyan-900' };
|
||||
return {
|
||||
icon: Workflow,
|
||||
label: event.type,
|
||||
color: 'text-cyan-500',
|
||||
bgColor: 'bg-cyan-100 dark:bg-cyan-900',
|
||||
};
|
||||
}
|
||||
if (isProjectEvent(event)) {
|
||||
return { icon: Folder, label: event.type, color: 'text-teal-500', bgColor: 'bg-teal-100 dark:bg-teal-900' };
|
||||
return {
|
||||
icon: Folder,
|
||||
label: event.type,
|
||||
color: 'text-teal-500',
|
||||
bgColor: 'bg-teal-100 dark:bg-teal-900',
|
||||
};
|
||||
}
|
||||
|
||||
return { icon: Activity, label: event.type, color: 'text-gray-500', bgColor: 'bg-gray-100 dark:bg-gray-800' };
|
||||
return {
|
||||
icon: Activity,
|
||||
label: event.type,
|
||||
color: 'text-gray-500',
|
||||
bgColor: 'bg-gray-100 dark:bg-gray-800',
|
||||
};
|
||||
}
|
||||
|
||||
function getEventSummary(event: ProjectEvent): string {
|
||||
@@ -304,7 +374,9 @@ function getEventSummary(event: ProjectEvent): string {
|
||||
case EventType.ISSUE_UPDATED:
|
||||
return `Issue ${payload.issue_id || ''} updated`;
|
||||
case EventType.ISSUE_ASSIGNED:
|
||||
return payload.assignee_name ? `Assigned to ${payload.assignee_name}` : 'Issue assignment changed';
|
||||
return payload.assignee_name
|
||||
? `Assigned to ${payload.assignee_name}`
|
||||
: 'Issue assignment changed';
|
||||
case EventType.ISSUE_CLOSED:
|
||||
return payload.resolution ? `Closed: ${payload.resolution}` : 'Issue closed';
|
||||
case EventType.SPRINT_STARTED:
|
||||
@@ -318,11 +390,15 @@ function getEventSummary(event: ProjectEvent): string {
|
||||
case EventType.APPROVAL_DENIED:
|
||||
return payload.reason ? `Denied: ${payload.reason}` : 'Approval denied';
|
||||
case EventType.WORKFLOW_STARTED:
|
||||
return payload.workflow_type ? `${payload.workflow_type} workflow started` : 'Workflow started';
|
||||
return payload.workflow_type
|
||||
? `${payload.workflow_type} workflow started`
|
||||
: 'Workflow started';
|
||||
case EventType.WORKFLOW_STEP_COMPLETED:
|
||||
return `Step ${payload.step_number}/${payload.total_steps}: ${payload.step_name || 'completed'}`;
|
||||
case EventType.WORKFLOW_COMPLETED:
|
||||
return payload.duration_seconds ? `Completed in ${payload.duration_seconds}s` : 'Workflow completed';
|
||||
return payload.duration_seconds
|
||||
? `Completed in ${payload.duration_seconds}s`
|
||||
: 'Workflow completed';
|
||||
case EventType.WORKFLOW_FAILED:
|
||||
return payload.error_message ? String(payload.error_message) : 'Workflow failed';
|
||||
default:
|
||||
@@ -391,11 +467,7 @@ function ConnectionIndicator({ state, onReconnect, className }: ConnectionIndica
|
||||
return (
|
||||
<div className={cn('flex items-center gap-2', className)} data-testid="connection-indicator">
|
||||
<span
|
||||
className={cn(
|
||||
'h-2 w-2 rounded-full',
|
||||
config.color,
|
||||
config.pulse && 'animate-pulse'
|
||||
)}
|
||||
className={cn('h-2 w-2 rounded-full', config.color, config.pulse && 'animate-pulse')}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">{config.label}</span>
|
||||
@@ -475,7 +547,10 @@ function FilterPanel({
|
||||
checked={showPendingOnly}
|
||||
onCheckedChange={(checked) => onShowPendingOnlyChange(checked as boolean)}
|
||||
/>
|
||||
<Label htmlFor="filter-pending" className="flex items-center gap-1 text-sm font-normal cursor-pointer">
|
||||
<Label
|
||||
htmlFor="filter-pending"
|
||||
className="flex items-center gap-1 text-sm font-normal cursor-pointer"
|
||||
>
|
||||
Show only pending approvals
|
||||
{pendingCount > 0 && (
|
||||
<Badge variant="destructive" className="text-xs">
|
||||
@@ -598,77 +673,85 @@ function EventItem({
|
||||
}}
|
||||
aria-label={expanded ? 'Collapse details' : 'Expand details'}
|
||||
>
|
||||
{expanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
||||
{expanded ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded Details */}
|
||||
{expanded && (() => {
|
||||
const issueId = payload.issue_id as string | undefined;
|
||||
const pullRequest = payload.pullRequest as string | number | undefined;
|
||||
const documentUrl = payload.documentUrl as string | undefined;
|
||||
const progress = payload.progress as number | undefined;
|
||||
{expanded &&
|
||||
(() => {
|
||||
const issueId = payload.issue_id as string | undefined;
|
||||
const pullRequest = payload.pullRequest as string | number | undefined;
|
||||
const documentUrl = payload.documentUrl as string | undefined;
|
||||
const progress = payload.progress as number | undefined;
|
||||
|
||||
return (
|
||||
<div className="mt-3 rounded-md bg-muted/50 p-3 space-y-3" data-testid="event-details">
|
||||
{/* Issue/PR Links */}
|
||||
{issueId && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<CircleDot className="h-4 w-4" aria-hidden="true" />
|
||||
<span>Issue #{issueId}</span>
|
||||
</div>
|
||||
)}
|
||||
{pullRequest && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<GitPullRequest className="h-4 w-4" aria-hidden="true" />
|
||||
<span>PR #{String(pullRequest)}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Document Links */}
|
||||
{documentUrl && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<ExternalLink className="h-4 w-4" aria-hidden="true" />
|
||||
<a href={documentUrl} className="text-primary hover:underline">
|
||||
{documentUrl}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Progress */}
|
||||
{progress !== undefined && (
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>Progress</span>
|
||||
<span>{progress}%</span>
|
||||
return (
|
||||
<div
|
||||
className="mt-3 rounded-md bg-muted/50 p-3 space-y-3"
|
||||
data-testid="event-details"
|
||||
>
|
||||
{/* Issue/PR Links */}
|
||||
{issueId && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<CircleDot className="h-4 w-4" aria-hidden="true" />
|
||||
<span>Issue #{issueId}</span>
|
||||
</div>
|
||||
<div className="h-2 rounded-full bg-muted">
|
||||
<div
|
||||
className="h-full rounded-full bg-primary"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
)}
|
||||
{pullRequest && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<GitPullRequest className="h-4 w-4" aria-hidden="true" />
|
||||
<span>PR #{String(pullRequest)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* Timestamp */}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{new Date(event.timestamp).toLocaleString()}
|
||||
</p>
|
||||
{/* Document Links */}
|
||||
{documentUrl && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<ExternalLink className="h-4 w-4" aria-hidden="true" />
|
||||
<a href={documentUrl} className="text-primary hover:underline">
|
||||
{documentUrl}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Raw Payload (for debugging) */}
|
||||
<details className="text-xs">
|
||||
<summary className="cursor-pointer text-muted-foreground hover:text-foreground">
|
||||
View raw payload
|
||||
</summary>
|
||||
<pre className="mt-2 overflow-x-auto whitespace-pre-wrap break-words rounded bg-muted p-2">
|
||||
{JSON.stringify(event.payload, null, 2)}
|
||||
</pre>
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
{/* Progress */}
|
||||
{progress !== undefined && (
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>Progress</span>
|
||||
<span>{progress}%</span>
|
||||
</div>
|
||||
<div className="h-2 rounded-full bg-muted">
|
||||
<div
|
||||
className="h-full rounded-full bg-primary"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Timestamp */}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{new Date(event.timestamp).toLocaleString()}
|
||||
</p>
|
||||
|
||||
{/* Raw Payload (for debugging) */}
|
||||
<details className="text-xs">
|
||||
<summary className="cursor-pointer text-muted-foreground hover:text-foreground">
|
||||
View raw payload
|
||||
</summary>
|
||||
<pre className="mt-2 overflow-x-auto whitespace-pre-wrap break-words rounded bg-muted p-2">
|
||||
{JSON.stringify(event.payload, null, 2)}
|
||||
</pre>
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Approval Actions */}
|
||||
{isPendingApproval && (onApprove || onReject) && (
|
||||
@@ -680,7 +763,12 @@ function EventItem({
|
||||
</Button>
|
||||
)}
|
||||
{onReject && (
|
||||
<Button variant="outline" size="sm" onClick={handleReject} data-testid="reject-button">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleReject}
|
||||
data-testid="reject-button"
|
||||
>
|
||||
<XCircle className="mr-2 h-4 w-4" />
|
||||
Reject
|
||||
</Button>
|
||||
@@ -712,7 +800,10 @@ function LoadingSkeleton() {
|
||||
|
||||
function EmptyState({ hasFilters }: { hasFilters: boolean }) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground" data-testid="empty-state">
|
||||
<div
|
||||
className="flex flex-col items-center justify-center py-12 text-muted-foreground"
|
||||
data-testid="empty-state"
|
||||
>
|
||||
<Activity className="h-12 w-12 mb-4" aria-hidden="true" />
|
||||
<h3 className="font-semibold">No activity found</h3>
|
||||
<p className="text-sm">
|
||||
@@ -894,7 +985,10 @@ export function ActivityFeed({
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{groupedEvents.map((group) => (
|
||||
<div key={group.label} data-testid={`event-group-${group.label.toLowerCase().replace(' ', '-')}`}>
|
||||
<div
|
||||
key={group.label}
|
||||
data-testid={`event-group-${group.label.toLowerCase().replace(' ', '-')}`}
|
||||
>
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<h3 className="text-sm font-medium text-muted-foreground">{group.label}</h3>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
|
||||
@@ -57,13 +57,19 @@ interface AgentTypeDetailProps {
|
||||
function AgentTypeStatusBadge({ isActive }: { isActive: boolean }) {
|
||||
if (isActive) {
|
||||
return (
|
||||
<Badge className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200" variant="outline">
|
||||
<Badge
|
||||
className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
|
||||
variant="outline"
|
||||
>
|
||||
Active
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Badge className="bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200" variant="outline">
|
||||
<Badge
|
||||
className="bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200"
|
||||
variant="outline"
|
||||
>
|
||||
Inactive
|
||||
</Badge>
|
||||
);
|
||||
@@ -139,9 +145,7 @@ export function AgentTypeDetail({
|
||||
<div className="py-12 text-center">
|
||||
<AlertTriangle className="mx-auto h-12 w-12 text-muted-foreground" />
|
||||
<h3 className="mt-4 font-semibold">Agent type not found</h3>
|
||||
<p className="text-muted-foreground">
|
||||
The requested agent type could not be found
|
||||
</p>
|
||||
<p className="text-muted-foreground">The requested agent type could not be found</p>
|
||||
<Button onClick={onBack} variant="outline" className="mt-4">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Go Back
|
||||
@@ -265,9 +269,7 @@ export function AgentTypeDetail({
|
||||
<div
|
||||
key={server.id}
|
||||
className={`flex items-center justify-between rounded-lg border p-3 ${
|
||||
isEnabled
|
||||
? 'border-primary/20 bg-primary/5'
|
||||
: 'border-muted bg-muted/50'
|
||||
isEnabled ? 'border-primary/20 bg-primary/5' : 'border-muted bg-muted/50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -284,9 +286,7 @@ export function AgentTypeDetail({
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">{server.name}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{server.description}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{server.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant={isEnabled ? 'default' : 'secondary'}>
|
||||
@@ -313,9 +313,7 @@ export function AgentTypeDetail({
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Primary Model</p>
|
||||
<p className="font-medium">
|
||||
{getModelDisplayName(agentType.primary_model)}
|
||||
</p>
|
||||
<p className="font-medium">{getModelDisplayName(agentType.primary_model)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Failover Model</p>
|
||||
@@ -355,9 +353,7 @@ export function AgentTypeDetail({
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-center">
|
||||
<p className="text-4xl font-bold text-primary">
|
||||
{agentType.instance_count}
|
||||
</p>
|
||||
<p className="text-4xl font-bold text-primary">{agentType.instance_count}</p>
|
||||
<p className="text-sm text-muted-foreground">Active instances</p>
|
||||
</div>
|
||||
<Button variant="outline" className="mt-4 w-full" size="sm" disabled>
|
||||
|
||||
@@ -26,16 +26,7 @@ import {
|
||||
} from '@/components/ui/select';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import {
|
||||
FileText,
|
||||
Cpu,
|
||||
Shield,
|
||||
MessageSquare,
|
||||
Sliders,
|
||||
Save,
|
||||
ArrowLeft,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { FileText, Cpu, Shield, MessageSquare, Sliders, Save, ArrowLeft, X } from 'lucide-react';
|
||||
import {
|
||||
agentTypeCreateSchema,
|
||||
type AgentTypeCreateFormValues,
|
||||
@@ -151,9 +142,7 @@ export function AgentTypeForm({
|
||||
{isEditing ? 'Edit Agent Type' : 'Create Agent Type'}
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
{isEditing
|
||||
? 'Modify agent type configuration'
|
||||
: 'Define a new agent type template'}
|
||||
{isEditing ? 'Modify agent type configuration' : 'Define a new agent type template'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
@@ -281,9 +270,7 @@ export function AgentTypeForm({
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Expertise Areas</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Add skills and areas of expertise
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">Add skills and areas of expertise</p>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="e.g., System Design"
|
||||
@@ -325,9 +312,7 @@ export function AgentTypeForm({
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Model Selection</CardTitle>
|
||||
<CardDescription>
|
||||
Choose the AI models that power this agent type
|
||||
</CardDescription>
|
||||
<CardDescription>Choose the AI models that power this agent type</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
@@ -358,9 +343,7 @@ export function AgentTypeForm({
|
||||
{errors.primary_model.message}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Main model used for this agent
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">Main model used for this agent</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="fallback_model">Fallover Model</Label>
|
||||
@@ -420,9 +403,7 @@ export function AgentTypeForm({
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
0 = deterministic, 2 = creative
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">0 = deterministic, 2 = creative</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="max_tokens">Max Tokens</Label>
|
||||
@@ -472,9 +453,7 @@ export function AgentTypeForm({
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>MCP Server Permissions</CardTitle>
|
||||
<CardDescription>
|
||||
Configure which MCP servers this agent can access
|
||||
</CardDescription>
|
||||
<CardDescription>Configure which MCP servers this agent can access</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{AVAILABLE_MCP_SERVERS.map((server) => (
|
||||
@@ -508,8 +487,8 @@ export function AgentTypeForm({
|
||||
<CardHeader>
|
||||
<CardTitle>Personality Prompt</CardTitle>
|
||||
<CardDescription>
|
||||
Define the agent's personality, behavior, and communication style. This
|
||||
prompt shapes how the agent approaches tasks and interacts.
|
||||
Define the agent's personality, behavior, and communication style. This prompt
|
||||
shapes how the agent approaches tasks and interacts.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -535,9 +514,7 @@ export function AgentTypeForm({
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<span>
|
||||
Character count: {watch('personality_prompt')?.length || 0}
|
||||
</span>
|
||||
<span>Character count: {watch('personality_prompt')?.length || 0}</span>
|
||||
<Separator orientation="vertical" className="h-4" />
|
||||
<span className="text-xs">
|
||||
Tip: Be specific about expertise, communication style, and decision-making
|
||||
|
||||
@@ -41,13 +41,19 @@ interface AgentTypeListProps {
|
||||
function AgentTypeStatusBadge({ isActive }: { isActive: boolean }) {
|
||||
if (isActive) {
|
||||
return (
|
||||
<Badge className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200" variant="outline">
|
||||
<Badge
|
||||
className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
|
||||
variant="outline"
|
||||
>
|
||||
Active
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Badge className="bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200" variant="outline">
|
||||
<Badge
|
||||
className="bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200"
|
||||
variant="outline"
|
||||
>
|
||||
Inactive
|
||||
</Badge>
|
||||
);
|
||||
|
||||
@@ -12,13 +12,7 @@
|
||||
import { Component, type ReactNode } from 'react';
|
||||
import { AlertTriangle, RefreshCw } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
@@ -59,25 +53,17 @@ function DefaultFallback({ error, onReset, showReset }: DefaultFallbackProps) {
|
||||
Something went wrong
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
An unexpected error occurred. Please try again or contact support if
|
||||
the problem persists.
|
||||
An unexpected error occurred. Please try again or contact support if the problem persists.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{error && (
|
||||
<div className="mb-4 rounded-md bg-muted p-3">
|
||||
<p className="font-mono text-sm text-muted-foreground">
|
||||
{error.message}
|
||||
</p>
|
||||
<p className="font-mono text-sm text-muted-foreground">{error.message}</p>
|
||||
</div>
|
||||
)}
|
||||
{showReset && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onReset}
|
||||
className="gap-2"
|
||||
>
|
||||
<Button variant="outline" size="sm" onClick={onReset} className="gap-2">
|
||||
<RefreshCw className="h-4 w-4" aria-hidden="true" />
|
||||
Try again
|
||||
</Button>
|
||||
@@ -108,10 +94,7 @@ function DefaultFallback({ error, onReset, showReset }: DefaultFallbackProps) {
|
||||
* </ErrorBoundary>
|
||||
* ```
|
||||
*/
|
||||
export class ErrorBoundary extends Component<
|
||||
ErrorBoundaryProps,
|
||||
ErrorBoundaryState
|
||||
> {
|
||||
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||
constructor(props: ErrorBoundaryProps) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: null };
|
||||
@@ -142,13 +125,7 @@ export class ErrorBoundary extends Component<
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return (
|
||||
<DefaultFallback
|
||||
error={error}
|
||||
onReset={this.handleReset}
|
||||
showReset={showReset}
|
||||
/>
|
||||
);
|
||||
return <DefaultFallback error={error} onReset={this.handleReset} showReset={showReset} />;
|
||||
}
|
||||
|
||||
return children;
|
||||
|
||||
@@ -153,7 +153,8 @@ export function ConnectionStatus({
|
||||
className={cn(
|
||||
'flex flex-col gap-3 rounded-lg border p-4',
|
||||
state === 'error' && 'border-destructive bg-destructive/5',
|
||||
state === 'connected' && 'border-green-200 bg-green-50 dark:border-green-900 dark:bg-green-950',
|
||||
state === 'connected' &&
|
||||
'border-green-200 bg-green-50 dark:border-green-900 dark:bg-green-950',
|
||||
className
|
||||
)}
|
||||
role="status"
|
||||
@@ -199,11 +200,7 @@ export function ConnectionStatus({
|
||||
{showErrorDetails && error && (
|
||||
<div className="rounded-md bg-destructive/10 p-3 text-sm">
|
||||
<p className="font-medium text-destructive">Error: {error.message}</p>
|
||||
{error.code && (
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
Code: {error.code}
|
||||
</p>
|
||||
)}
|
||||
{error.code && <p className="mt-1 text-muted-foreground">Code: {error.code}</p>}
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{new Date(error.timestamp).toLocaleTimeString()}
|
||||
</p>
|
||||
|
||||
@@ -250,17 +250,11 @@ function getEventSummary(event: ProjectEvent): string {
|
||||
? `Assigned to ${payload.assignee_name}`
|
||||
: 'Issue assignment changed';
|
||||
case EventType.ISSUE_CLOSED:
|
||||
return payload.resolution
|
||||
? `Closed: ${payload.resolution}`
|
||||
: 'Issue closed';
|
||||
return payload.resolution ? `Closed: ${payload.resolution}` : 'Issue closed';
|
||||
case EventType.SPRINT_STARTED:
|
||||
return payload.sprint_name
|
||||
? `Sprint "${payload.sprint_name}" started`
|
||||
: 'Sprint started';
|
||||
return payload.sprint_name ? `Sprint "${payload.sprint_name}" started` : 'Sprint started';
|
||||
case EventType.SPRINT_COMPLETED:
|
||||
return payload.sprint_name
|
||||
? `Sprint "${payload.sprint_name}" completed`
|
||||
: 'Sprint completed';
|
||||
return payload.sprint_name ? `Sprint "${payload.sprint_name}" completed` : 'Sprint completed';
|
||||
case EventType.APPROVAL_REQUESTED:
|
||||
return String(payload.description || 'Approval requested');
|
||||
case EventType.APPROVAL_GRANTED:
|
||||
@@ -278,9 +272,7 @@ function getEventSummary(event: ProjectEvent): string {
|
||||
? `Completed in ${payload.duration_seconds}s`
|
||||
: 'Workflow completed';
|
||||
case EventType.WORKFLOW_FAILED:
|
||||
return payload.error_message
|
||||
? String(payload.error_message)
|
||||
: 'Workflow failed';
|
||||
return payload.error_message ? String(payload.error_message) : 'Workflow failed';
|
||||
default:
|
||||
return event.type;
|
||||
}
|
||||
|
||||
@@ -72,8 +72,8 @@ export function HeroSection({ onOpenDemoModal }: HeroSectionProps) {
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.2 }}
|
||||
>
|
||||
Opinionated, secure, and production-ready. Syndarix gives you the solid foundation
|
||||
you need to stop configuring and start shipping.{' '}
|
||||
Opinionated, secure, and production-ready. Syndarix gives you the solid foundation you
|
||||
need to stop configuring and start shipping.{' '}
|
||||
<span className="text-foreground font-medium">Start building features on day one.</span>
|
||||
</motion.p>
|
||||
|
||||
|
||||
@@ -74,11 +74,7 @@ function generateBreadcrumbs(pathname: string): BreadcrumbItem[] {
|
||||
return breadcrumbs;
|
||||
}
|
||||
|
||||
export function AppBreadcrumbs({
|
||||
items,
|
||||
showHome = true,
|
||||
className,
|
||||
}: AppBreadcrumbsProps) {
|
||||
export function AppBreadcrumbs({ items, showHome = true, className }: AppBreadcrumbsProps) {
|
||||
const pathname = usePathname();
|
||||
|
||||
// Use provided items or generate from pathname
|
||||
|
||||
@@ -49,11 +49,7 @@ export function AppHeader({
|
||||
{/* Left side - Logo and Project Switcher */}
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Logo - visible on mobile, hidden on desktop when sidebar is visible */}
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center gap-2 lg:hidden"
|
||||
aria-label="Syndarix home"
|
||||
>
|
||||
<Link href="/" className="flex items-center gap-2 lg:hidden" aria-label="Syndarix home">
|
||||
<Image
|
||||
src="/logo-icon.svg"
|
||||
alt=""
|
||||
|
||||
@@ -73,11 +73,7 @@ export function AppLayout({
|
||||
{!hideBreadcrumbs && <AppBreadcrumbs items={breadcrumbs} />}
|
||||
|
||||
{/* Main content */}
|
||||
<main
|
||||
className={cn('flex-1', className)}
|
||||
id="main-content"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<main className={cn('flex-1', className)} id="main-content" tabIndex={-1}>
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
@@ -110,11 +106,7 @@ const maxWidthClasses: Record<string, string> = {
|
||||
full: 'max-w-full',
|
||||
};
|
||||
|
||||
export function PageContainer({
|
||||
children,
|
||||
maxWidth = '6xl',
|
||||
className,
|
||||
}: PageContainerProps) {
|
||||
export function PageContainer({ children, maxWidth = '6xl', className }: PageContainerProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -144,12 +136,7 @@ interface PageHeaderProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function PageHeader({
|
||||
title,
|
||||
description,
|
||||
actions,
|
||||
className,
|
||||
}: PageHeaderProps) {
|
||||
export function PageHeader({ title, description, actions, className }: PageHeaderProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -160,9 +147,7 @@ export function PageHeader({
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-2xl font-bold tracking-tight sm:text-3xl">{title}</h1>
|
||||
{description && (
|
||||
<p className="text-muted-foreground">{description}</p>
|
||||
)}
|
||||
{description && <p className="text-muted-foreground">{description}</p>}
|
||||
</div>
|
||||
{actions && <div className="flex items-center gap-2">{actions}</div>}
|
||||
</div>
|
||||
|
||||
@@ -98,9 +98,7 @@ export function ProjectSwitcher({
|
||||
className={cn('gap-2 min-w-[160px] justify-between', className)}
|
||||
data-testid="project-switcher-trigger"
|
||||
aria-label={
|
||||
currentProject
|
||||
? `Switch project, current: ${currentProject.name}`
|
||||
: 'Select project'
|
||||
currentProject ? `Switch project, current: ${currentProject.name}` : 'Select project'
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -112,11 +110,7 @@ export function ProjectSwitcher({
|
||||
<ChevronDown className="h-4 w-4 opacity-50" aria-hidden="true" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
className="w-[200px]"
|
||||
data-testid="project-switcher-menu"
|
||||
>
|
||||
<DropdownMenuContent align="start" className="w-[200px]" data-testid="project-switcher-menu">
|
||||
<DropdownMenuLabel>Projects</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{projects.map((project) => (
|
||||
|
||||
@@ -11,13 +11,7 @@ import { Link } from '@/lib/i18n/routing';
|
||||
import { usePathname } from '@/lib/i18n/routing';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from '@/components/ui/sheet';
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
|
||||
import {
|
||||
FolderKanban,
|
||||
Bot,
|
||||
@@ -113,9 +107,7 @@ function NavLink({ item, collapsed, basePath = '' }: NavLinkProps) {
|
||||
const pathname = usePathname();
|
||||
const href = basePath ? `${basePath}${item.href}` : item.href;
|
||||
|
||||
const isActive = item.exact
|
||||
? pathname === href
|
||||
: pathname.startsWith(href);
|
||||
const isActive = item.exact ? pathname === href : pathname.startsWith(href);
|
||||
|
||||
const Icon = item.icon;
|
||||
|
||||
@@ -155,9 +147,7 @@ function SidebarContent({
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Sidebar Header */}
|
||||
<div className="flex h-14 items-center justify-between border-b px-4">
|
||||
{!collapsed && (
|
||||
<span className="text-lg font-semibold text-foreground">Navigation</span>
|
||||
)}
|
||||
{!collapsed && <span className="text-lg font-semibold text-foreground">Navigation</span>}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -308,11 +298,7 @@ export function Sidebar({ projectSlug, className }: SidebarProps) {
|
||||
data-testid="sidebar"
|
||||
aria-label="Main navigation"
|
||||
>
|
||||
<SidebarContent
|
||||
collapsed={collapsed}
|
||||
projectSlug={projectSlug}
|
||||
onToggle={handleToggle}
|
||||
/>
|
||||
<SidebarContent collapsed={collapsed} projectSlug={projectSlug} onToggle={handleToggle} />
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -20,14 +20,7 @@ import {
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
||||
import {
|
||||
User,
|
||||
LogOut,
|
||||
Shield,
|
||||
Lock,
|
||||
Monitor,
|
||||
UserCog,
|
||||
} from 'lucide-react';
|
||||
import { User, LogOut, Shield, Lock, Monitor, UserCog } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface UserMenuProps {
|
||||
@@ -76,20 +69,14 @@ export function UserMenu({ className }: UserMenuProps) {
|
||||
</Avatar>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-56"
|
||||
align="end"
|
||||
data-testid="user-menu-content"
|
||||
>
|
||||
<DropdownMenuContent className="w-56" align="end" data-testid="user-menu-content">
|
||||
{/* User info header */}
|
||||
<DropdownMenuLabel className="font-normal">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-sm font-medium leading-none">
|
||||
{user.first_name} {user.last_name}
|
||||
</p>
|
||||
<p className="text-xs leading-none text-muted-foreground">
|
||||
{user.email}
|
||||
</p>
|
||||
<p className="text-xs leading-none text-muted-foreground">{user.email}</p>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
@@ -143,11 +130,7 @@ export function UserMenu({ className }: UserMenuProps) {
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
href="/admin"
|
||||
className="cursor-pointer"
|
||||
data-testid="user-menu-admin"
|
||||
>
|
||||
<Link href="/admin" className="cursor-pointer" data-testid="user-menu-admin">
|
||||
<Shield className="mr-2 h-4 w-4" aria-hidden="true" />
|
||||
{t('adminPanel')}
|
||||
</Link>
|
||||
|
||||
@@ -9,13 +9,7 @@
|
||||
import { Bot, MoreVertical } from 'lucide-react';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -228,11 +222,7 @@ export function AgentPanel({
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{agents.map((agent) => (
|
||||
<AgentListItem
|
||||
key={agent.id}
|
||||
agent={agent}
|
||||
onAction={onAgentAction}
|
||||
/>
|
||||
<AgentListItem key={agent.id} agent={agent} onAction={onAgentAction} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -60,16 +60,10 @@ export function AgentStatusIndicator({
|
||||
aria-label={`Status: ${config.label}`}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'inline-block rounded-full',
|
||||
sizeClasses[size],
|
||||
config.color
|
||||
)}
|
||||
className={cn('inline-block rounded-full', sizeClasses[size], config.color)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{showLabel && (
|
||||
<span className="text-xs text-muted-foreground">{config.label}</span>
|
||||
)}
|
||||
{showLabel && <span className="text-xs text-muted-foreground">{config.label}</span>}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -109,15 +109,7 @@ export function BurndownChart({
|
||||
{data.map((d, i) => {
|
||||
const x = padding.left + (i / (data.length - 1)) * innerWidth;
|
||||
const y = padding.top + innerHeight - (d.remaining / maxPoints) * innerHeight;
|
||||
return (
|
||||
<circle
|
||||
key={i}
|
||||
cx={x}
|
||||
cy={y}
|
||||
r="2"
|
||||
className="fill-primary"
|
||||
/>
|
||||
);
|
||||
return <circle key={i} cx={x} cy={y} r="2" className="fill-primary" />;
|
||||
})}
|
||||
</svg>
|
||||
|
||||
|
||||
@@ -6,22 +6,10 @@
|
||||
|
||||
'use client';
|
||||
|
||||
import {
|
||||
GitBranch,
|
||||
CircleDot,
|
||||
PlayCircle,
|
||||
Clock,
|
||||
AlertCircle,
|
||||
CheckCircle2,
|
||||
} from 'lucide-react';
|
||||
import { GitBranch, CircleDot, PlayCircle, Clock, AlertCircle, CheckCircle2 } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import type { IssueCountSummary } from './types';
|
||||
@@ -141,12 +129,7 @@ export function IssueSummary({
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3" role="list" aria-label="Issue counts by status">
|
||||
<StatusRow
|
||||
icon={CircleDot}
|
||||
iconColor="text-blue-500"
|
||||
label="Open"
|
||||
count={summary.open}
|
||||
/>
|
||||
<StatusRow icon={CircleDot} iconColor="text-blue-500" label="Open" count={summary.open} />
|
||||
<StatusRow
|
||||
icon={PlayCircle}
|
||||
iconColor="text-yellow-500"
|
||||
@@ -177,12 +160,7 @@ export function IssueSummary({
|
||||
|
||||
{onViewAllIssues && (
|
||||
<div className="pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
size="sm"
|
||||
onClick={onViewAllIssues}
|
||||
>
|
||||
<Button variant="outline" className="w-full" size="sm" onClick={onViewAllIssues}>
|
||||
View All Issues ({summary.total})
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -85,14 +85,12 @@ export function ProjectHeader({
|
||||
}
|
||||
|
||||
const showPauseButton = canPause && project.status === 'active';
|
||||
const showStartButton = canStart && project.status !== 'completed' && project.status !== 'archived';
|
||||
const showStartButton =
|
||||
canStart && project.status !== 'completed' && project.status !== 'archived';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col gap-4 md:flex-row md:items-start md:justify-between',
|
||||
className
|
||||
)}
|
||||
className={cn('flex flex-col gap-4 md:flex-row md:items-start md:justify-between', className)}
|
||||
data-testid="project-header"
|
||||
>
|
||||
{/* Project Info */}
|
||||
@@ -102,20 +100,13 @@ export function ProjectHeader({
|
||||
<ProjectStatusBadge status={project.status} />
|
||||
<AutonomyBadge level={project.autonomy_level} />
|
||||
</div>
|
||||
{project.description && (
|
||||
<p className="text-muted-foreground">{project.description}</p>
|
||||
)}
|
||||
{project.description && <p className="text-muted-foreground">{project.description}</p>}
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{onSettings && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onSettings}
|
||||
aria-label="Project settings"
|
||||
>
|
||||
<Button variant="ghost" size="icon" onClick={onSettings} aria-label="Project settings">
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -19,12 +19,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import type { ActivityItem } from './types';
|
||||
|
||||
@@ -104,9 +99,7 @@ function ActivityItemRow({ activity, onActionClick }: ActivityItemRowProps) {
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm">
|
||||
{activity.agent && (
|
||||
<span className="font-medium">{activity.agent}</span>
|
||||
)}{' '}
|
||||
{activity.agent && <span className="font-medium">{activity.agent}</span>}{' '}
|
||||
<span className="text-muted-foreground">{activity.message}</span>
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{timestamp}</p>
|
||||
|
||||
@@ -9,13 +9,7 @@
|
||||
import { TrendingUp, Calendar } from 'lucide-react';
|
||||
import { format } from 'date-fns';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -188,10 +182,7 @@ export function SprintProgress({
|
||||
</div>
|
||||
|
||||
{availableSprints.length > 1 && onSprintChange && (
|
||||
<Select
|
||||
value={selectedSprintId || sprint.id}
|
||||
onValueChange={onSprintChange}
|
||||
>
|
||||
<Select value={selectedSprintId || sprint.id} onValueChange={onSprintChange}>
|
||||
<SelectTrigger className="w-32" aria-label="Select sprint">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
@@ -231,16 +222,8 @@ export function SprintProgress({
|
||||
label="In Progress"
|
||||
colorClass="text-blue-600"
|
||||
/>
|
||||
<StatCard
|
||||
value={sprint.blocked_issues}
|
||||
label="Blocked"
|
||||
colorClass="text-red-600"
|
||||
/>
|
||||
<StatCard
|
||||
value={sprint.todo_issues}
|
||||
label="To Do"
|
||||
colorClass="text-gray-600"
|
||||
/>
|
||||
<StatCard value={sprint.blocked_issues} label="Blocked" colorClass="text-red-600" />
|
||||
<StatCard value={sprint.todo_issues} label="To Do" colorClass="text-gray-600" />
|
||||
</div>
|
||||
|
||||
{/* Burndown Chart */}
|
||||
|
||||
@@ -81,9 +81,7 @@ export function AutonomyBadge({ level, showDescription = false, className }: Aut
|
||||
<Badge variant="secondary" className={cn('gap-1', className)} title={config.description}>
|
||||
<CircleDot className="h-3 w-3" aria-hidden="true" />
|
||||
{config.label}
|
||||
{showDescription && (
|
||||
<span className="text-muted-foreground"> - {config.description}</span>
|
||||
)}
|
||||
{showDescription && <span className="text-muted-foreground"> - {config.description}</span>}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -123,7 +123,13 @@ export interface IssueCountSummary {
|
||||
|
||||
export interface ActivityItem {
|
||||
id: string;
|
||||
type: 'agent_message' | 'issue_update' | 'agent_status' | 'approval_request' | 'sprint_event' | 'system';
|
||||
type:
|
||||
| 'agent_message'
|
||||
| 'issue_update'
|
||||
| 'agent_status'
|
||||
| 'approval_request'
|
||||
| 'sprint_event'
|
||||
| 'system';
|
||||
agent?: string;
|
||||
message: string;
|
||||
timestamp: string;
|
||||
|
||||
@@ -73,16 +73,13 @@ export function ProjectWizard({ locale, className }: ProjectWizardProps) {
|
||||
mutationFn: async (projectData: ProjectCreateData): Promise<ProjectResponse> => {
|
||||
// Call the projects API endpoint
|
||||
// Note: The API client already handles authentication via interceptors
|
||||
const response = await apiClient.instance.post<ProjectResponse>(
|
||||
'/api/v1/projects',
|
||||
{
|
||||
name: projectData.name,
|
||||
slug: projectData.slug,
|
||||
description: projectData.description,
|
||||
autonomy_level: projectData.autonomy_level,
|
||||
settings: projectData.settings,
|
||||
}
|
||||
);
|
||||
const response = await apiClient.instance.post<ProjectResponse>('/api/v1/projects', {
|
||||
name: projectData.name,
|
||||
slug: projectData.slug,
|
||||
description: projectData.description,
|
||||
autonomy_level: projectData.autonomy_level,
|
||||
settings: projectData.settings,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
},
|
||||
@@ -123,7 +120,10 @@ export function ProjectWizard({ locale, className }: ProjectWizardProps) {
|
||||
<Card className="text-center">
|
||||
<CardContent className="space-y-6 p-8">
|
||||
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-green-100 dark:bg-green-900">
|
||||
<CheckCircle2 className="h-8 w-8 text-green-600 dark:text-green-400" aria-hidden="true" />
|
||||
<CheckCircle2
|
||||
className="h-8 w-8 text-green-600 dark:text-green-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold">Project Created Successfully!</h2>
|
||||
@@ -192,10 +192,7 @@ export function ProjectWizard({ locale, className }: ProjectWizardProps) {
|
||||
<ArrowRight className="ml-2 h-4 w-4" aria-hidden="true" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={handleCreate}
|
||||
disabled={createProjectMutation.isPending}
|
||||
>
|
||||
<Button onClick={handleCreate} disabled={createProjectMutation.isPending}>
|
||||
{createProjectMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden="true" />
|
||||
|
||||
@@ -29,7 +29,12 @@ export function StepIndicator({ currentStep, isScriptMode, className }: StepIndi
|
||||
</span>
|
||||
<span>{steps[displayStep - 1]}</span>
|
||||
</div>
|
||||
<div className="flex gap-1" role="progressbar" aria-valuenow={displayStep} aria-valuemax={totalSteps}>
|
||||
<div
|
||||
className="flex gap-1"
|
||||
role="progressbar"
|
||||
aria-valuenow={displayStep}
|
||||
aria-valuemax={totalSteps}
|
||||
>
|
||||
{Array.from({ length: totalSteps }, (_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
|
||||
@@ -18,9 +18,4 @@ export type {
|
||||
} from './types';
|
||||
|
||||
// Re-export constants
|
||||
export {
|
||||
complexityOptions,
|
||||
clientModeOptions,
|
||||
autonomyOptions,
|
||||
WIZARD_STEPS,
|
||||
} from './constants';
|
||||
export { complexityOptions, clientModeOptions, autonomyOptions, WIZARD_STEPS } from './constants';
|
||||
|
||||
@@ -11,13 +11,7 @@ import { Bot, User, MessageSquare, Sparkles } from 'lucide-react';
|
||||
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
|
||||
@@ -11,12 +11,7 @@
|
||||
import { Check, AlertCircle } from 'lucide-react';
|
||||
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { SelectableCard } from '../SelectableCard';
|
||||
import { autonomyOptions } from '../constants';
|
||||
|
||||
@@ -34,7 +34,11 @@ export function ClientModeStep({ state, updateState }: ClientModeStepProps) {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2" role="radiogroup" aria-label="Client interaction mode options">
|
||||
<div
|
||||
className="grid gap-6 md:grid-cols-2"
|
||||
role="radiogroup"
|
||||
aria-label="Client interaction mode options"
|
||||
>
|
||||
{clientModeOptions.map((option) => {
|
||||
const Icon = option.icon;
|
||||
const isSelected = state.clientMode === option.id;
|
||||
|
||||
@@ -39,7 +39,11 @@ export function ComplexityStep({ state, updateState }: ComplexityStepProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2" role="radiogroup" aria-label="Project complexity options">
|
||||
<div
|
||||
className="grid gap-4 md:grid-cols-2"
|
||||
role="radiogroup"
|
||||
aria-label="Project complexity options"
|
||||
>
|
||||
{complexityOptions.map((option) => {
|
||||
const Icon = option.icon;
|
||||
const isSelected = state.complexity === option.id;
|
||||
|
||||
@@ -8,12 +8,7 @@
|
||||
|
||||
import { CheckCircle2 } from 'lucide-react';
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { complexityOptions, clientModeOptions, autonomyOptions } from '../constants';
|
||||
import type { WizardState } from '../types';
|
||||
|
||||
|
||||
@@ -20,11 +20,7 @@ interface ActivityTimelineProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ActivityTimeline({
|
||||
activities,
|
||||
onAddComment,
|
||||
className,
|
||||
}: ActivityTimelineProps) {
|
||||
export function ActivityTimeline({ activities, onAddComment, className }: ActivityTimelineProps) {
|
||||
return (
|
||||
<Card className={className}>
|
||||
<CardHeader>
|
||||
@@ -43,11 +39,7 @@ export function ActivityTimeline({
|
||||
<CardContent>
|
||||
<div className="space-y-6" role="list" aria-label="Issue activity">
|
||||
{activities.map((item, index) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex gap-4"
|
||||
role="listitem"
|
||||
>
|
||||
<div key={item.id} className="flex gap-4" role="listitem">
|
||||
<div className="relative flex flex-col items-center">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-muted">
|
||||
{item.actor.type === 'agent' ? (
|
||||
@@ -74,9 +66,7 @@ export function ActivityTimeline({
|
||||
</div>
|
||||
|
||||
{activities.length === 0 && (
|
||||
<div className="py-8 text-center text-muted-foreground">
|
||||
No activity yet
|
||||
</div>
|
||||
<div className="py-8 text-center text-muted-foreground">No activity yet</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -34,16 +34,11 @@ export function BulkActions({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-4 rounded-lg border bg-muted/50 p-3',
|
||||
className
|
||||
)}
|
||||
className={cn('flex items-center gap-4 rounded-lg border bg-muted/50 p-3', className)}
|
||||
role="toolbar"
|
||||
aria-label="Bulk actions for selected issues"
|
||||
>
|
||||
<span className="text-sm font-medium">
|
||||
{selectedCount} selected
|
||||
</span>
|
||||
<span className="text-sm font-medium">{selectedCount} selected</span>
|
||||
<Separator orientation="vertical" className="h-6" />
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" onClick={onChangeStatus}>
|
||||
|
||||
@@ -44,9 +44,7 @@ export function IssueDetailPanel({ issue, className }: IssueDetailPanelProps) {
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">{issue.assignee.name}</p>
|
||||
<p className="text-xs text-muted-foreground capitalize">
|
||||
{issue.assignee.type}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground capitalize">{issue.assignee.type}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -92,9 +90,7 @@ export function IssueDetailPanel({ issue, className }: IssueDetailPanelProps) {
|
||||
{issue.due_date && (
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Due Date</p>
|
||||
<p className="font-medium">
|
||||
{new Date(issue.due_date).toLocaleDateString()}
|
||||
</p>
|
||||
<p className="font-medium">{new Date(issue.due_date).toLocaleDateString()}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -136,19 +132,13 @@ export function IssueDetailPanel({ issue, className }: IssueDetailPanelProps) {
|
||||
<CardContent className="space-y-4">
|
||||
{issue.branch && (
|
||||
<div className="flex items-center gap-2">
|
||||
<GitBranch
|
||||
className="h-4 w-4 text-muted-foreground"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<GitBranch className="h-4 w-4 text-muted-foreground" aria-hidden="true" />
|
||||
<span className="font-mono text-sm">{issue.branch}</span>
|
||||
</div>
|
||||
)}
|
||||
{issue.pull_request && (
|
||||
<div className="flex items-center gap-2">
|
||||
<GitPullRequest
|
||||
className="h-4 w-4 text-muted-foreground"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<GitPullRequest className="h-4 w-4 text-muted-foreground" aria-hidden="true" />
|
||||
<span className="text-sm">{issue.pull_request}</span>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Open
|
||||
|
||||
@@ -136,10 +136,7 @@ export function IssueFilters({ filters, onFiltersChange, className }: IssueFilte
|
||||
<div className="grid gap-4 sm:grid-cols-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="priority-filter">Priority</Label>
|
||||
<Select
|
||||
value={filters.priority || 'all'}
|
||||
onValueChange={handlePriorityChange}
|
||||
>
|
||||
<Select value={filters.priority || 'all'} onValueChange={handlePriorityChange}>
|
||||
<SelectTrigger id="priority-filter">
|
||||
<SelectValue placeholder="All" />
|
||||
</SelectTrigger>
|
||||
@@ -172,10 +169,7 @@ export function IssueFilters({ filters, onFiltersChange, className }: IssueFilte
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="assignee-filter">Assignee</Label>
|
||||
<Select
|
||||
value={filters.assignee || 'all'}
|
||||
onValueChange={handleAssigneeChange}
|
||||
>
|
||||
<Select value={filters.assignee || 'all'} onValueChange={handleAssigneeChange}>
|
||||
<SelectTrigger id="assignee-filter">
|
||||
<SelectValue placeholder="All" />
|
||||
</SelectTrigger>
|
||||
|
||||
@@ -8,14 +8,7 @@
|
||||
* @module features/issues/components/StatusBadge
|
||||
*/
|
||||
|
||||
import {
|
||||
CircleDot,
|
||||
PlayCircle,
|
||||
Clock,
|
||||
AlertCircle,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
} from 'lucide-react';
|
||||
import { CircleDot, PlayCircle, Clock, AlertCircle, CheckCircle2, XCircle } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { IssueStatus } from '../types';
|
||||
import { STATUS_CONFIG } from '../constants';
|
||||
@@ -42,9 +35,7 @@ export function StatusBadge({ status, className, showLabel = true }: StatusBadge
|
||||
return (
|
||||
<div className={cn('flex items-center gap-1.5', config.color, className)}>
|
||||
<Icon className="h-4 w-4" aria-hidden="true" />
|
||||
{showLabel && (
|
||||
<span className="text-sm font-medium">{config.label}</span>
|
||||
)}
|
||||
{showLabel && <span className="text-sm font-medium">{config.label}</span>}
|
||||
<span className="sr-only">{config.label}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,14 +8,7 @@
|
||||
* @module features/issues/components/StatusWorkflow
|
||||
*/
|
||||
|
||||
import {
|
||||
CircleDot,
|
||||
PlayCircle,
|
||||
Clock,
|
||||
AlertCircle,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
} from 'lucide-react';
|
||||
import { CircleDot, PlayCircle, Clock, AlertCircle, CheckCircle2, XCircle } from 'lucide-react';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { IssueStatus } from '../types';
|
||||
@@ -63,18 +56,14 @@ export function StatusWorkflow({
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-2 rounded-lg p-2 text-left transition-colors',
|
||||
isActive
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'hover:bg-muted',
|
||||
isActive ? 'bg-primary/10 text-primary' : 'hover:bg-muted',
|
||||
disabled && 'cursor-not-allowed opacity-50'
|
||||
)}
|
||||
onClick={() => !disabled && onStatusChange(status)}
|
||||
>
|
||||
<Icon className={cn('h-4 w-4', config.color)} aria-hidden="true" />
|
||||
<span className="text-sm">{config.label}</span>
|
||||
{isActive && (
|
||||
<CheckCircle2 className="ml-auto h-4 w-4" aria-hidden="true" />
|
||||
)}
|
||||
{isActive && <CheckCircle2 className="ml-auto h-4 w-4" aria-hidden="true" />}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -18,8 +18,7 @@ export const mockIssues: IssueSummary[] = [
|
||||
number: 42,
|
||||
type: 'story',
|
||||
title: 'Implement user authentication flow',
|
||||
description:
|
||||
'Create complete authentication flow with login, register, and password reset.',
|
||||
description: 'Create complete authentication flow with login, register, and password reset.',
|
||||
status: 'in_progress',
|
||||
priority: 'high',
|
||||
labels: ['feature', 'auth', 'backend'],
|
||||
|
||||
@@ -44,12 +44,7 @@ const DEFAULT_PAGE_LIMIT = 20;
|
||||
export function useAgentTypes(params: AgentTypeListParams = {}) {
|
||||
const { user } = useAuth();
|
||||
|
||||
const {
|
||||
page = 1,
|
||||
limit = DEFAULT_PAGE_LIMIT,
|
||||
is_active = true,
|
||||
search,
|
||||
} = params;
|
||||
const { page = 1, limit = DEFAULT_PAGE_LIMIT, is_active = true, search } = params;
|
||||
|
||||
return useQuery({
|
||||
queryKey: agentTypeKeys.list({ page, limit, is_active, search }),
|
||||
@@ -152,10 +147,7 @@ export function useUpdateAgentType() {
|
||||
},
|
||||
onSuccess: (updatedAgentType) => {
|
||||
// Update the cache for this specific agent type
|
||||
queryClient.setQueryData(
|
||||
agentTypeKeys.detail(updatedAgentType.id),
|
||||
updatedAgentType
|
||||
);
|
||||
queryClient.setQueryData(agentTypeKeys.detail(updatedAgentType.id), updatedAgentType);
|
||||
// Invalidate lists to reflect changes
|
||||
queryClient.invalidateQueries({ queryKey: agentTypeKeys.lists() });
|
||||
},
|
||||
|
||||
@@ -5,4 +5,8 @@
|
||||
*/
|
||||
|
||||
export { useDebounce } from './useDebounce';
|
||||
export { useProjectEvents, type UseProjectEventsOptions, type UseProjectEventsResult } from './useProjectEvents';
|
||||
export {
|
||||
useProjectEvents,
|
||||
type UseProjectEventsOptions,
|
||||
type UseProjectEventsResult,
|
||||
} from './useProjectEvents';
|
||||
|
||||
@@ -385,7 +385,16 @@ export function useProjectEvents(
|
||||
mountedRef.current = false;
|
||||
cleanup();
|
||||
};
|
||||
}, [autoConnect, isAuthenticated, accessToken, projectId, connectionState, connect, disconnect, cleanup]);
|
||||
}, [
|
||||
autoConnect,
|
||||
isAuthenticated,
|
||||
accessToken,
|
||||
projectId,
|
||||
connectionState,
|
||||
connect,
|
||||
disconnect,
|
||||
cleanup,
|
||||
]);
|
||||
|
||||
return {
|
||||
events,
|
||||
|
||||
@@ -53,10 +53,7 @@ const modelParamsSchema = z.object({
|
||||
* Schema for agent type form fields
|
||||
*/
|
||||
export const agentTypeFormSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(1, 'Name is required')
|
||||
.max(255, 'Name must be less than 255 characters'),
|
||||
name: z.string().min(1, 'Name is required').max(255, 'Name must be less than 255 characters'),
|
||||
|
||||
slug: z
|
||||
.string()
|
||||
|
||||
Reference in New Issue
Block a user