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:
@@ -103,9 +103,11 @@ test.describe('Activity Feed Page', () => {
|
||||
|
||||
test('approval actions are visible for pending approvals', async ({ page }) => {
|
||||
// Find approval event
|
||||
const approvalEvent = page.locator('[data-testid^="event-item-"]', {
|
||||
has: page.getByText('Action Required'),
|
||||
}).first();
|
||||
const approvalEvent = page
|
||||
.locator('[data-testid^="event-item-"]', {
|
||||
has: page.getByText('Action Required'),
|
||||
})
|
||||
.first();
|
||||
|
||||
// Approval buttons should be visible
|
||||
await expect(approvalEvent.getByTestId('approve-button')).toBeVisible();
|
||||
|
||||
@@ -120,7 +120,10 @@ test.describe('Homepage - Hero Section', () => {
|
||||
test('should navigate to GitHub when clicking View on GitHub', async ({ page }) => {
|
||||
const githubLink = page.getByRole('link', { name: /View on GitHub/i }).first();
|
||||
await expect(githubLink).toBeVisible();
|
||||
await expect(githubLink).toHaveAttribute('href', expect.stringContaining('gitea.pragmazest.com'));
|
||||
await expect(githubLink).toHaveAttribute(
|
||||
'href',
|
||||
expect.stringContaining('gitea.pragmazest.com')
|
||||
);
|
||||
});
|
||||
|
||||
test('should navigate to components when clicking Explore Components', async ({ page }) => {
|
||||
|
||||
@@ -33,7 +33,9 @@ test.describe('Project Dashboard Page', () => {
|
||||
await expect(page.getByTestId('project-header')).toBeVisible();
|
||||
|
||||
// Check project name
|
||||
await expect(page.getByRole('heading', { level: 1 })).toContainText('E-Commerce Platform Redesign');
|
||||
await expect(page.getByRole('heading', { level: 1 })).toContainText(
|
||||
'E-Commerce Platform Redesign'
|
||||
);
|
||||
|
||||
// Check status badges
|
||||
await expect(page.getByText('In Progress')).toBeVisible();
|
||||
@@ -288,7 +290,9 @@ test.describe('Project Dashboard Activity Feed', () => {
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Look for action buttons in activity feed (if any require action)
|
||||
const reviewButton = page.getByTestId('recent-activity').getByRole('button', { name: /review/i });
|
||||
const reviewButton = page
|
||||
.getByTestId('recent-activity')
|
||||
.getByRole('button', { name: /review/i });
|
||||
const count = await reviewButton.count();
|
||||
|
||||
// Either there are action items or not - both are valid
|
||||
|
||||
@@ -7,42 +7,42 @@
|
||||
* - Please do NOT modify this file.
|
||||
*/
|
||||
|
||||
const PACKAGE_VERSION = '2.12.3'
|
||||
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
|
||||
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
||||
const activeClientIds = new Set()
|
||||
const PACKAGE_VERSION = '2.12.3';
|
||||
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82';
|
||||
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse');
|
||||
const activeClientIds = new Set();
|
||||
|
||||
addEventListener('install', function () {
|
||||
self.skipWaiting()
|
||||
})
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
addEventListener('activate', function (event) {
|
||||
event.waitUntil(self.clients.claim())
|
||||
})
|
||||
event.waitUntil(self.clients.claim());
|
||||
});
|
||||
|
||||
addEventListener('message', async function (event) {
|
||||
const clientId = Reflect.get(event.source || {}, 'id')
|
||||
const clientId = Reflect.get(event.source || {}, 'id');
|
||||
|
||||
if (!clientId || !self.clients) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const client = await self.clients.get(clientId)
|
||||
const client = await self.clients.get(clientId);
|
||||
|
||||
if (!client) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const allClients = await self.clients.matchAll({
|
||||
type: 'window',
|
||||
})
|
||||
});
|
||||
|
||||
switch (event.data) {
|
||||
case 'KEEPALIVE_REQUEST': {
|
||||
sendToClient(client, {
|
||||
type: 'KEEPALIVE_RESPONSE',
|
||||
})
|
||||
break
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'INTEGRITY_CHECK_REQUEST': {
|
||||
@@ -52,12 +52,12 @@ addEventListener('message', async function (event) {
|
||||
packageVersion: PACKAGE_VERSION,
|
||||
checksum: INTEGRITY_CHECKSUM,
|
||||
},
|
||||
})
|
||||
break
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'MOCK_ACTIVATE': {
|
||||
activeClientIds.add(clientId)
|
||||
activeClientIds.add(clientId);
|
||||
|
||||
sendToClient(client, {
|
||||
type: 'MOCKING_ENABLED',
|
||||
@@ -67,54 +67,51 @@ addEventListener('message', async function (event) {
|
||||
frameType: client.frameType,
|
||||
},
|
||||
},
|
||||
})
|
||||
break
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'CLIENT_CLOSED': {
|
||||
activeClientIds.delete(clientId)
|
||||
activeClientIds.delete(clientId);
|
||||
|
||||
const remainingClients = allClients.filter((client) => {
|
||||
return client.id !== clientId
|
||||
})
|
||||
return client.id !== clientId;
|
||||
});
|
||||
|
||||
// Unregister itself when there are no more clients
|
||||
if (remainingClients.length === 0) {
|
||||
self.registration.unregister()
|
||||
self.registration.unregister();
|
||||
}
|
||||
|
||||
break
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
addEventListener('fetch', function (event) {
|
||||
const requestInterceptedAt = Date.now()
|
||||
const requestInterceptedAt = Date.now();
|
||||
|
||||
// Bypass navigation requests.
|
||||
if (event.request.mode === 'navigate') {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
// Opening the DevTools triggers the "only-if-cached" request
|
||||
// that cannot be handled by the worker. Bypass such requests.
|
||||
if (
|
||||
event.request.cache === 'only-if-cached' &&
|
||||
event.request.mode !== 'same-origin'
|
||||
) {
|
||||
return
|
||||
if (event.request.cache === 'only-if-cached' && event.request.mode !== 'same-origin') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Bypass all requests when there are no active clients.
|
||||
// Prevents the self-unregistered worked from handling requests
|
||||
// after it's been terminated (still remains active until the next reload).
|
||||
if (activeClientIds.size === 0) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const requestId = crypto.randomUUID()
|
||||
event.respondWith(handleRequest(event, requestId, requestInterceptedAt))
|
||||
})
|
||||
const requestId = crypto.randomUUID();
|
||||
event.respondWith(handleRequest(event, requestId, requestInterceptedAt));
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {FetchEvent} event
|
||||
@@ -122,23 +119,18 @@ addEventListener('fetch', function (event) {
|
||||
* @param {number} requestInterceptedAt
|
||||
*/
|
||||
async function handleRequest(event, requestId, requestInterceptedAt) {
|
||||
const client = await resolveMainClient(event)
|
||||
const requestCloneForEvents = event.request.clone()
|
||||
const response = await getResponse(
|
||||
event,
|
||||
client,
|
||||
requestId,
|
||||
requestInterceptedAt,
|
||||
)
|
||||
const client = await resolveMainClient(event);
|
||||
const requestCloneForEvents = event.request.clone();
|
||||
const response = await getResponse(event, client, requestId, requestInterceptedAt);
|
||||
|
||||
// Send back the response clone for the "response:*" life-cycle events.
|
||||
// Ensure MSW is active and ready to handle the message, otherwise
|
||||
// this message will pend indefinitely.
|
||||
if (client && activeClientIds.has(client.id)) {
|
||||
const serializedRequest = await serializeRequest(requestCloneForEvents)
|
||||
const serializedRequest = await serializeRequest(requestCloneForEvents);
|
||||
|
||||
// Clone the response so both the client and the library could consume it.
|
||||
const responseClone = response.clone()
|
||||
const responseClone = response.clone();
|
||||
|
||||
sendToClient(
|
||||
client,
|
||||
@@ -159,11 +151,11 @@ async function handleRequest(event, requestId, requestInterceptedAt) {
|
||||
},
|
||||
},
|
||||
},
|
||||
responseClone.body ? [serializedRequest.body, responseClone.body] : [],
|
||||
)
|
||||
responseClone.body ? [serializedRequest.body, responseClone.body] : []
|
||||
);
|
||||
}
|
||||
|
||||
return response
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -175,30 +167,30 @@ async function handleRequest(event, requestId, requestInterceptedAt) {
|
||||
* @returns {Promise<Client | undefined>}
|
||||
*/
|
||||
async function resolveMainClient(event) {
|
||||
const client = await self.clients.get(event.clientId)
|
||||
const client = await self.clients.get(event.clientId);
|
||||
|
||||
if (activeClientIds.has(event.clientId)) {
|
||||
return client
|
||||
return client;
|
||||
}
|
||||
|
||||
if (client?.frameType === 'top-level') {
|
||||
return client
|
||||
return client;
|
||||
}
|
||||
|
||||
const allClients = await self.clients.matchAll({
|
||||
type: 'window',
|
||||
})
|
||||
});
|
||||
|
||||
return allClients
|
||||
.filter((client) => {
|
||||
// Get only those clients that are currently visible.
|
||||
return client.visibilityState === 'visible'
|
||||
return client.visibilityState === 'visible';
|
||||
})
|
||||
.find((client) => {
|
||||
// Find the client ID that's recorded in the
|
||||
// set of clients that have registered the worker.
|
||||
return activeClientIds.has(client.id)
|
||||
})
|
||||
return activeClientIds.has(client.id);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -211,36 +203,34 @@ async function resolveMainClient(event) {
|
||||
async function getResponse(event, client, requestId, requestInterceptedAt) {
|
||||
// Clone the request because it might've been already used
|
||||
// (i.e. its body has been read and sent to the client).
|
||||
const requestClone = event.request.clone()
|
||||
const requestClone = event.request.clone();
|
||||
|
||||
function passthrough() {
|
||||
// Cast the request headers to a new Headers instance
|
||||
// so the headers can be manipulated with.
|
||||
const headers = new Headers(requestClone.headers)
|
||||
const headers = new Headers(requestClone.headers);
|
||||
|
||||
// Remove the "accept" header value that marked this request as passthrough.
|
||||
// This prevents request alteration and also keeps it compliant with the
|
||||
// user-defined CORS policies.
|
||||
const acceptHeader = headers.get('accept')
|
||||
const acceptHeader = headers.get('accept');
|
||||
if (acceptHeader) {
|
||||
const values = acceptHeader.split(',').map((value) => value.trim())
|
||||
const filteredValues = values.filter(
|
||||
(value) => value !== 'msw/passthrough',
|
||||
)
|
||||
const values = acceptHeader.split(',').map((value) => value.trim());
|
||||
const filteredValues = values.filter((value) => value !== 'msw/passthrough');
|
||||
|
||||
if (filteredValues.length > 0) {
|
||||
headers.set('accept', filteredValues.join(', '))
|
||||
headers.set('accept', filteredValues.join(', '));
|
||||
} else {
|
||||
headers.delete('accept')
|
||||
headers.delete('accept');
|
||||
}
|
||||
}
|
||||
|
||||
return fetch(requestClone, { headers })
|
||||
return fetch(requestClone, { headers });
|
||||
}
|
||||
|
||||
// Bypass mocking when the client is not active.
|
||||
if (!client) {
|
||||
return passthrough()
|
||||
return passthrough();
|
||||
}
|
||||
|
||||
// Bypass initial page load requests (i.e. static assets).
|
||||
@@ -248,11 +238,11 @@ async function getResponse(event, client, requestId, requestInterceptedAt) {
|
||||
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
|
||||
// and is not ready to handle requests.
|
||||
if (!activeClientIds.has(client.id)) {
|
||||
return passthrough()
|
||||
return passthrough();
|
||||
}
|
||||
|
||||
// Notify the client that a request has been intercepted.
|
||||
const serializedRequest = await serializeRequest(event.request)
|
||||
const serializedRequest = await serializeRequest(event.request);
|
||||
const clientMessage = await sendToClient(
|
||||
client,
|
||||
{
|
||||
@@ -263,20 +253,20 @@ async function getResponse(event, client, requestId, requestInterceptedAt) {
|
||||
...serializedRequest,
|
||||
},
|
||||
},
|
||||
[serializedRequest.body],
|
||||
)
|
||||
[serializedRequest.body]
|
||||
);
|
||||
|
||||
switch (clientMessage.type) {
|
||||
case 'MOCK_RESPONSE': {
|
||||
return respondWithMock(clientMessage.data)
|
||||
return respondWithMock(clientMessage.data);
|
||||
}
|
||||
|
||||
case 'PASSTHROUGH': {
|
||||
return passthrough()
|
||||
return passthrough();
|
||||
}
|
||||
}
|
||||
|
||||
return passthrough()
|
||||
return passthrough();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -287,21 +277,18 @@ async function getResponse(event, client, requestId, requestInterceptedAt) {
|
||||
*/
|
||||
function sendToClient(client, message, transferrables = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const channel = new MessageChannel()
|
||||
const channel = new MessageChannel();
|
||||
|
||||
channel.port1.onmessage = (event) => {
|
||||
if (event.data && event.data.error) {
|
||||
return reject(event.data.error)
|
||||
return reject(event.data.error);
|
||||
}
|
||||
|
||||
resolve(event.data)
|
||||
}
|
||||
resolve(event.data);
|
||||
};
|
||||
|
||||
client.postMessage(message, [
|
||||
channel.port2,
|
||||
...transferrables.filter(Boolean),
|
||||
])
|
||||
})
|
||||
client.postMessage(message, [channel.port2, ...transferrables.filter(Boolean)]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -314,17 +301,17 @@ function respondWithMock(response) {
|
||||
// instance will have status code set to 0. Since it's not possible to create
|
||||
// a Response instance with status code 0, handle that use-case separately.
|
||||
if (response.status === 0) {
|
||||
return Response.error()
|
||||
return Response.error();
|
||||
}
|
||||
|
||||
const mockedResponse = new Response(response.body, response)
|
||||
const mockedResponse = new Response(response.body, response);
|
||||
|
||||
Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
|
||||
value: true,
|
||||
enumerable: true,
|
||||
})
|
||||
});
|
||||
|
||||
return mockedResponse
|
||||
return mockedResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -345,5 +332,5 @@ async function serializeRequest(request) {
|
||||
referrerPolicy: request.referrerPolicy,
|
||||
body: await request.arrayBuffer(),
|
||||
keepalive: request.keepalive,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -245,7 +245,10 @@ describe('HomePage', () => {
|
||||
const githubLinks = screen.getAllByRole('link', { name: /GitHub/i });
|
||||
expect(githubLinks.length).toBeGreaterThan(0);
|
||||
// Syndarix uses Gitea for version control
|
||||
expect(githubLinks[0]).toHaveAttribute('href', expect.stringContaining('gitea.pragmazest.com'));
|
||||
expect(githubLinks[0]).toHaveAttribute(
|
||||
'href',
|
||||
expect.stringContaining('gitea.pragmazest.com')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -356,9 +356,7 @@ describe('ActivityFeed', () => {
|
||||
await user.click(within(eventItem).getByTestId('approve-button'));
|
||||
|
||||
expect(onApprove).toHaveBeenCalledTimes(1);
|
||||
expect(onApprove).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ id: 'event-001' })
|
||||
);
|
||||
expect(onApprove).toHaveBeenCalledWith(expect.objectContaining({ id: 'event-001' }));
|
||||
});
|
||||
|
||||
it('calls onReject when reject button clicked', async () => {
|
||||
@@ -370,9 +368,7 @@ describe('ActivityFeed', () => {
|
||||
await user.click(within(eventItem).getByTestId('reject-button'));
|
||||
|
||||
expect(onReject).toHaveBeenCalledTimes(1);
|
||||
expect(onReject).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ id: 'event-001' })
|
||||
);
|
||||
expect(onReject).toHaveBeenCalledWith(expect.objectContaining({ id: 'event-001' }));
|
||||
});
|
||||
|
||||
it('shows pending count badge', () => {
|
||||
@@ -440,9 +436,7 @@ describe('ActivityFeed', () => {
|
||||
await user.click(screen.getByTestId('event-item-event-001'));
|
||||
|
||||
expect(onEventClick).toHaveBeenCalledTimes(1);
|
||||
expect(onEventClick).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ id: 'event-001' })
|
||||
);
|
||||
expect(onEventClick).toHaveBeenCalledWith(expect.objectContaining({ id: 'event-001' }));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -459,7 +453,9 @@ describe('ActivityFeed', () => {
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('has proper ARIA labels for interactive elements', () => {
|
||||
render(<ActivityFeed {...defaultProps} onReconnect={jest.fn()} connectionState="disconnected" />);
|
||||
render(
|
||||
<ActivityFeed {...defaultProps} onReconnect={jest.fn()} connectionState="disconnected" />
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText('Reconnect')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -160,9 +160,7 @@ describe('AgentTypeDetail', () => {
|
||||
it('shows not found state when agentType is null', () => {
|
||||
render(<AgentTypeDetail {...defaultProps} agentType={null} isLoading={false} />);
|
||||
expect(screen.getByText('Agent type not found')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('The requested agent type could not be found')
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('The requested agent type could not be found')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows danger zone with deactivate button', () => {
|
||||
@@ -191,9 +189,7 @@ describe('AgentTypeDetail', () => {
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(
|
||||
<AgentTypeDetail {...defaultProps} className="custom-class" />
|
||||
);
|
||||
const { container } = render(<AgentTypeDetail {...defaultProps} className="custom-class" />);
|
||||
expect(container.firstChild).toHaveClass('custom-class');
|
||||
});
|
||||
|
||||
@@ -205,18 +201,13 @@ describe('AgentTypeDetail', () => {
|
||||
});
|
||||
|
||||
it('shows no expertise message when expertise is empty', () => {
|
||||
render(
|
||||
<AgentTypeDetail {...defaultProps} agentType={{ ...mockAgentType, expertise: [] }} />
|
||||
);
|
||||
render(<AgentTypeDetail {...defaultProps} agentType={{ ...mockAgentType, expertise: [] }} />);
|
||||
expect(screen.getByText('No expertise areas defined')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "None configured" when no fallback model', () => {
|
||||
render(
|
||||
<AgentTypeDetail
|
||||
{...defaultProps}
|
||||
agentType={{ ...mockAgentType, fallback_models: [] }}
|
||||
/>
|
||||
<AgentTypeDetail {...defaultProps} agentType={{ ...mockAgentType, fallback_models: [] }} />
|
||||
);
|
||||
expect(screen.getByText('None configured')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -36,9 +36,7 @@ describe('AgentTypeForm', () => {
|
||||
it('renders create form title', () => {
|
||||
render(<AgentTypeForm {...defaultProps} />);
|
||||
expect(screen.getByText('Create Agent Type')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Define a new agent type template')
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('Define a new agent type template')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders all tabs', () => {
|
||||
@@ -233,9 +231,7 @@ describe('AgentTypeForm', () => {
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(
|
||||
<AgentTypeForm {...defaultProps} className="custom-class" />
|
||||
);
|
||||
const { container } = render(<AgentTypeForm {...defaultProps} className="custom-class" />);
|
||||
expect(container.querySelector('form')).toHaveClass('custom-class');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -143,15 +143,11 @@ describe('AgentTypeList', () => {
|
||||
it('shows empty state when no agent types', () => {
|
||||
render(<AgentTypeList {...defaultProps} agentTypes={[]} />);
|
||||
expect(screen.getByText('No agent types found')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Create your first agent type to get started')
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('Create your first agent type to get started')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows filter hint in empty state when filters are applied', () => {
|
||||
render(
|
||||
<AgentTypeList {...defaultProps} agentTypes={[]} searchQuery="nonexistent" />
|
||||
);
|
||||
render(<AgentTypeList {...defaultProps} agentTypes={[]} searchQuery="nonexistent" />);
|
||||
expect(screen.getByText('Try adjusting your search or filters')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -175,9 +171,7 @@ describe('AgentTypeList', () => {
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(
|
||||
<AgentTypeList {...defaultProps} className="custom-class" />
|
||||
);
|
||||
const { container } = render(<AgentTypeList {...defaultProps} className="custom-class" />);
|
||||
expect(container.firstChild).toHaveClass('custom-class');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -182,9 +182,7 @@ describe('ConnectionStatus', () => {
|
||||
|
||||
describe('className prop', () => {
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(
|
||||
<ConnectionStatus state="connected" className="custom-class" />
|
||||
);
|
||||
const { container } = render(<ConnectionStatus state="connected" className="custom-class" />);
|
||||
|
||||
expect(container.querySelector('.custom-class')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -71,7 +71,10 @@ describe('CTASection', () => {
|
||||
);
|
||||
|
||||
const githubLink = screen.getByRole('link', { name: /get started on github/i });
|
||||
expect(githubLink).toHaveAttribute('href', 'https://gitea.pragmazest.com/cardosofelipe/syndarix');
|
||||
expect(githubLink).toHaveAttribute(
|
||||
'href',
|
||||
'https://gitea.pragmazest.com/cardosofelipe/syndarix'
|
||||
);
|
||||
expect(githubLink).toHaveAttribute('target', '_blank');
|
||||
expect(githubLink).toHaveAttribute('rel', 'noopener noreferrer');
|
||||
});
|
||||
|
||||
@@ -221,7 +221,10 @@ describe('Header', () => {
|
||||
const mobileGithubLink = githubLinks[1];
|
||||
fireEvent.click(mobileGithubLink);
|
||||
// Syndarix uses Gitea for version control
|
||||
expect(mobileGithubLink).toHaveAttribute('href', expect.stringContaining('gitea.pragmazest.com'));
|
||||
expect(mobileGithubLink).toHaveAttribute(
|
||||
'href',
|
||||
expect.stringContaining('gitea.pragmazest.com')
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -100,7 +100,10 @@ describe('HeroSection', () => {
|
||||
);
|
||||
|
||||
const githubLink = screen.getByRole('link', { name: /view on github/i });
|
||||
expect(githubLink).toHaveAttribute('href', 'https://gitea.pragmazest.com/cardosofelipe/syndarix');
|
||||
expect(githubLink).toHaveAttribute(
|
||||
'href',
|
||||
'https://gitea.pragmazest.com/cardosofelipe/syndarix'
|
||||
);
|
||||
expect(githubLink).toHaveAttribute('target', '_blank');
|
||||
expect(githubLink).toHaveAttribute('rel', 'noopener noreferrer');
|
||||
});
|
||||
|
||||
@@ -123,12 +123,7 @@ describe('AppHeader', () => {
|
||||
});
|
||||
|
||||
it('displays current project name', () => {
|
||||
render(
|
||||
<AppHeader
|
||||
projects={mockProjects}
|
||||
currentProject={mockProjects[0]}
|
||||
/>
|
||||
);
|
||||
render(<AppHeader projects={mockProjects} currentProject={mockProjects[0]} />);
|
||||
|
||||
// Multiple instances may show the project name
|
||||
expect(screen.getAllByText('Project One').length).toBeGreaterThan(0);
|
||||
@@ -137,12 +132,7 @@ describe('AppHeader', () => {
|
||||
it('calls onProjectChange when project is changed', async () => {
|
||||
const mockOnChange = jest.fn();
|
||||
|
||||
render(
|
||||
<AppHeader
|
||||
projects={mockProjects}
|
||||
onProjectChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
render(<AppHeader projects={mockProjects} onProjectChange={mockOnChange} />);
|
||||
|
||||
// The actual test of project switching is in ProjectSwitcher.test.tsx
|
||||
// Here we just verify the prop is passed by checking switcher exists
|
||||
|
||||
@@ -145,9 +145,7 @@ describe('AppLayout', () => {
|
||||
});
|
||||
|
||||
it('passes custom breadcrumbs to AppBreadcrumbs', () => {
|
||||
const customBreadcrumbs = [
|
||||
{ label: 'Custom', href: '/custom', current: true },
|
||||
];
|
||||
const customBreadcrumbs = [{ label: 'Custom', href: '/custom', current: true }];
|
||||
|
||||
render(
|
||||
<AppLayout breadcrumbs={customBreadcrumbs}>
|
||||
@@ -344,10 +342,7 @@ describe('PageHeader', () => {
|
||||
|
||||
it('renders actions when provided', () => {
|
||||
render(
|
||||
<PageHeader
|
||||
title="Title"
|
||||
actions={<button data-testid="action-button">Action</button>}
|
||||
/>
|
||||
<PageHeader title="Title" actions={<button data-testid="action-button">Action</button>} />
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('action-button')).toBeInTheDocument();
|
||||
|
||||
@@ -45,12 +45,7 @@ describe('ProjectSwitcher', () => {
|
||||
});
|
||||
|
||||
it('displays current project name', () => {
|
||||
render(
|
||||
<ProjectSwitcher
|
||||
projects={mockProjects}
|
||||
currentProject={mockProjects[0]}
|
||||
/>
|
||||
);
|
||||
render(<ProjectSwitcher projects={mockProjects} currentProject={mockProjects[0]} />);
|
||||
|
||||
expect(screen.getByText('Project One')).toBeInTheDocument();
|
||||
});
|
||||
@@ -94,12 +89,7 @@ describe('ProjectSwitcher', () => {
|
||||
it('shows current indicator on selected project', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<ProjectSwitcher
|
||||
projects={mockProjects}
|
||||
currentProject={mockProjects[0]}
|
||||
/>
|
||||
);
|
||||
render(<ProjectSwitcher projects={mockProjects} currentProject={mockProjects[0]} />);
|
||||
|
||||
const trigger = screen.getByTestId('project-switcher-trigger');
|
||||
await user.click(trigger);
|
||||
@@ -144,12 +134,7 @@ describe('ProjectSwitcher', () => {
|
||||
const user = userEvent.setup();
|
||||
const mockOnChange = jest.fn();
|
||||
|
||||
render(
|
||||
<ProjectSwitcher
|
||||
projects={mockProjects}
|
||||
onProjectChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
render(<ProjectSwitcher projects={mockProjects} onProjectChange={mockOnChange} />);
|
||||
|
||||
const trigger = screen.getByTestId('project-switcher-trigger');
|
||||
await user.click(trigger);
|
||||
@@ -189,18 +174,10 @@ describe('ProjectSwitcher', () => {
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('has accessible label on trigger', () => {
|
||||
render(
|
||||
<ProjectSwitcher
|
||||
projects={mockProjects}
|
||||
currentProject={mockProjects[0]}
|
||||
/>
|
||||
);
|
||||
render(<ProjectSwitcher projects={mockProjects} currentProject={mockProjects[0]} />);
|
||||
|
||||
const trigger = screen.getByTestId('project-switcher-trigger');
|
||||
expect(trigger).toHaveAttribute(
|
||||
'aria-label',
|
||||
'Switch project, current: Project One'
|
||||
);
|
||||
expect(trigger).toHaveAttribute('aria-label', 'Switch project, current: Project One');
|
||||
});
|
||||
|
||||
it('has accessible label when no current project', () => {
|
||||
@@ -220,12 +197,7 @@ describe('ProjectSelect', () => {
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('renders select component', () => {
|
||||
render(
|
||||
<ProjectSelect
|
||||
projects={mockProjects}
|
||||
onValueChange={jest.fn()}
|
||||
/>
|
||||
);
|
||||
render(<ProjectSelect projects={mockProjects} onValueChange={jest.fn()} />);
|
||||
|
||||
expect(screen.getByTestId('project-select')).toBeInTheDocument();
|
||||
});
|
||||
@@ -243,23 +215,14 @@ describe('ProjectSelect', () => {
|
||||
});
|
||||
|
||||
it('has combobox role', () => {
|
||||
render(
|
||||
<ProjectSelect
|
||||
projects={mockProjects}
|
||||
onValueChange={jest.fn()}
|
||||
/>
|
||||
);
|
||||
render(<ProjectSelect projects={mockProjects} onValueChange={jest.fn()} />);
|
||||
|
||||
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
render(
|
||||
<ProjectSelect
|
||||
projects={mockProjects}
|
||||
onValueChange={jest.fn()}
|
||||
className="custom-class"
|
||||
/>
|
||||
<ProjectSelect projects={mockProjects} onValueChange={jest.fn()} className="custom-class" />
|
||||
);
|
||||
|
||||
const select = screen.getByTestId('project-select');
|
||||
|
||||
@@ -74,7 +74,12 @@ const mockUseProjectEventsDefault = {
|
||||
events: [] as ProjectEvent[],
|
||||
isConnected: true,
|
||||
connectionState: 'connected' as ConnectionState,
|
||||
error: null as { message: string; timestamp: string; code?: string; retryAttempt?: number } | null,
|
||||
error: null as {
|
||||
message: string;
|
||||
timestamp: string;
|
||||
code?: string;
|
||||
retryAttempt?: number;
|
||||
} | null,
|
||||
retryCount: 0,
|
||||
reconnect: mockReconnect,
|
||||
disconnect: mockDisconnect,
|
||||
@@ -389,11 +394,7 @@ describe('Event to Activity Conversion', () => {
|
||||
});
|
||||
|
||||
it('handles system actor type', () => {
|
||||
const event = createMockEvent(
|
||||
EventType.SPRINT_STARTED,
|
||||
{ sprint_name: 'Sprint 5' },
|
||||
'system'
|
||||
);
|
||||
const event = createMockEvent(EventType.SPRINT_STARTED, { sprint_name: 'Sprint 5' }, 'system');
|
||||
mockUseProjectEventsResult.events = [event];
|
||||
render(<ProjectDashboard projectId="test" />);
|
||||
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();
|
||||
|
||||
@@ -46,13 +46,7 @@ describe('ProjectHeader', () => {
|
||||
|
||||
it('shows pause button when canPause is true and project is active', () => {
|
||||
const onPauseProject = jest.fn();
|
||||
render(
|
||||
<ProjectHeader
|
||||
project={mockProject}
|
||||
canPause={true}
|
||||
onPauseProject={onPauseProject}
|
||||
/>
|
||||
);
|
||||
render(<ProjectHeader project={mockProject} canPause={true} onPauseProject={onPauseProject} />);
|
||||
expect(screen.getByRole('button', { name: /pause project/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -64,13 +58,7 @@ describe('ProjectHeader', () => {
|
||||
|
||||
it('shows run sprint button when canStart is true', () => {
|
||||
const onStartSprint = jest.fn();
|
||||
render(
|
||||
<ProjectHeader
|
||||
project={mockProject}
|
||||
canStart={true}
|
||||
onStartSprint={onStartSprint}
|
||||
/>
|
||||
);
|
||||
render(<ProjectHeader project={mockProject} canStart={true} onStartSprint={onStartSprint} />);
|
||||
expect(screen.getByRole('button', { name: /run sprint/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -83,13 +71,7 @@ describe('ProjectHeader', () => {
|
||||
it('calls onStartSprint when run sprint button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onStartSprint = jest.fn();
|
||||
render(
|
||||
<ProjectHeader
|
||||
project={mockProject}
|
||||
canStart={true}
|
||||
onStartSprint={onStartSprint}
|
||||
/>
|
||||
);
|
||||
render(<ProjectHeader project={mockProject} canStart={true} onStartSprint={onStartSprint} />);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /run sprint/i }));
|
||||
expect(onStartSprint).toHaveBeenCalledTimes(1);
|
||||
@@ -98,13 +80,7 @@ describe('ProjectHeader', () => {
|
||||
it('calls onPauseProject when pause button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onPauseProject = jest.fn();
|
||||
render(
|
||||
<ProjectHeader
|
||||
project={mockProject}
|
||||
canPause={true}
|
||||
onPauseProject={onPauseProject}
|
||||
/>
|
||||
);
|
||||
render(<ProjectHeader project={mockProject} canPause={true} onPauseProject={onPauseProject} />);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /pause project/i }));
|
||||
expect(onPauseProject).toHaveBeenCalledTimes(1);
|
||||
@@ -113,12 +89,7 @@ describe('ProjectHeader', () => {
|
||||
it('calls onCreateSprint when new sprint button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onCreateSprint = jest.fn();
|
||||
render(
|
||||
<ProjectHeader
|
||||
project={mockProject}
|
||||
onCreateSprint={onCreateSprint}
|
||||
/>
|
||||
);
|
||||
render(<ProjectHeader project={mockProject} onCreateSprint={onCreateSprint} />);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /new sprint/i }));
|
||||
expect(onCreateSprint).toHaveBeenCalledTimes(1);
|
||||
@@ -127,12 +98,7 @@ describe('ProjectHeader', () => {
|
||||
it('calls onSettings when settings button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSettings = jest.fn();
|
||||
render(
|
||||
<ProjectHeader
|
||||
project={mockProject}
|
||||
onSettings={onSettings}
|
||||
/>
|
||||
);
|
||||
render(<ProjectHeader project={mockProject} onSettings={onSettings} />);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /project settings/i }));
|
||||
expect(onSettings).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -65,38 +65,20 @@ describe('RecentActivity', () => {
|
||||
|
||||
it('shows View All button when there are more activities than maxItems', () => {
|
||||
const onViewAll = jest.fn();
|
||||
render(
|
||||
<RecentActivity
|
||||
activities={mockActivities}
|
||||
maxItems={2}
|
||||
onViewAll={onViewAll}
|
||||
/>
|
||||
);
|
||||
render(<RecentActivity activities={mockActivities} maxItems={2} onViewAll={onViewAll} />);
|
||||
expect(screen.getByRole('button', { name: /view all/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show View All button when all activities are shown', () => {
|
||||
const onViewAll = jest.fn();
|
||||
render(
|
||||
<RecentActivity
|
||||
activities={mockActivities}
|
||||
maxItems={5}
|
||||
onViewAll={onViewAll}
|
||||
/>
|
||||
);
|
||||
render(<RecentActivity activities={mockActivities} maxItems={5} onViewAll={onViewAll} />);
|
||||
expect(screen.queryByRole('button', { name: /view all/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onViewAll when View All button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onViewAll = jest.fn();
|
||||
render(
|
||||
<RecentActivity
|
||||
activities={mockActivities}
|
||||
maxItems={2}
|
||||
onViewAll={onViewAll}
|
||||
/>
|
||||
);
|
||||
render(<RecentActivity activities={mockActivities} maxItems={2} onViewAll={onViewAll} />);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /view all/i }));
|
||||
expect(onViewAll).toHaveBeenCalledTimes(1);
|
||||
@@ -104,24 +86,14 @@ describe('RecentActivity', () => {
|
||||
|
||||
it('shows Review Request button for items requiring action', () => {
|
||||
const onActionClick = jest.fn();
|
||||
render(
|
||||
<RecentActivity
|
||||
activities={mockActivities}
|
||||
onActionClick={onActionClick}
|
||||
/>
|
||||
);
|
||||
render(<RecentActivity activities={mockActivities} onActionClick={onActionClick} />);
|
||||
expect(screen.getByRole('button', { name: /review request/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onActionClick when Review Request button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onActionClick = jest.fn();
|
||||
render(
|
||||
<RecentActivity
|
||||
activities={mockActivities}
|
||||
onActionClick={onActionClick}
|
||||
/>
|
||||
);
|
||||
render(<RecentActivity activities={mockActivities} onActionClick={onActionClick} />);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /review request/i }));
|
||||
expect(onActionClick).toHaveBeenCalledWith('act-003');
|
||||
|
||||
@@ -23,9 +23,7 @@ describe('ProjectStatusBadge', () => {
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(
|
||||
<ProjectStatusBadge status="active" className="custom-class" />
|
||||
);
|
||||
const { container } = render(<ProjectStatusBadge status="active" className="custom-class" />);
|
||||
expect(container.firstChild).toHaveClass('custom-class');
|
||||
});
|
||||
});
|
||||
@@ -54,9 +52,7 @@ describe('AutonomyBadge', () => {
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(
|
||||
<AutonomyBadge level="milestone" className="custom-class" />
|
||||
);
|
||||
const { container } = render(<AutonomyBadge level="milestone" className="custom-class" />);
|
||||
expect(container.firstChild).toHaveClass('custom-class');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -264,7 +264,9 @@ describe('ProjectWizard', () => {
|
||||
await user.click(screen.getByRole('button', { name: /create project/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /go to project dashboard/i })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('button', { name: /go to project dashboard/i })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /go to project dashboard/i }));
|
||||
|
||||
@@ -60,7 +60,9 @@ describe('AutonomyStep', () => {
|
||||
|
||||
it('has accessible radiogroup role', () => {
|
||||
render(<AutonomyStep state={defaultState} updateState={mockUpdateState} />);
|
||||
expect(screen.getByRole('radiogroup', { name: /autonomy level options/i })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('radiogroup', { name: /autonomy level options/i })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -69,7 +71,9 @@ describe('AutonomyStep', () => {
|
||||
const user = userEvent.setup();
|
||||
render(<AutonomyStep state={defaultState} updateState={mockUpdateState} />);
|
||||
|
||||
const fullControlOption = screen.getByRole('button', { name: /full control.*review every action/i });
|
||||
const fullControlOption = screen.getByRole('button', {
|
||||
name: /full control.*review every action/i,
|
||||
});
|
||||
await user.click(fullControlOption);
|
||||
|
||||
expect(mockUpdateState).toHaveBeenCalledWith({ autonomyLevel: 'full_control' });
|
||||
@@ -89,7 +93,9 @@ describe('AutonomyStep', () => {
|
||||
const user = userEvent.setup();
|
||||
render(<AutonomyStep state={defaultState} updateState={mockUpdateState} />);
|
||||
|
||||
const autonomousOption = screen.getByRole('button', { name: /autonomous.*only major decisions/i });
|
||||
const autonomousOption = screen.getByRole('button', {
|
||||
name: /autonomous.*only major decisions/i,
|
||||
});
|
||||
await user.click(autonomousOption);
|
||||
|
||||
expect(mockUpdateState).toHaveBeenCalledWith({ autonomyLevel: 'autonomous' });
|
||||
@@ -180,7 +186,7 @@ describe('AutonomyStep', () => {
|
||||
|
||||
autonomyOptions.forEach((option) => {
|
||||
const button = screen.getByRole('button', {
|
||||
name: new RegExp(`${option.label}.*${option.description}`, 'i')
|
||||
name: new RegExp(`${option.label}.*${option.description}`, 'i'),
|
||||
});
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
@@ -207,7 +213,9 @@ describe('AutonomyStep', () => {
|
||||
};
|
||||
render(<AutonomyStep state={stateWithFullControl} updateState={mockUpdateState} />);
|
||||
|
||||
const autonomousOption = screen.getByRole('button', { name: /autonomous.*only major decisions/i });
|
||||
const autonomousOption = screen.getByRole('button', {
|
||||
name: /autonomous.*only major decisions/i,
|
||||
});
|
||||
await user.click(autonomousOption);
|
||||
|
||||
expect(mockUpdateState).toHaveBeenCalledWith({ autonomyLevel: 'autonomous' });
|
||||
|
||||
@@ -59,7 +59,9 @@ describe('ClientModeStep', () => {
|
||||
|
||||
it('has accessible radiogroup role', () => {
|
||||
render(<ClientModeStep state={defaultState} updateState={mockUpdateState} />);
|
||||
expect(screen.getByRole('radiogroup', { name: /client interaction mode options/i })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('radiogroup', { name: /client interaction mode options/i })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -76,7 +78,9 @@ describe('ClientModeStep', () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ClientModeStep state={defaultState} updateState={mockUpdateState} />);
|
||||
|
||||
const technicalOption = screen.getByRole('button', { name: /technical mode.*detailed technical/i });
|
||||
const technicalOption = screen.getByRole('button', {
|
||||
name: /technical mode.*detailed technical/i,
|
||||
});
|
||||
await user.click(technicalOption);
|
||||
|
||||
expect(mockUpdateState).toHaveBeenCalledWith({ clientMode: 'technical' });
|
||||
@@ -123,7 +127,7 @@ describe('ClientModeStep', () => {
|
||||
|
||||
clientModeOptions.forEach((option) => {
|
||||
const button = screen.getByRole('button', {
|
||||
name: new RegExp(`${option.label}.*${option.description}`, 'i')
|
||||
name: new RegExp(`${option.label}.*${option.description}`, 'i'),
|
||||
});
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
@@ -168,7 +172,9 @@ describe('ClientModeStep', () => {
|
||||
};
|
||||
render(<ClientModeStep state={stateWithTechnical} updateState={mockUpdateState} />);
|
||||
|
||||
const technicalOption = screen.getByRole('button', { name: /technical mode.*detailed technical/i });
|
||||
const technicalOption = screen.getByRole('button', {
|
||||
name: /technical mode.*detailed technical/i,
|
||||
});
|
||||
await user.click(technicalOption);
|
||||
|
||||
// Should still call updateState
|
||||
|
||||
@@ -65,7 +65,9 @@ describe('ComplexityStep', () => {
|
||||
|
||||
it('has accessible radiogroup role', () => {
|
||||
render(<ComplexityStep state={defaultState} updateState={mockUpdateState} />);
|
||||
expect(screen.getByRole('radiogroup', { name: /project complexity options/i })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('radiogroup', { name: /project complexity options/i })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -164,7 +166,7 @@ describe('ComplexityStep', () => {
|
||||
|
||||
complexityOptions.forEach((option) => {
|
||||
const button = screen.getByRole('button', {
|
||||
name: new RegExp(`${option.label}.*${option.description}`, 'i')
|
||||
name: new RegExp(`${option.label}.*${option.description}`, 'i'),
|
||||
});
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -211,9 +211,7 @@ describe('IssueDetailPanel', () => {
|
||||
it('renders labels without color property', () => {
|
||||
const issueWithColorlessLabels: IssueDetail = {
|
||||
...defaultIssue,
|
||||
labels: [
|
||||
{ id: 'lbl-1', name: 'colorless-label' },
|
||||
],
|
||||
labels: [{ id: 'lbl-1', name: 'colorless-label' }],
|
||||
};
|
||||
render(<IssueDetailPanel issue={issueWithColorlessLabels} />);
|
||||
expect(screen.getByText('colorless-label')).toBeInTheDocument();
|
||||
|
||||
@@ -14,9 +14,7 @@ describe('StatusWorkflow', () => {
|
||||
});
|
||||
|
||||
it('renders all status options', () => {
|
||||
render(
|
||||
<StatusWorkflow currentStatus="open" onStatusChange={mockOnStatusChange} />
|
||||
);
|
||||
render(<StatusWorkflow currentStatus="open" onStatusChange={mockOnStatusChange} />);
|
||||
|
||||
expect(screen.getByText('Open')).toBeInTheDocument();
|
||||
expect(screen.getByText('In Progress')).toBeInTheDocument();
|
||||
@@ -26,9 +24,7 @@ describe('StatusWorkflow', () => {
|
||||
});
|
||||
|
||||
it('highlights current status', () => {
|
||||
render(
|
||||
<StatusWorkflow currentStatus="in_progress" onStatusChange={mockOnStatusChange} />
|
||||
);
|
||||
render(<StatusWorkflow currentStatus="in_progress" onStatusChange={mockOnStatusChange} />);
|
||||
|
||||
const inProgressButton = screen.getByRole('radio', { name: /in progress/i });
|
||||
expect(inProgressButton).toHaveAttribute('aria-checked', 'true');
|
||||
@@ -36,9 +32,7 @@ describe('StatusWorkflow', () => {
|
||||
|
||||
it('calls onStatusChange when status is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<StatusWorkflow currentStatus="open" onStatusChange={mockOnStatusChange} />
|
||||
);
|
||||
render(<StatusWorkflow currentStatus="open" onStatusChange={mockOnStatusChange} />);
|
||||
|
||||
const inProgressButton = screen.getByRole('radio', { name: /in progress/i });
|
||||
await user.click(inProgressButton);
|
||||
@@ -48,9 +42,7 @@ describe('StatusWorkflow', () => {
|
||||
|
||||
it('disables status buttons when disabled prop is true', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<StatusWorkflow currentStatus="open" onStatusChange={mockOnStatusChange} disabled />
|
||||
);
|
||||
render(<StatusWorkflow currentStatus="open" onStatusChange={mockOnStatusChange} disabled />);
|
||||
|
||||
const inProgressButton = screen.getByRole('radio', { name: /in progress/i });
|
||||
expect(inProgressButton).toBeDisabled();
|
||||
@@ -72,9 +64,7 @@ describe('StatusWorkflow', () => {
|
||||
});
|
||||
|
||||
it('has proper radiogroup role', () => {
|
||||
render(
|
||||
<StatusWorkflow currentStatus="open" onStatusChange={mockOnStatusChange} />
|
||||
);
|
||||
render(<StatusWorkflow currentStatus="open" onStatusChange={mockOnStatusChange} />);
|
||||
|
||||
expect(screen.getByRole('radiogroup', { name: /issue status/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -16,10 +16,9 @@ describe('useDebounce', () => {
|
||||
});
|
||||
|
||||
it('updates the debounced value after the delay', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ value, delay }) => useDebounce(value, delay),
|
||||
{ initialProps: { value: 'initial', delay: 500 } }
|
||||
);
|
||||
const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), {
|
||||
initialProps: { value: 'initial', delay: 500 },
|
||||
});
|
||||
|
||||
// Change the value
|
||||
rerender({ value: 'updated', delay: 500 });
|
||||
@@ -37,10 +36,9 @@ describe('useDebounce', () => {
|
||||
});
|
||||
|
||||
it('does not update the value before the delay', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ value, delay }) => useDebounce(value, delay),
|
||||
{ initialProps: { value: 'initial', delay: 500 } }
|
||||
);
|
||||
const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), {
|
||||
initialProps: { value: 'initial', delay: 500 },
|
||||
});
|
||||
|
||||
rerender({ value: 'updated', delay: 500 });
|
||||
|
||||
@@ -53,10 +51,9 @@ describe('useDebounce', () => {
|
||||
});
|
||||
|
||||
it('resets the timer when value changes rapidly', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ value, delay }) => useDebounce(value, delay),
|
||||
{ initialProps: { value: 'initial', delay: 500 } }
|
||||
);
|
||||
const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), {
|
||||
initialProps: { value: 'initial', delay: 500 },
|
||||
});
|
||||
|
||||
// First change
|
||||
rerender({ value: 'first', delay: 500 });
|
||||
@@ -89,10 +86,9 @@ describe('useDebounce', () => {
|
||||
it('cleans up timeout on unmount', () => {
|
||||
const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout');
|
||||
|
||||
const { unmount, rerender } = renderHook(
|
||||
({ value, delay }) => useDebounce(value, delay),
|
||||
{ initialProps: { value: 'initial', delay: 500 } }
|
||||
);
|
||||
const { unmount, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), {
|
||||
initialProps: { value: 'initial', delay: 500 },
|
||||
});
|
||||
|
||||
rerender({ value: 'updated', delay: 500 });
|
||||
unmount();
|
||||
@@ -102,10 +98,9 @@ describe('useDebounce', () => {
|
||||
});
|
||||
|
||||
it('works with different delay values', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ value, delay }) => useDebounce(value, delay),
|
||||
{ initialProps: { value: 'initial', delay: 1000 } }
|
||||
);
|
||||
const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), {
|
||||
initialProps: { value: 'initial', delay: 1000 },
|
||||
});
|
||||
|
||||
rerender({ value: 'updated', delay: 1000 });
|
||||
|
||||
@@ -138,10 +133,9 @@ describe('useDebounce', () => {
|
||||
});
|
||||
|
||||
it('handles zero delay', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ value, delay }) => useDebounce(value, delay),
|
||||
{ initialProps: { value: 'initial', delay: 0 } }
|
||||
);
|
||||
const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), {
|
||||
initialProps: { value: 'initial', delay: 0 },
|
||||
});
|
||||
|
||||
rerender({ value: 'updated', delay: 0 });
|
||||
|
||||
|
||||
@@ -119,9 +119,7 @@ describe('useProjectEvents', () => {
|
||||
|
||||
describe('initialization', () => {
|
||||
it('should start disconnected', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useProjectEvents('project-123', { autoConnect: false })
|
||||
);
|
||||
const { result } = renderHook(() => useProjectEvents('project-123', { autoConnect: false }));
|
||||
|
||||
expect(result.current.connectionState).toBe('disconnected');
|
||||
expect(result.current.isConnected).toBe(false);
|
||||
|
||||
@@ -189,10 +189,9 @@ describe('Event Store', () => {
|
||||
|
||||
useEventStore.getState().addEvents([agentEvent, issueEvent, sprintEvent]);
|
||||
|
||||
const filtered = useEventStore.getState().getFilteredEvents('project-123', [
|
||||
EventType.AGENT_MESSAGE,
|
||||
EventType.ISSUE_CREATED,
|
||||
]);
|
||||
const filtered = useEventStore
|
||||
.getState()
|
||||
.getFilteredEvents('project-123', [EventType.AGENT_MESSAGE, EventType.ISSUE_CREATED]);
|
||||
|
||||
expect(filtered).toHaveLength(2);
|
||||
expect(filtered.map((e) => e.type)).toContain(EventType.AGENT_MESSAGE);
|
||||
@@ -210,9 +209,9 @@ describe('Event Store', () => {
|
||||
});
|
||||
|
||||
it('should return empty array for non-existent project', () => {
|
||||
const filtered = useEventStore.getState().getFilteredEvents('non-existent', [
|
||||
EventType.AGENT_MESSAGE,
|
||||
]);
|
||||
const filtered = useEventStore
|
||||
.getState()
|
||||
.getFilteredEvents('non-existent', [EventType.AGENT_MESSAGE]);
|
||||
expect(filtered).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user