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 }) => {
|
test('approval actions are visible for pending approvals', async ({ page }) => {
|
||||||
// Find approval event
|
// Find approval event
|
||||||
const approvalEvent = page.locator('[data-testid^="event-item-"]', {
|
const approvalEvent = page
|
||||||
has: page.getByText('Action Required'),
|
.locator('[data-testid^="event-item-"]', {
|
||||||
}).first();
|
has: page.getByText('Action Required'),
|
||||||
|
})
|
||||||
|
.first();
|
||||||
|
|
||||||
// Approval buttons should be visible
|
// Approval buttons should be visible
|
||||||
await expect(approvalEvent.getByTestId('approve-button')).toBeVisible();
|
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 }) => {
|
test('should navigate to GitHub when clicking View on GitHub', async ({ page }) => {
|
||||||
const githubLink = page.getByRole('link', { name: /View on GitHub/i }).first();
|
const githubLink = page.getByRole('link', { name: /View on GitHub/i }).first();
|
||||||
await expect(githubLink).toBeVisible();
|
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 }) => {
|
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();
|
await expect(page.getByTestId('project-header')).toBeVisible();
|
||||||
|
|
||||||
// Check project name
|
// 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
|
// Check status badges
|
||||||
await expect(page.getByText('In Progress')).toBeVisible();
|
await expect(page.getByText('In Progress')).toBeVisible();
|
||||||
@@ -288,7 +290,9 @@ test.describe('Project Dashboard Activity Feed', () => {
|
|||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
// Look for action buttons in activity feed (if any require action)
|
// 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();
|
const count = await reviewButton.count();
|
||||||
|
|
||||||
// Either there are action items or not - both are valid
|
// Either there are action items or not - both are valid
|
||||||
|
|||||||
@@ -7,42 +7,42 @@
|
|||||||
* - Please do NOT modify this file.
|
* - Please do NOT modify this file.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const PACKAGE_VERSION = '2.12.3'
|
const PACKAGE_VERSION = '2.12.3';
|
||||||
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
|
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82';
|
||||||
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse');
|
||||||
const activeClientIds = new Set()
|
const activeClientIds = new Set();
|
||||||
|
|
||||||
addEventListener('install', function () {
|
addEventListener('install', function () {
|
||||||
self.skipWaiting()
|
self.skipWaiting();
|
||||||
})
|
});
|
||||||
|
|
||||||
addEventListener('activate', function (event) {
|
addEventListener('activate', function (event) {
|
||||||
event.waitUntil(self.clients.claim())
|
event.waitUntil(self.clients.claim());
|
||||||
})
|
});
|
||||||
|
|
||||||
addEventListener('message', async function (event) {
|
addEventListener('message', async function (event) {
|
||||||
const clientId = Reflect.get(event.source || {}, 'id')
|
const clientId = Reflect.get(event.source || {}, 'id');
|
||||||
|
|
||||||
if (!clientId || !self.clients) {
|
if (!clientId || !self.clients) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = await self.clients.get(clientId)
|
const client = await self.clients.get(clientId);
|
||||||
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const allClients = await self.clients.matchAll({
|
const allClients = await self.clients.matchAll({
|
||||||
type: 'window',
|
type: 'window',
|
||||||
})
|
});
|
||||||
|
|
||||||
switch (event.data) {
|
switch (event.data) {
|
||||||
case 'KEEPALIVE_REQUEST': {
|
case 'KEEPALIVE_REQUEST': {
|
||||||
sendToClient(client, {
|
sendToClient(client, {
|
||||||
type: 'KEEPALIVE_RESPONSE',
|
type: 'KEEPALIVE_RESPONSE',
|
||||||
})
|
});
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'INTEGRITY_CHECK_REQUEST': {
|
case 'INTEGRITY_CHECK_REQUEST': {
|
||||||
@@ -52,12 +52,12 @@ addEventListener('message', async function (event) {
|
|||||||
packageVersion: PACKAGE_VERSION,
|
packageVersion: PACKAGE_VERSION,
|
||||||
checksum: INTEGRITY_CHECKSUM,
|
checksum: INTEGRITY_CHECKSUM,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'MOCK_ACTIVATE': {
|
case 'MOCK_ACTIVATE': {
|
||||||
activeClientIds.add(clientId)
|
activeClientIds.add(clientId);
|
||||||
|
|
||||||
sendToClient(client, {
|
sendToClient(client, {
|
||||||
type: 'MOCKING_ENABLED',
|
type: 'MOCKING_ENABLED',
|
||||||
@@ -67,54 +67,51 @@ addEventListener('message', async function (event) {
|
|||||||
frameType: client.frameType,
|
frameType: client.frameType,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'CLIENT_CLOSED': {
|
case 'CLIENT_CLOSED': {
|
||||||
activeClientIds.delete(clientId)
|
activeClientIds.delete(clientId);
|
||||||
|
|
||||||
const remainingClients = allClients.filter((client) => {
|
const remainingClients = allClients.filter((client) => {
|
||||||
return client.id !== clientId
|
return client.id !== clientId;
|
||||||
})
|
});
|
||||||
|
|
||||||
// Unregister itself when there are no more clients
|
// Unregister itself when there are no more clients
|
||||||
if (remainingClients.length === 0) {
|
if (remainingClients.length === 0) {
|
||||||
self.registration.unregister()
|
self.registration.unregister();
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
addEventListener('fetch', function (event) {
|
addEventListener('fetch', function (event) {
|
||||||
const requestInterceptedAt = Date.now()
|
const requestInterceptedAt = Date.now();
|
||||||
|
|
||||||
// Bypass navigation requests.
|
// Bypass navigation requests.
|
||||||
if (event.request.mode === 'navigate') {
|
if (event.request.mode === 'navigate') {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Opening the DevTools triggers the "only-if-cached" request
|
// Opening the DevTools triggers the "only-if-cached" request
|
||||||
// that cannot be handled by the worker. Bypass such requests.
|
// that cannot be handled by the worker. Bypass such requests.
|
||||||
if (
|
if (event.request.cache === 'only-if-cached' && event.request.mode !== 'same-origin') {
|
||||||
event.request.cache === 'only-if-cached' &&
|
return;
|
||||||
event.request.mode !== 'same-origin'
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bypass all requests when there are no active clients.
|
// Bypass all requests when there are no active clients.
|
||||||
// Prevents the self-unregistered worked from handling requests
|
// Prevents the self-unregistered worked from handling requests
|
||||||
// after it's been terminated (still remains active until the next reload).
|
// after it's been terminated (still remains active until the next reload).
|
||||||
if (activeClientIds.size === 0) {
|
if (activeClientIds.size === 0) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestId = crypto.randomUUID()
|
const requestId = crypto.randomUUID();
|
||||||
event.respondWith(handleRequest(event, requestId, requestInterceptedAt))
|
event.respondWith(handleRequest(event, requestId, requestInterceptedAt));
|
||||||
})
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {FetchEvent} event
|
* @param {FetchEvent} event
|
||||||
@@ -122,23 +119,18 @@ addEventListener('fetch', function (event) {
|
|||||||
* @param {number} requestInterceptedAt
|
* @param {number} requestInterceptedAt
|
||||||
*/
|
*/
|
||||||
async function handleRequest(event, requestId, requestInterceptedAt) {
|
async function handleRequest(event, requestId, requestInterceptedAt) {
|
||||||
const client = await resolveMainClient(event)
|
const client = await resolveMainClient(event);
|
||||||
const requestCloneForEvents = event.request.clone()
|
const requestCloneForEvents = event.request.clone();
|
||||||
const response = await getResponse(
|
const response = await getResponse(event, client, requestId, requestInterceptedAt);
|
||||||
event,
|
|
||||||
client,
|
|
||||||
requestId,
|
|
||||||
requestInterceptedAt,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Send back the response clone for the "response:*" life-cycle events.
|
// Send back the response clone for the "response:*" life-cycle events.
|
||||||
// Ensure MSW is active and ready to handle the message, otherwise
|
// Ensure MSW is active and ready to handle the message, otherwise
|
||||||
// this message will pend indefinitely.
|
// this message will pend indefinitely.
|
||||||
if (client && activeClientIds.has(client.id)) {
|
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.
|
// Clone the response so both the client and the library could consume it.
|
||||||
const responseClone = response.clone()
|
const responseClone = response.clone();
|
||||||
|
|
||||||
sendToClient(
|
sendToClient(
|
||||||
client,
|
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>}
|
* @returns {Promise<Client | undefined>}
|
||||||
*/
|
*/
|
||||||
async function resolveMainClient(event) {
|
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)) {
|
if (activeClientIds.has(event.clientId)) {
|
||||||
return client
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (client?.frameType === 'top-level') {
|
if (client?.frameType === 'top-level') {
|
||||||
return client
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
const allClients = await self.clients.matchAll({
|
const allClients = await self.clients.matchAll({
|
||||||
type: 'window',
|
type: 'window',
|
||||||
})
|
});
|
||||||
|
|
||||||
return allClients
|
return allClients
|
||||||
.filter((client) => {
|
.filter((client) => {
|
||||||
// Get only those clients that are currently visible.
|
// Get only those clients that are currently visible.
|
||||||
return client.visibilityState === 'visible'
|
return client.visibilityState === 'visible';
|
||||||
})
|
})
|
||||||
.find((client) => {
|
.find((client) => {
|
||||||
// Find the client ID that's recorded in the
|
// Find the client ID that's recorded in the
|
||||||
// set of clients that have registered the worker.
|
// 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) {
|
async function getResponse(event, client, requestId, requestInterceptedAt) {
|
||||||
// Clone the request because it might've been already used
|
// Clone the request because it might've been already used
|
||||||
// (i.e. its body has been read and sent to the client).
|
// (i.e. its body has been read and sent to the client).
|
||||||
const requestClone = event.request.clone()
|
const requestClone = event.request.clone();
|
||||||
|
|
||||||
function passthrough() {
|
function passthrough() {
|
||||||
// Cast the request headers to a new Headers instance
|
// Cast the request headers to a new Headers instance
|
||||||
// so the headers can be manipulated with.
|
// 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.
|
// Remove the "accept" header value that marked this request as passthrough.
|
||||||
// This prevents request alteration and also keeps it compliant with the
|
// This prevents request alteration and also keeps it compliant with the
|
||||||
// user-defined CORS policies.
|
// user-defined CORS policies.
|
||||||
const acceptHeader = headers.get('accept')
|
const acceptHeader = headers.get('accept');
|
||||||
if (acceptHeader) {
|
if (acceptHeader) {
|
||||||
const values = acceptHeader.split(',').map((value) => value.trim())
|
const values = acceptHeader.split(',').map((value) => value.trim());
|
||||||
const filteredValues = values.filter(
|
const filteredValues = values.filter((value) => value !== 'msw/passthrough');
|
||||||
(value) => value !== 'msw/passthrough',
|
|
||||||
)
|
|
||||||
|
|
||||||
if (filteredValues.length > 0) {
|
if (filteredValues.length > 0) {
|
||||||
headers.set('accept', filteredValues.join(', '))
|
headers.set('accept', filteredValues.join(', '));
|
||||||
} else {
|
} else {
|
||||||
headers.delete('accept')
|
headers.delete('accept');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetch(requestClone, { headers })
|
return fetch(requestClone, { headers });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bypass mocking when the client is not active.
|
// Bypass mocking when the client is not active.
|
||||||
if (!client) {
|
if (!client) {
|
||||||
return passthrough()
|
return passthrough();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bypass initial page load requests (i.e. static assets).
|
// 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
|
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
|
||||||
// and is not ready to handle requests.
|
// and is not ready to handle requests.
|
||||||
if (!activeClientIds.has(client.id)) {
|
if (!activeClientIds.has(client.id)) {
|
||||||
return passthrough()
|
return passthrough();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify the client that a request has been intercepted.
|
// 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(
|
const clientMessage = await sendToClient(
|
||||||
client,
|
client,
|
||||||
{
|
{
|
||||||
@@ -263,20 +253,20 @@ async function getResponse(event, client, requestId, requestInterceptedAt) {
|
|||||||
...serializedRequest,
|
...serializedRequest,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[serializedRequest.body],
|
[serializedRequest.body]
|
||||||
)
|
);
|
||||||
|
|
||||||
switch (clientMessage.type) {
|
switch (clientMessage.type) {
|
||||||
case 'MOCK_RESPONSE': {
|
case 'MOCK_RESPONSE': {
|
||||||
return respondWithMock(clientMessage.data)
|
return respondWithMock(clientMessage.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'PASSTHROUGH': {
|
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 = []) {
|
function sendToClient(client, message, transferrables = []) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const channel = new MessageChannel()
|
const channel = new MessageChannel();
|
||||||
|
|
||||||
channel.port1.onmessage = (event) => {
|
channel.port1.onmessage = (event) => {
|
||||||
if (event.data && event.data.error) {
|
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, [
|
client.postMessage(message, [channel.port2, ...transferrables.filter(Boolean)]);
|
||||||
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
|
// 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.
|
// a Response instance with status code 0, handle that use-case separately.
|
||||||
if (response.status === 0) {
|
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, {
|
Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
|
||||||
value: true,
|
value: true,
|
||||||
enumerable: true,
|
enumerable: true,
|
||||||
})
|
});
|
||||||
|
|
||||||
return mockedResponse
|
return mockedResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -345,5 +332,5 @@ async function serializeRequest(request) {
|
|||||||
referrerPolicy: request.referrerPolicy,
|
referrerPolicy: request.referrerPolicy,
|
||||||
body: await request.arrayBuffer(),
|
body: await request.arrayBuffer(),
|
||||||
keepalive: request.keepalive,
|
keepalive: request.keepalive,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,13 +199,10 @@ export default function ActivityFeedPage() {
|
|||||||
<BellOff className="h-4 w-4" />
|
<BellOff className="h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button variant="ghost" size="icon" onClick={reconnect} aria-label="Refresh connection">
|
||||||
variant="ghost"
|
<RefreshCw
|
||||||
size="icon"
|
className={connectionState === 'connecting' ? 'h-4 w-4 animate-spin' : 'h-4 w-4'}
|
||||||
onClick={reconnect}
|
/>
|
||||||
aria-label="Refresh connection"
|
|
||||||
>
|
|
||||||
<RefreshCw className={connectionState === 'connecting' ? 'h-4 w-4 animate-spin' : 'h-4 w-4'} />
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -214,7 +211,8 @@ export default function ActivityFeedPage() {
|
|||||||
{(!isConnected || sseEvents.length === 0) && (
|
{(!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">
|
<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">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -32,11 +32,7 @@ export default function AgentTypeDetailPage() {
|
|||||||
const [viewMode, setViewMode] = useState<ViewMode>(isNew ? 'create' : 'detail');
|
const [viewMode, setViewMode] = useState<ViewMode>(isNew ? 'create' : 'detail');
|
||||||
|
|
||||||
// Fetch agent type data (skip if creating new)
|
// Fetch agent type data (skip if creating new)
|
||||||
const {
|
const { data: agentType, isLoading, error } = useAgentType(isNew ? null : id);
|
||||||
data: agentType,
|
|
||||||
isLoading,
|
|
||||||
error,
|
|
||||||
} = useAgentType(isNew ? null : id);
|
|
||||||
|
|
||||||
// Mutations
|
// Mutations
|
||||||
const createMutation = useCreateAgentType();
|
const createMutation = useCreateAgentType();
|
||||||
@@ -171,7 +167,7 @@ export default function AgentTypeDetailPage() {
|
|||||||
<div className="container mx-auto px-4 py-6">
|
<div className="container mx-auto px-4 py-6">
|
||||||
{(viewMode === 'create' || viewMode === 'edit') && (
|
{(viewMode === 'create' || viewMode === 'edit') && (
|
||||||
<AgentTypeForm
|
<AgentTypeForm
|
||||||
agentType={viewMode === 'edit' ? agentType ?? undefined : undefined}
|
agentType={viewMode === 'edit' ? (agentType ?? undefined) : undefined}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
isSubmitting={createMutation.isPending || updateMutation.isPending}
|
isSubmitting={createMutation.isPending || updateMutation.isPending}
|
||||||
|
|||||||
@@ -10,13 +10,7 @@
|
|||||||
|
|
||||||
import { use } from 'react';
|
import { use } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import {
|
import { ArrowLeft, Calendar, Clock, ExternalLink, Edit } from 'lucide-react';
|
||||||
ArrowLeft,
|
|
||||||
Calendar,
|
|
||||||
Clock,
|
|
||||||
ExternalLink,
|
|
||||||
Edit,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
@@ -65,10 +59,7 @@ export default function IssueDetailPage({ params }: IssueDetailPageProps) {
|
|||||||
<Link href={`/${locale}/projects/${projectId}/issues`}>
|
<Link href={`/${locale}/projects/${projectId}/issues`}>
|
||||||
<Button variant="outline">Back to Issues</Button>
|
<Button variant="outline">Back to Issues</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Button
|
<Button variant="outline" onClick={() => window.location.reload()}>
|
||||||
variant="outline"
|
|
||||||
onClick={() => window.location.reload()}
|
|
||||||
>
|
|
||||||
Retry
|
Retry
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -171,9 +162,7 @@ export default function IssueDetailPage({ params }: IssueDetailPageProps) {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="prose prose-sm max-w-none dark:prose-invert">
|
<div className="prose prose-sm max-w-none dark:prose-invert">
|
||||||
<pre className="whitespace-pre-wrap font-sans text-sm">
|
<pre className="whitespace-pre-wrap font-sans text-sm">{issue.description}</pre>
|
||||||
{issue.description}
|
|
||||||
</pre>
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -14,12 +14,7 @@ import { useRouter } from 'next/navigation';
|
|||||||
import { Plus, Upload } from 'lucide-react';
|
import { Plus, Upload } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import {
|
import { IssueFilters, IssueTable, BulkActions, useIssues } from '@/features/issues';
|
||||||
IssueFilters,
|
|
||||||
IssueTable,
|
|
||||||
BulkActions,
|
|
||||||
useIssues,
|
|
||||||
} from '@/features/issues';
|
|
||||||
import type { IssueFiltersType, IssueSort } from '@/features/issues';
|
import type { IssueFiltersType, IssueSort } from '@/features/issues';
|
||||||
|
|
||||||
interface ProjectIssuesPageProps {
|
interface ProjectIssuesPageProps {
|
||||||
@@ -95,11 +90,7 @@ export default function ProjectIssuesPage({ params }: ProjectIssuesPageProps) {
|
|||||||
<p className="mt-2 text-sm text-muted-foreground">
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
Failed to load issues. Please try again later.
|
Failed to load issues. Please try again later.
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button variant="outline" className="mt-4" onClick={() => window.location.reload()}>
|
||||||
variant="outline"
|
|
||||||
className="mt-4"
|
|
||||||
onClick={() => window.location.reload()}
|
|
||||||
>
|
|
||||||
Retry
|
Retry
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -169,26 +160,15 @@ export default function ProjectIssuesPage({ params }: ProjectIssuesPageProps) {
|
|||||||
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
||||||
<span>
|
<span>
|
||||||
Showing {(data.pagination.page - 1) * data.pagination.page_size + 1} to{' '}
|
Showing {(data.pagination.page - 1) * data.pagination.page_size + 1} to{' '}
|
||||||
{Math.min(
|
{Math.min(data.pagination.page * data.pagination.page_size, data.pagination.total)} of{' '}
|
||||||
data.pagination.page * data.pagination.page_size,
|
{data.pagination.total} issues
|
||||||
data.pagination.total
|
|
||||||
)}{' '}
|
|
||||||
of {data.pagination.total} issues
|
|
||||||
</span>
|
</span>
|
||||||
{data.pagination.total_pages > 1 && (
|
{data.pagination.total_pages > 1 && (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button variant="outline" size="sm" disabled={!data.pagination.has_prev}>
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
disabled={!data.pagination.has_prev}
|
|
||||||
>
|
|
||||||
Previous
|
Previous
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button variant="outline" size="sm" disabled={!data.pagination.has_next}>
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
disabled={!data.pagination.has_next}
|
|
||||||
>
|
|
||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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.
|
The Project Dashboard prototype has been created and is ready for review.
|
||||||
|
|
||||||
### How to View
|
### How to View
|
||||||
|
|
||||||
1. Start the frontend dev server: `cd frontend && npm run dev`
|
1. Start the frontend dev server: `cd frontend && npm run dev`
|
||||||
2. Navigate to: `http://localhost:3000/en/prototypes/project-dashboard`
|
2. Navigate to: `http://localhost:3000/en/prototypes/project-dashboard`
|
||||||
|
|
||||||
### What's Included
|
### What's Included
|
||||||
|
|
||||||
**Header Section**
|
**Header Section**
|
||||||
|
|
||||||
- Project name with status badge (In Progress, Completed, Paused, Blocked)
|
- Project name with status badge (In Progress, Completed, Paused, Blocked)
|
||||||
- Autonomy level indicator (Full Control, Milestone, Autonomous)
|
- Autonomy level indicator (Full Control, Milestone, Autonomous)
|
||||||
- Quick action buttons (Pause Project, Run Sprint)
|
- Quick action buttons (Pause Project, Run Sprint)
|
||||||
|
|
||||||
**Agent Panel**
|
**Agent Panel**
|
||||||
|
|
||||||
- List of all project agents with avatars
|
- List of all project agents with avatars
|
||||||
- Real-time status indicators (active = green, idle = yellow, pending = gray)
|
- Real-time status indicators (active = green, idle = yellow, pending = gray)
|
||||||
- Current task description for each agent
|
- Current task description for each agent
|
||||||
- Last activity timestamp
|
- Last activity timestamp
|
||||||
|
|
||||||
**Sprint Overview**
|
**Sprint Overview**
|
||||||
|
|
||||||
- Current sprint progress bar
|
- Current sprint progress bar
|
||||||
- Issue statistics grid (Completed, In Progress, Blocked, To Do)
|
- Issue statistics grid (Completed, In Progress, Blocked, To Do)
|
||||||
- Visual burndown chart with ideal vs actual lines
|
- Visual burndown chart with ideal vs actual lines
|
||||||
- Sprint selector dropdown
|
- Sprint selector dropdown
|
||||||
|
|
||||||
**Issue Summary Sidebar**
|
**Issue Summary Sidebar**
|
||||||
|
|
||||||
- Count of issues by status with color-coded icons
|
- Count of issues by status with color-coded icons
|
||||||
- Quick links to view all issues
|
- Quick links to view all issues
|
||||||
|
|
||||||
**Recent Activity Feed**
|
**Recent Activity Feed**
|
||||||
|
|
||||||
- Chronological event list with type icons
|
- Chronological event list with type icons
|
||||||
- Agent attribution
|
- Agent attribution
|
||||||
- Highlighted approval requests with action buttons
|
- Highlighted approval requests with action buttons
|
||||||
|
|
||||||
### Key Design Decisions
|
### Key Design Decisions
|
||||||
|
|
||||||
- Three-column layout on desktop (2/3 main, 1/3 sidebar)
|
- Three-column layout on desktop (2/3 main, 1/3 sidebar)
|
||||||
- Agent status uses traffic light colors for intuitive understanding
|
- Agent status uses traffic light colors for intuitive understanding
|
||||||
- Burndown chart is simplified for quick scanning
|
- Burndown chart is simplified for quick scanning
|
||||||
- Activity feed limited to 5 items with "View All" link
|
- Activity feed limited to 5 items with "View All" link
|
||||||
|
|
||||||
### Questions for Review
|
### Questions for Review
|
||||||
|
|
||||||
1. Is the burndown chart detailed enough?
|
1. Is the burndown chart detailed enough?
|
||||||
2. Should agent cards be expandable for more details?
|
2. Should agent cards be expandable for more details?
|
||||||
3. Is the 5-item activity feed sufficient?
|
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.**
|
**Please review and approve or provide feedback.**
|
||||||
|
|
||||||
Files:
|
Files:
|
||||||
|
|
||||||
- `/frontend/src/app/[locale]/prototypes/project-dashboard/page.tsx`
|
- `/frontend/src/app/[locale]/prototypes/project-dashboard/page.tsx`
|
||||||
- `/frontend/src/app/[locale]/prototypes/project-dashboard/README.md`
|
- `/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.
|
The Agent Configuration UI prototype has been created and is ready for review.
|
||||||
|
|
||||||
### How to View
|
### How to View
|
||||||
|
|
||||||
1. Start the frontend dev server: `cd frontend && npm run dev`
|
1. Start the frontend dev server: `cd frontend && npm run dev`
|
||||||
2. Navigate to: `http://localhost:3000/en/prototypes/agent-configuration`
|
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
|
- **Personality Tab**: Large textarea for personality prompt
|
||||||
|
|
||||||
### Key Design Decisions
|
### Key Design Decisions
|
||||||
|
|
||||||
- Separate views for browsing, viewing, and editing
|
- Separate views for browsing, viewing, and editing
|
||||||
- Tabbed editor reduces cognitive load
|
- Tabbed editor reduces cognitive load
|
||||||
- MCP permissions show nested scopes when enabled
|
- MCP permissions show nested scopes when enabled
|
||||||
- Model parameters have helpful descriptions
|
- Model parameters have helpful descriptions
|
||||||
|
|
||||||
### User Flows to Test
|
### User Flows to Test
|
||||||
|
|
||||||
1. Click any card to see detail view
|
1. Click any card to see detail view
|
||||||
2. Click "Edit" to see editor view
|
2. Click "Edit" to see editor view
|
||||||
3. Click "Create Agent Type" for blank editor
|
3. Click "Create Agent Type" for blank editor
|
||||||
4. Navigate tabs in editor
|
4. Navigate tabs in editor
|
||||||
|
|
||||||
### Questions for Review
|
### Questions for Review
|
||||||
|
|
||||||
1. Is the tabbed editor the right approach?
|
1. Is the tabbed editor the right approach?
|
||||||
2. Should expertise be free-form tags or predefined list?
|
2. Should expertise be free-form tags or predefined list?
|
||||||
3. Should model parameters have "presets"?
|
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.**
|
**Please review and approve or provide feedback.**
|
||||||
|
|
||||||
Files:
|
Files:
|
||||||
|
|
||||||
- `/frontend/src/app/[locale]/prototypes/agent-configuration/page.tsx`
|
- `/frontend/src/app/[locale]/prototypes/agent-configuration/page.tsx`
|
||||||
- `/frontend/src/app/[locale]/prototypes/agent-configuration/README.md`
|
- `/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.
|
The Issue List and Detail Views prototype has been created and is ready for review.
|
||||||
|
|
||||||
### How to View
|
### How to View
|
||||||
|
|
||||||
1. Start the frontend dev server: `cd frontend && npm run dev`
|
1. Start the frontend dev server: `cd frontend && npm run dev`
|
||||||
2. Navigate to: `http://localhost:3000/en/prototypes/issue-management`
|
2. Navigate to: `http://localhost:3000/en/prototypes/issue-management`
|
||||||
|
|
||||||
### What's Included
|
### What's Included
|
||||||
|
|
||||||
**List View**
|
**List View**
|
||||||
|
|
||||||
- Filterable table with sortable columns
|
- Filterable table with sortable columns
|
||||||
- Quick status filter + expandable advanced filters
|
- Quick status filter + expandable advanced filters
|
||||||
- Bulk action bar (appears when selecting issues)
|
- 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
|
- Labels displayed as badges
|
||||||
|
|
||||||
**Filter Options**
|
**Filter Options**
|
||||||
|
|
||||||
- Status: Open, In Progress, In Review, Blocked, Done
|
- Status: Open, In Progress, In Review, Blocked, Done
|
||||||
- Priority: High, Medium, Low
|
- Priority: High, Medium, Low
|
||||||
- Sprint: Current sprints, Backlog
|
- 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.
|
- Labels: Feature, Bug, Backend, Frontend, etc.
|
||||||
|
|
||||||
**Detail View**
|
**Detail View**
|
||||||
|
|
||||||
- Full issue content (markdown-like display)
|
- Full issue content (markdown-like display)
|
||||||
- Status workflow panel (click to change status)
|
- Status workflow panel (click to change status)
|
||||||
- Assignment panel with agent avatar
|
- 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)
|
- Development section (branch, PR link)
|
||||||
|
|
||||||
### Key Design Decisions
|
### Key Design Decisions
|
||||||
|
|
||||||
- Table layout for density and scannability
|
- Table layout for density and scannability
|
||||||
- Status workflow matches common issue tracker patterns
|
- Status workflow matches common issue tracker patterns
|
||||||
- Sync status indicator shows data freshness
|
- Sync status indicator shows data freshness
|
||||||
- Activity timeline shows issue history
|
- Activity timeline shows issue history
|
||||||
|
|
||||||
### User Flows to Test
|
### User Flows to Test
|
||||||
|
|
||||||
1. Use search and filters
|
1. Use search and filters
|
||||||
2. Click checkboxes to see bulk actions
|
2. Click checkboxes to see bulk actions
|
||||||
3. Sort by clicking column headers
|
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
|
5. Click status buttons in detail view
|
||||||
|
|
||||||
### Questions for Review
|
### Questions for Review
|
||||||
|
|
||||||
1. Should we add Kanban view as alternative?
|
1. Should we add Kanban view as alternative?
|
||||||
2. Is the sync indicator clear enough?
|
2. Is the sync indicator clear enough?
|
||||||
3. Should there be inline editing?
|
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.**
|
**Please review and approve or provide feedback.**
|
||||||
|
|
||||||
Files:
|
Files:
|
||||||
|
|
||||||
- `/frontend/src/app/[locale]/prototypes/issue-management/page.tsx`
|
- `/frontend/src/app/[locale]/prototypes/issue-management/page.tsx`
|
||||||
- `/frontend/src/app/[locale]/prototypes/issue-management/README.md`
|
- `/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.
|
The Real-time Activity Feed prototype has been created and is ready for review.
|
||||||
|
|
||||||
### How to View
|
### How to View
|
||||||
|
|
||||||
1. Start the frontend dev server: `cd frontend && npm run dev`
|
1. Start the frontend dev server: `cd frontend && npm run dev`
|
||||||
2. Navigate to: `http://localhost:3000/en/prototypes/activity-feed`
|
2. Navigate to: `http://localhost:3000/en/prototypes/activity-feed`
|
||||||
|
|
||||||
### What's Included
|
### What's Included
|
||||||
|
|
||||||
**Event Types Displayed**
|
**Event Types Displayed**
|
||||||
|
|
||||||
- Agent Status: Started, paused, resumed, stopped
|
- Agent Status: Started, paused, resumed, stopped
|
||||||
- Agent Message: Updates, questions, progress reports
|
- Agent Message: Updates, questions, progress reports
|
||||||
- Issue Update: Status changes, assignments, creation
|
- 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
|
- Milestone: Goals achieved, completions
|
||||||
|
|
||||||
**Features**
|
**Features**
|
||||||
|
|
||||||
- Real-time connection indicator (pulsing green when connected)
|
- Real-time connection indicator (pulsing green when connected)
|
||||||
- Time-based event grouping (New, Earlier Today, Yesterday, etc.)
|
- Time-based event grouping (New, Earlier Today, Yesterday, etc.)
|
||||||
- Search functionality
|
- Search functionality
|
||||||
@@ -231,6 +256,7 @@ The Real-time Activity Feed prototype has been created and is ready for review.
|
|||||||
- Mark all read functionality
|
- Mark all read functionality
|
||||||
|
|
||||||
### Key Design Decisions
|
### Key Design Decisions
|
||||||
|
|
||||||
- Card-based layout for clear event separation
|
- Card-based layout for clear event separation
|
||||||
- Orange left border highlights action-required items
|
- Orange left border highlights action-required items
|
||||||
- Time grouping helps users orient in timeline
|
- 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
|
- Real-time indicator builds trust in data freshness
|
||||||
|
|
||||||
### User Flows to Test
|
### User Flows to Test
|
||||||
|
|
||||||
1. Scroll through the event feed
|
1. Scroll through the event feed
|
||||||
2. Click events to expand details
|
2. Click events to expand details
|
||||||
3. Open filter panel and select filters
|
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"
|
5. Click "Mark all read"
|
||||||
|
|
||||||
### Questions for Review
|
### Questions for Review
|
||||||
|
|
||||||
1. Should events be grouped by time or show flat?
|
1. Should events be grouped by time or show flat?
|
||||||
2. Should there be sound notifications for urgent items?
|
2. Should there be sound notifications for urgent items?
|
||||||
3. Should users be able to "star" events?
|
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.**
|
**Please review and approve or provide feedback.**
|
||||||
|
|
||||||
Files:
|
Files:
|
||||||
|
|
||||||
- `/frontend/src/app/[locale]/prototypes/activity-feed/page.tsx`
|
- `/frontend/src/app/[locale]/prototypes/activity-feed/page.tsx`
|
||||||
- `/frontend/src/app/[locale]/prototypes/activity-feed/README.md`
|
- `/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
|
- Issue #39: Real-time Activity Feed
|
||||||
|
|
||||||
After the user reviews each prototype and provides feedback:
|
After the user reviews each prototype and provides feedback:
|
||||||
|
|
||||||
1. Iterate on the design based on feedback
|
1. Iterate on the design based on feedback
|
||||||
2. Get explicit approval
|
2. Get explicit approval
|
||||||
3. Begin implementation
|
3. Begin implementation
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import {
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from '@/components/ui/card';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -248,12 +248,47 @@ const EVENT_TYPE_CONFIG: Record<
|
|||||||
};
|
};
|
||||||
|
|
||||||
const FILTER_CATEGORIES = [
|
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: '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: 'approval',
|
||||||
{ id: 'project', label: 'Projects', types: [EventType.PROJECT_CREATED, EventType.PROJECT_UPDATED, EventType.PROJECT_ARCHIVED] },
|
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
|
// Fallback based on event category
|
||||||
if (isAgentEvent(event)) {
|
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)) {
|
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)) {
|
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)) {
|
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)) {
|
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)) {
|
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 {
|
function getEventSummary(event: ProjectEvent): string {
|
||||||
@@ -304,7 +374,9 @@ function getEventSummary(event: ProjectEvent): string {
|
|||||||
case EventType.ISSUE_UPDATED:
|
case EventType.ISSUE_UPDATED:
|
||||||
return `Issue ${payload.issue_id || ''} updated`;
|
return `Issue ${payload.issue_id || ''} updated`;
|
||||||
case EventType.ISSUE_ASSIGNED:
|
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:
|
case EventType.ISSUE_CLOSED:
|
||||||
return payload.resolution ? `Closed: ${payload.resolution}` : 'Issue closed';
|
return payload.resolution ? `Closed: ${payload.resolution}` : 'Issue closed';
|
||||||
case EventType.SPRINT_STARTED:
|
case EventType.SPRINT_STARTED:
|
||||||
@@ -318,11 +390,15 @@ function getEventSummary(event: ProjectEvent): string {
|
|||||||
case EventType.APPROVAL_DENIED:
|
case EventType.APPROVAL_DENIED:
|
||||||
return payload.reason ? `Denied: ${payload.reason}` : 'Approval denied';
|
return payload.reason ? `Denied: ${payload.reason}` : 'Approval denied';
|
||||||
case EventType.WORKFLOW_STARTED:
|
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:
|
case EventType.WORKFLOW_STEP_COMPLETED:
|
||||||
return `Step ${payload.step_number}/${payload.total_steps}: ${payload.step_name || 'completed'}`;
|
return `Step ${payload.step_number}/${payload.total_steps}: ${payload.step_name || 'completed'}`;
|
||||||
case EventType.WORKFLOW_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:
|
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:
|
default:
|
||||||
@@ -391,11 +467,7 @@ function ConnectionIndicator({ state, onReconnect, className }: ConnectionIndica
|
|||||||
return (
|
return (
|
||||||
<div className={cn('flex items-center gap-2', className)} data-testid="connection-indicator">
|
<div className={cn('flex items-center gap-2', className)} data-testid="connection-indicator">
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn('h-2 w-2 rounded-full', config.color, config.pulse && 'animate-pulse')}
|
||||||
'h-2 w-2 rounded-full',
|
|
||||||
config.color,
|
|
||||||
config.pulse && 'animate-pulse'
|
|
||||||
)}
|
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-muted-foreground">{config.label}</span>
|
<span className="text-sm text-muted-foreground">{config.label}</span>
|
||||||
@@ -475,7 +547,10 @@ function FilterPanel({
|
|||||||
checked={showPendingOnly}
|
checked={showPendingOnly}
|
||||||
onCheckedChange={(checked) => onShowPendingOnlyChange(checked as boolean)}
|
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
|
Show only pending approvals
|
||||||
{pendingCount > 0 && (
|
{pendingCount > 0 && (
|
||||||
<Badge variant="destructive" className="text-xs">
|
<Badge variant="destructive" className="text-xs">
|
||||||
@@ -598,77 +673,85 @@ function EventItem({
|
|||||||
}}
|
}}
|
||||||
aria-label={expanded ? 'Collapse details' : 'Expand details'}
|
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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Expanded Details */}
|
{/* Expanded Details */}
|
||||||
{expanded && (() => {
|
{expanded &&
|
||||||
const issueId = payload.issue_id as string | undefined;
|
(() => {
|
||||||
const pullRequest = payload.pullRequest as string | number | undefined;
|
const issueId = payload.issue_id as string | undefined;
|
||||||
const documentUrl = payload.documentUrl as string | undefined;
|
const pullRequest = payload.pullRequest as string | number | undefined;
|
||||||
const progress = payload.progress as number | undefined;
|
const documentUrl = payload.documentUrl as string | undefined;
|
||||||
|
const progress = payload.progress as number | undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-3 rounded-md bg-muted/50 p-3 space-y-3" data-testid="event-details">
|
<div
|
||||||
{/* Issue/PR Links */}
|
className="mt-3 rounded-md bg-muted/50 p-3 space-y-3"
|
||||||
{issueId && (
|
data-testid="event-details"
|
||||||
<div className="flex items-center gap-2 text-sm">
|
>
|
||||||
<CircleDot className="h-4 w-4" aria-hidden="true" />
|
{/* Issue/PR Links */}
|
||||||
<span>Issue #{issueId}</span>
|
{issueId && (
|
||||||
</div>
|
<div className="flex items-center gap-2 text-sm">
|
||||||
)}
|
<CircleDot className="h-4 w-4" aria-hidden="true" />
|
||||||
{pullRequest && (
|
<span>Issue #{issueId}</span>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="h-2 rounded-full bg-muted">
|
)}
|
||||||
<div
|
{pullRequest && (
|
||||||
className="h-full rounded-full bg-primary"
|
<div className="flex items-center gap-2 text-sm">
|
||||||
style={{ width: `${progress}%` }}
|
<GitPullRequest className="h-4 w-4" aria-hidden="true" />
|
||||||
/>
|
<span>PR #{String(pullRequest)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Timestamp */}
|
{/* Document Links */}
|
||||||
<p className="text-xs text-muted-foreground">
|
{documentUrl && (
|
||||||
{new Date(event.timestamp).toLocaleString()}
|
<div className="flex items-center gap-2 text-sm">
|
||||||
</p>
|
<ExternalLink className="h-4 w-4" aria-hidden="true" />
|
||||||
|
<a href={documentUrl} className="text-primary hover:underline">
|
||||||
|
{documentUrl}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Raw Payload (for debugging) */}
|
{/* Progress */}
|
||||||
<details className="text-xs">
|
{progress !== undefined && (
|
||||||
<summary className="cursor-pointer text-muted-foreground hover:text-foreground">
|
<div className="space-y-1">
|
||||||
View raw payload
|
<div className="flex justify-between text-sm">
|
||||||
</summary>
|
<span>Progress</span>
|
||||||
<pre className="mt-2 overflow-x-auto whitespace-pre-wrap break-words rounded bg-muted p-2">
|
<span>{progress}%</span>
|
||||||
{JSON.stringify(event.payload, null, 2)}
|
</div>
|
||||||
</pre>
|
<div className="h-2 rounded-full bg-muted">
|
||||||
</details>
|
<div
|
||||||
</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 */}
|
{/* Approval Actions */}
|
||||||
{isPendingApproval && (onApprove || onReject) && (
|
{isPendingApproval && (onApprove || onReject) && (
|
||||||
@@ -680,7 +763,12 @@ function EventItem({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{onReject && (
|
{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" />
|
<XCircle className="mr-2 h-4 w-4" />
|
||||||
Reject
|
Reject
|
||||||
</Button>
|
</Button>
|
||||||
@@ -712,7 +800,10 @@ function LoadingSkeleton() {
|
|||||||
|
|
||||||
function EmptyState({ hasFilters }: { hasFilters: boolean }) {
|
function EmptyState({ hasFilters }: { hasFilters: boolean }) {
|
||||||
return (
|
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" />
|
<Activity className="h-12 w-12 mb-4" aria-hidden="true" />
|
||||||
<h3 className="font-semibold">No activity found</h3>
|
<h3 className="font-semibold">No activity found</h3>
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
@@ -894,7 +985,10 @@ export function ActivityFeed({
|
|||||||
) : (
|
) : (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{groupedEvents.map((group) => (
|
{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">
|
<div className="mb-3 flex items-center gap-2">
|
||||||
<h3 className="text-sm font-medium text-muted-foreground">{group.label}</h3>
|
<h3 className="text-sm font-medium text-muted-foreground">{group.label}</h3>
|
||||||
<Badge variant="secondary" className="text-xs">
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
|||||||
@@ -57,13 +57,19 @@ interface AgentTypeDetailProps {
|
|||||||
function AgentTypeStatusBadge({ isActive }: { isActive: boolean }) {
|
function AgentTypeStatusBadge({ isActive }: { isActive: boolean }) {
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
return (
|
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
|
Active
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
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
|
Inactive
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
@@ -139,9 +145,7 @@ export function AgentTypeDetail({
|
|||||||
<div className="py-12 text-center">
|
<div className="py-12 text-center">
|
||||||
<AlertTriangle className="mx-auto h-12 w-12 text-muted-foreground" />
|
<AlertTriangle className="mx-auto h-12 w-12 text-muted-foreground" />
|
||||||
<h3 className="mt-4 font-semibold">Agent type not found</h3>
|
<h3 className="mt-4 font-semibold">Agent type not found</h3>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">The requested agent type could not be found</p>
|
||||||
The requested agent type could not be found
|
|
||||||
</p>
|
|
||||||
<Button onClick={onBack} variant="outline" className="mt-4">
|
<Button onClick={onBack} variant="outline" className="mt-4">
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
Go Back
|
Go Back
|
||||||
@@ -265,9 +269,7 @@ export function AgentTypeDetail({
|
|||||||
<div
|
<div
|
||||||
key={server.id}
|
key={server.id}
|
||||||
className={`flex items-center justify-between rounded-lg border p-3 ${
|
className={`flex items-center justify-between rounded-lg border p-3 ${
|
||||||
isEnabled
|
isEnabled ? 'border-primary/20 bg-primary/5' : 'border-muted bg-muted/50'
|
||||||
? 'border-primary/20 bg-primary/5'
|
|
||||||
: 'border-muted bg-muted/50'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -284,9 +286,7 @@ export function AgentTypeDetail({
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">{server.name}</p>
|
<p className="font-medium">{server.name}</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">{server.description}</p>
|
||||||
{server.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant={isEnabled ? 'default' : 'secondary'}>
|
<Badge variant={isEnabled ? 'default' : 'secondary'}>
|
||||||
@@ -313,9 +313,7 @@ export function AgentTypeDetail({
|
|||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground">Primary Model</p>
|
<p className="text-sm text-muted-foreground">Primary Model</p>
|
||||||
<p className="font-medium">
|
<p className="font-medium">{getModelDisplayName(agentType.primary_model)}</p>
|
||||||
{getModelDisplayName(agentType.primary_model)}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground">Failover Model</p>
|
<p className="text-sm text-muted-foreground">Failover Model</p>
|
||||||
@@ -355,9 +353,7 @@ export function AgentTypeDetail({
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-4xl font-bold text-primary">
|
<p className="text-4xl font-bold text-primary">{agentType.instance_count}</p>
|
||||||
{agentType.instance_count}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-muted-foreground">Active instances</p>
|
<p className="text-sm text-muted-foreground">Active instances</p>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" className="mt-4 w-full" size="sm" disabled>
|
<Button variant="outline" className="mt-4 w-full" size="sm" disabled>
|
||||||
|
|||||||
@@ -26,16 +26,7 @@ import {
|
|||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import {
|
import { FileText, Cpu, Shield, MessageSquare, Sliders, Save, ArrowLeft, X } from 'lucide-react';
|
||||||
FileText,
|
|
||||||
Cpu,
|
|
||||||
Shield,
|
|
||||||
MessageSquare,
|
|
||||||
Sliders,
|
|
||||||
Save,
|
|
||||||
ArrowLeft,
|
|
||||||
X,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import {
|
import {
|
||||||
agentTypeCreateSchema,
|
agentTypeCreateSchema,
|
||||||
type AgentTypeCreateFormValues,
|
type AgentTypeCreateFormValues,
|
||||||
@@ -151,9 +142,7 @@ export function AgentTypeForm({
|
|||||||
{isEditing ? 'Edit Agent Type' : 'Create Agent Type'}
|
{isEditing ? 'Edit Agent Type' : 'Create Agent Type'}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
{isEditing
|
{isEditing ? 'Modify agent type configuration' : 'Define a new agent type template'}
|
||||||
? 'Modify agent type configuration'
|
|
||||||
: 'Define a new agent type template'}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
@@ -281,9 +270,7 @@ export function AgentTypeForm({
|
|||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Expertise Areas</Label>
|
<Label>Expertise Areas</Label>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">Add skills and areas of expertise</p>
|
||||||
Add skills and areas of expertise
|
|
||||||
</p>
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder="e.g., System Design"
|
placeholder="e.g., System Design"
|
||||||
@@ -325,9 +312,7 @@ export function AgentTypeForm({
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Model Selection</CardTitle>
|
<CardTitle>Model Selection</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>Choose the AI models that power this agent type</CardDescription>
|
||||||
Choose the AI models that power this agent type
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-6">
|
<CardContent className="space-y-6">
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
@@ -358,9 +343,7 @@ export function AgentTypeForm({
|
|||||||
{errors.primary_model.message}
|
{errors.primary_model.message}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">Main model used for this agent</p>
|
||||||
Main model used for this agent
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="fallback_model">Fallover Model</Label>
|
<Label htmlFor="fallback_model">Fallover Model</Label>
|
||||||
@@ -420,9 +403,7 @@ export function AgentTypeForm({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">0 = deterministic, 2 = creative</p>
|
||||||
0 = deterministic, 2 = creative
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="max_tokens">Max Tokens</Label>
|
<Label htmlFor="max_tokens">Max Tokens</Label>
|
||||||
@@ -472,9 +453,7 @@ export function AgentTypeForm({
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>MCP Server Permissions</CardTitle>
|
<CardTitle>MCP Server Permissions</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>Configure which MCP servers this agent can access</CardDescription>
|
||||||
Configure which MCP servers this agent can access
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
{AVAILABLE_MCP_SERVERS.map((server) => (
|
{AVAILABLE_MCP_SERVERS.map((server) => (
|
||||||
@@ -508,8 +487,8 @@ export function AgentTypeForm({
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Personality Prompt</CardTitle>
|
<CardTitle>Personality Prompt</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Define the agent's personality, behavior, and communication style. This
|
Define the agent's personality, behavior, and communication style. This prompt
|
||||||
prompt shapes how the agent approaches tasks and interacts.
|
shapes how the agent approaches tasks and interacts.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -535,9 +514,7 @@ export function AgentTypeForm({
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||||
<span>
|
<span>Character count: {watch('personality_prompt')?.length || 0}</span>
|
||||||
Character count: {watch('personality_prompt')?.length || 0}
|
|
||||||
</span>
|
|
||||||
<Separator orientation="vertical" className="h-4" />
|
<Separator orientation="vertical" className="h-4" />
|
||||||
<span className="text-xs">
|
<span className="text-xs">
|
||||||
Tip: Be specific about expertise, communication style, and decision-making
|
Tip: Be specific about expertise, communication style, and decision-making
|
||||||
|
|||||||
@@ -41,13 +41,19 @@ interface AgentTypeListProps {
|
|||||||
function AgentTypeStatusBadge({ isActive }: { isActive: boolean }) {
|
function AgentTypeStatusBadge({ isActive }: { isActive: boolean }) {
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
return (
|
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
|
Active
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
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
|
Inactive
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,13 +12,7 @@
|
|||||||
import { Component, type ReactNode } from 'react';
|
import { Component, type ReactNode } from 'react';
|
||||||
import { AlertTriangle, RefreshCw } from 'lucide-react';
|
import { AlertTriangle, RefreshCw } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from '@/components/ui/card';
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Types
|
// Types
|
||||||
@@ -59,25 +53,17 @@ function DefaultFallback({ error, onReset, showReset }: DefaultFallbackProps) {
|
|||||||
Something went wrong
|
Something went wrong
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
An unexpected error occurred. Please try again or contact support if
|
An unexpected error occurred. Please try again or contact support if the problem persists.
|
||||||
the problem persists.
|
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{error && (
|
{error && (
|
||||||
<div className="mb-4 rounded-md bg-muted p-3">
|
<div className="mb-4 rounded-md bg-muted p-3">
|
||||||
<p className="font-mono text-sm text-muted-foreground">
|
<p className="font-mono text-sm text-muted-foreground">{error.message}</p>
|
||||||
{error.message}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{showReset && (
|
{showReset && (
|
||||||
<Button
|
<Button variant="outline" size="sm" onClick={onReset} className="gap-2">
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={onReset}
|
|
||||||
className="gap-2"
|
|
||||||
>
|
|
||||||
<RefreshCw className="h-4 w-4" aria-hidden="true" />
|
<RefreshCw className="h-4 w-4" aria-hidden="true" />
|
||||||
Try again
|
Try again
|
||||||
</Button>
|
</Button>
|
||||||
@@ -108,10 +94,7 @@ function DefaultFallback({ error, onReset, showReset }: DefaultFallbackProps) {
|
|||||||
* </ErrorBoundary>
|
* </ErrorBoundary>
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export class ErrorBoundary extends Component<
|
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||||
ErrorBoundaryProps,
|
|
||||||
ErrorBoundaryState
|
|
||||||
> {
|
|
||||||
constructor(props: ErrorBoundaryProps) {
|
constructor(props: ErrorBoundaryProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { hasError: false, error: null };
|
this.state = { hasError: false, error: null };
|
||||||
@@ -142,13 +125,7 @@ export class ErrorBoundary extends Component<
|
|||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <DefaultFallback error={error} onReset={this.handleReset} showReset={showReset} />;
|
||||||
<DefaultFallback
|
|
||||||
error={error}
|
|
||||||
onReset={this.handleReset}
|
|
||||||
showReset={showReset}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return children;
|
return children;
|
||||||
|
|||||||
@@ -153,7 +153,8 @@ export function ConnectionStatus({
|
|||||||
className={cn(
|
className={cn(
|
||||||
'flex flex-col gap-3 rounded-lg border p-4',
|
'flex flex-col gap-3 rounded-lg border p-4',
|
||||||
state === 'error' && 'border-destructive bg-destructive/5',
|
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
|
className
|
||||||
)}
|
)}
|
||||||
role="status"
|
role="status"
|
||||||
@@ -199,11 +200,7 @@ export function ConnectionStatus({
|
|||||||
{showErrorDetails && error && (
|
{showErrorDetails && error && (
|
||||||
<div className="rounded-md bg-destructive/10 p-3 text-sm">
|
<div className="rounded-md bg-destructive/10 p-3 text-sm">
|
||||||
<p className="font-medium text-destructive">Error: {error.message}</p>
|
<p className="font-medium text-destructive">Error: {error.message}</p>
|
||||||
{error.code && (
|
{error.code && <p className="mt-1 text-muted-foreground">Code: {error.code}</p>}
|
||||||
<p className="mt-1 text-muted-foreground">
|
|
||||||
Code: {error.code}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<p className="mt-1 text-xs text-muted-foreground">
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
{new Date(error.timestamp).toLocaleTimeString()}
|
{new Date(error.timestamp).toLocaleTimeString()}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -250,17 +250,11 @@ function getEventSummary(event: ProjectEvent): string {
|
|||||||
? `Assigned to ${payload.assignee_name}`
|
? `Assigned to ${payload.assignee_name}`
|
||||||
: 'Issue assignment changed';
|
: 'Issue assignment changed';
|
||||||
case EventType.ISSUE_CLOSED:
|
case EventType.ISSUE_CLOSED:
|
||||||
return payload.resolution
|
return payload.resolution ? `Closed: ${payload.resolution}` : 'Issue closed';
|
||||||
? `Closed: ${payload.resolution}`
|
|
||||||
: 'Issue closed';
|
|
||||||
case EventType.SPRINT_STARTED:
|
case EventType.SPRINT_STARTED:
|
||||||
return payload.sprint_name
|
return payload.sprint_name ? `Sprint "${payload.sprint_name}" started` : 'Sprint started';
|
||||||
? `Sprint "${payload.sprint_name}" started`
|
|
||||||
: 'Sprint started';
|
|
||||||
case EventType.SPRINT_COMPLETED:
|
case EventType.SPRINT_COMPLETED:
|
||||||
return payload.sprint_name
|
return payload.sprint_name ? `Sprint "${payload.sprint_name}" completed` : 'Sprint completed';
|
||||||
? `Sprint "${payload.sprint_name}" completed`
|
|
||||||
: 'Sprint completed';
|
|
||||||
case EventType.APPROVAL_REQUESTED:
|
case EventType.APPROVAL_REQUESTED:
|
||||||
return String(payload.description || 'Approval requested');
|
return String(payload.description || 'Approval requested');
|
||||||
case EventType.APPROVAL_GRANTED:
|
case EventType.APPROVAL_GRANTED:
|
||||||
@@ -278,9 +272,7 @@ function getEventSummary(event: ProjectEvent): string {
|
|||||||
? `Completed in ${payload.duration_seconds}s`
|
? `Completed in ${payload.duration_seconds}s`
|
||||||
: 'Workflow completed';
|
: 'Workflow completed';
|
||||||
case EventType.WORKFLOW_FAILED:
|
case EventType.WORKFLOW_FAILED:
|
||||||
return payload.error_message
|
return payload.error_message ? String(payload.error_message) : 'Workflow failed';
|
||||||
? String(payload.error_message)
|
|
||||||
: 'Workflow failed';
|
|
||||||
default:
|
default:
|
||||||
return event.type;
|
return event.type;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,8 +72,8 @@ export function HeroSection({ onOpenDemoModal }: HeroSectionProps) {
|
|||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.5, delay: 0.2 }}
|
transition={{ duration: 0.5, delay: 0.2 }}
|
||||||
>
|
>
|
||||||
Opinionated, secure, and production-ready. Syndarix gives you the solid foundation
|
Opinionated, secure, and production-ready. Syndarix gives you the solid foundation you
|
||||||
you need to stop configuring and start shipping.{' '}
|
need to stop configuring and start shipping.{' '}
|
||||||
<span className="text-foreground font-medium">Start building features on day one.</span>
|
<span className="text-foreground font-medium">Start building features on day one.</span>
|
||||||
</motion.p>
|
</motion.p>
|
||||||
|
|
||||||
|
|||||||
@@ -74,11 +74,7 @@ function generateBreadcrumbs(pathname: string): BreadcrumbItem[] {
|
|||||||
return breadcrumbs;
|
return breadcrumbs;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AppBreadcrumbs({
|
export function AppBreadcrumbs({ items, showHome = true, className }: AppBreadcrumbsProps) {
|
||||||
items,
|
|
||||||
showHome = true,
|
|
||||||
className,
|
|
||||||
}: AppBreadcrumbsProps) {
|
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
// Use provided items or generate from pathname
|
// Use provided items or generate from pathname
|
||||||
|
|||||||
@@ -49,11 +49,7 @@ export function AppHeader({
|
|||||||
{/* Left side - Logo and Project Switcher */}
|
{/* Left side - Logo and Project Switcher */}
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
{/* Logo - visible on mobile, hidden on desktop when sidebar is visible */}
|
{/* Logo - visible on mobile, hidden on desktop when sidebar is visible */}
|
||||||
<Link
|
<Link href="/" className="flex items-center gap-2 lg:hidden" aria-label="Syndarix home">
|
||||||
href="/"
|
|
||||||
className="flex items-center gap-2 lg:hidden"
|
|
||||||
aria-label="Syndarix home"
|
|
||||||
>
|
|
||||||
<Image
|
<Image
|
||||||
src="/logo-icon.svg"
|
src="/logo-icon.svg"
|
||||||
alt=""
|
alt=""
|
||||||
|
|||||||
@@ -73,11 +73,7 @@ export function AppLayout({
|
|||||||
{!hideBreadcrumbs && <AppBreadcrumbs items={breadcrumbs} />}
|
{!hideBreadcrumbs && <AppBreadcrumbs items={breadcrumbs} />}
|
||||||
|
|
||||||
{/* Main content */}
|
{/* Main content */}
|
||||||
<main
|
<main className={cn('flex-1', className)} id="main-content" tabIndex={-1}>
|
||||||
className={cn('flex-1', className)}
|
|
||||||
id="main-content"
|
|
||||||
tabIndex={-1}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
@@ -110,11 +106,7 @@ const maxWidthClasses: Record<string, string> = {
|
|||||||
full: 'max-w-full',
|
full: 'max-w-full',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function PageContainer({
|
export function PageContainer({ children, maxWidth = '6xl', className }: PageContainerProps) {
|
||||||
children,
|
|
||||||
maxWidth = '6xl',
|
|
||||||
className,
|
|
||||||
}: PageContainerProps) {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -144,12 +136,7 @@ interface PageHeaderProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PageHeader({
|
export function PageHeader({ title, description, actions, className }: PageHeaderProps) {
|
||||||
title,
|
|
||||||
description,
|
|
||||||
actions,
|
|
||||||
className,
|
|
||||||
}: PageHeaderProps) {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -160,9 +147,7 @@ export function PageHeader({
|
|||||||
>
|
>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h1 className="text-2xl font-bold tracking-tight sm:text-3xl">{title}</h1>
|
<h1 className="text-2xl font-bold tracking-tight sm:text-3xl">{title}</h1>
|
||||||
{description && (
|
{description && <p className="text-muted-foreground">{description}</p>}
|
||||||
<p className="text-muted-foreground">{description}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{actions && <div className="flex items-center gap-2">{actions}</div>}
|
{actions && <div className="flex items-center gap-2">{actions}</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -98,9 +98,7 @@ export function ProjectSwitcher({
|
|||||||
className={cn('gap-2 min-w-[160px] justify-between', className)}
|
className={cn('gap-2 min-w-[160px] justify-between', className)}
|
||||||
data-testid="project-switcher-trigger"
|
data-testid="project-switcher-trigger"
|
||||||
aria-label={
|
aria-label={
|
||||||
currentProject
|
currentProject ? `Switch project, current: ${currentProject.name}` : 'Select project'
|
||||||
? `Switch project, current: ${currentProject.name}`
|
|
||||||
: 'Select project'
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<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" />
|
<ChevronDown className="h-4 w-4 opacity-50" aria-hidden="true" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent align="start" className="w-[200px]" data-testid="project-switcher-menu">
|
||||||
align="start"
|
|
||||||
className="w-[200px]"
|
|
||||||
data-testid="project-switcher-menu"
|
|
||||||
>
|
|
||||||
<DropdownMenuLabel>Projects</DropdownMenuLabel>
|
<DropdownMenuLabel>Projects</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
{projects.map((project) => (
|
{projects.map((project) => (
|
||||||
|
|||||||
@@ -11,13 +11,7 @@ import { Link } from '@/lib/i18n/routing';
|
|||||||
import { usePathname } from '@/lib/i18n/routing';
|
import { usePathname } from '@/lib/i18n/routing';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
|
||||||
Sheet,
|
|
||||||
SheetContent,
|
|
||||||
SheetHeader,
|
|
||||||
SheetTitle,
|
|
||||||
SheetTrigger,
|
|
||||||
} from '@/components/ui/sheet';
|
|
||||||
import {
|
import {
|
||||||
FolderKanban,
|
FolderKanban,
|
||||||
Bot,
|
Bot,
|
||||||
@@ -113,9 +107,7 @@ function NavLink({ item, collapsed, basePath = '' }: NavLinkProps) {
|
|||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const href = basePath ? `${basePath}${item.href}` : item.href;
|
const href = basePath ? `${basePath}${item.href}` : item.href;
|
||||||
|
|
||||||
const isActive = item.exact
|
const isActive = item.exact ? pathname === href : pathname.startsWith(href);
|
||||||
? pathname === href
|
|
||||||
: pathname.startsWith(href);
|
|
||||||
|
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
|
|
||||||
@@ -155,9 +147,7 @@ function SidebarContent({
|
|||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
{/* Sidebar Header */}
|
{/* Sidebar Header */}
|
||||||
<div className="flex h-14 items-center justify-between border-b px-4">
|
<div className="flex h-14 items-center justify-between border-b px-4">
|
||||||
{!collapsed && (
|
{!collapsed && <span className="text-lg font-semibold text-foreground">Navigation</span>}
|
||||||
<span className="text-lg font-semibold text-foreground">Navigation</span>
|
|
||||||
)}
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
@@ -308,11 +298,7 @@ export function Sidebar({ projectSlug, className }: SidebarProps) {
|
|||||||
data-testid="sidebar"
|
data-testid="sidebar"
|
||||||
aria-label="Main navigation"
|
aria-label="Main navigation"
|
||||||
>
|
>
|
||||||
<SidebarContent
|
<SidebarContent collapsed={collapsed} projectSlug={projectSlug} onToggle={handleToggle} />
|
||||||
collapsed={collapsed}
|
|
||||||
projectSlug={projectSlug}
|
|
||||||
onToggle={handleToggle}
|
|
||||||
/>
|
|
||||||
</aside>
|
</aside>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,14 +20,7 @@ import {
|
|||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
||||||
import {
|
import { User, LogOut, Shield, Lock, Monitor, UserCog } from 'lucide-react';
|
||||||
User,
|
|
||||||
LogOut,
|
|
||||||
Shield,
|
|
||||||
Lock,
|
|
||||||
Monitor,
|
|
||||||
UserCog,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
interface UserMenuProps {
|
interface UserMenuProps {
|
||||||
@@ -76,20 +69,14 @@ export function UserMenu({ className }: UserMenuProps) {
|
|||||||
</Avatar>
|
</Avatar>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent className="w-56" align="end" data-testid="user-menu-content">
|
||||||
className="w-56"
|
|
||||||
align="end"
|
|
||||||
data-testid="user-menu-content"
|
|
||||||
>
|
|
||||||
{/* User info header */}
|
{/* User info header */}
|
||||||
<DropdownMenuLabel className="font-normal">
|
<DropdownMenuLabel className="font-normal">
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="text-sm font-medium leading-none">
|
<p className="text-sm font-medium leading-none">
|
||||||
{user.first_name} {user.last_name}
|
{user.first_name} {user.last_name}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs leading-none text-muted-foreground">
|
<p className="text-xs leading-none text-muted-foreground">{user.email}</p>
|
||||||
{user.email}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
@@ -143,11 +130,7 @@ export function UserMenu({ className }: UserMenuProps) {
|
|||||||
<>
|
<>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link
|
<Link href="/admin" className="cursor-pointer" data-testid="user-menu-admin">
|
||||||
href="/admin"
|
|
||||||
className="cursor-pointer"
|
|
||||||
data-testid="user-menu-admin"
|
|
||||||
>
|
|
||||||
<Shield className="mr-2 h-4 w-4" aria-hidden="true" />
|
<Shield className="mr-2 h-4 w-4" aria-hidden="true" />
|
||||||
{t('adminPanel')}
|
{t('adminPanel')}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -9,13 +9,7 @@
|
|||||||
import { Bot, MoreVertical } from 'lucide-react';
|
import { Bot, MoreVertical } from 'lucide-react';
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from '@/components/ui/card';
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -228,11 +222,7 @@ export function AgentPanel({
|
|||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{agents.map((agent) => (
|
{agents.map((agent) => (
|
||||||
<AgentListItem
|
<AgentListItem key={agent.id} agent={agent} onAction={onAgentAction} />
|
||||||
key={agent.id}
|
|
||||||
agent={agent}
|
|
||||||
onAction={onAgentAction}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -60,16 +60,10 @@ export function AgentStatusIndicator({
|
|||||||
aria-label={`Status: ${config.label}`}
|
aria-label={`Status: ${config.label}`}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn('inline-block rounded-full', sizeClasses[size], config.color)}
|
||||||
'inline-block rounded-full',
|
|
||||||
sizeClasses[size],
|
|
||||||
config.color
|
|
||||||
)}
|
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
{showLabel && (
|
{showLabel && <span className="text-xs text-muted-foreground">{config.label}</span>}
|
||||||
<span className="text-xs text-muted-foreground">{config.label}</span>
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,15 +109,7 @@ export function BurndownChart({
|
|||||||
{data.map((d, i) => {
|
{data.map((d, i) => {
|
||||||
const x = padding.left + (i / (data.length - 1)) * innerWidth;
|
const x = padding.left + (i / (data.length - 1)) * innerWidth;
|
||||||
const y = padding.top + innerHeight - (d.remaining / maxPoints) * innerHeight;
|
const y = padding.top + innerHeight - (d.remaining / maxPoints) * innerHeight;
|
||||||
return (
|
return <circle key={i} cx={x} cy={y} r="2" className="fill-primary" />;
|
||||||
<circle
|
|
||||||
key={i}
|
|
||||||
cx={x}
|
|
||||||
cy={y}
|
|
||||||
r="2"
|
|
||||||
className="fill-primary"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
|
|||||||
@@ -6,22 +6,10 @@
|
|||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import {
|
import { GitBranch, CircleDot, PlayCircle, Clock, AlertCircle, CheckCircle2 } from 'lucide-react';
|
||||||
GitBranch,
|
|
||||||
CircleDot,
|
|
||||||
PlayCircle,
|
|
||||||
Clock,
|
|
||||||
AlertCircle,
|
|
||||||
CheckCircle2,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from '@/components/ui/card';
|
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import type { IssueCountSummary } from './types';
|
import type { IssueCountSummary } from './types';
|
||||||
@@ -141,12 +129,7 @@ export function IssueSummary({
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-3" role="list" aria-label="Issue counts by status">
|
<div className="space-y-3" role="list" aria-label="Issue counts by status">
|
||||||
<StatusRow
|
<StatusRow icon={CircleDot} iconColor="text-blue-500" label="Open" count={summary.open} />
|
||||||
icon={CircleDot}
|
|
||||||
iconColor="text-blue-500"
|
|
||||||
label="Open"
|
|
||||||
count={summary.open}
|
|
||||||
/>
|
|
||||||
<StatusRow
|
<StatusRow
|
||||||
icon={PlayCircle}
|
icon={PlayCircle}
|
||||||
iconColor="text-yellow-500"
|
iconColor="text-yellow-500"
|
||||||
@@ -177,12 +160,7 @@ export function IssueSummary({
|
|||||||
|
|
||||||
{onViewAllIssues && (
|
{onViewAllIssues && (
|
||||||
<div className="pt-2">
|
<div className="pt-2">
|
||||||
<Button
|
<Button variant="outline" className="w-full" size="sm" onClick={onViewAllIssues}>
|
||||||
variant="outline"
|
|
||||||
className="w-full"
|
|
||||||
size="sm"
|
|
||||||
onClick={onViewAllIssues}
|
|
||||||
>
|
|
||||||
View All Issues ({summary.total})
|
View All Issues ({summary.total})
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -85,14 +85,12 @@ export function ProjectHeader({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const showPauseButton = canPause && project.status === 'active';
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn('flex flex-col gap-4 md:flex-row md:items-start md:justify-between', className)}
|
||||||
'flex flex-col gap-4 md:flex-row md:items-start md:justify-between',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
data-testid="project-header"
|
data-testid="project-header"
|
||||||
>
|
>
|
||||||
{/* Project Info */}
|
{/* Project Info */}
|
||||||
@@ -102,20 +100,13 @@ export function ProjectHeader({
|
|||||||
<ProjectStatusBadge status={project.status} />
|
<ProjectStatusBadge status={project.status} />
|
||||||
<AutonomyBadge level={project.autonomy_level} />
|
<AutonomyBadge level={project.autonomy_level} />
|
||||||
</div>
|
</div>
|
||||||
{project.description && (
|
{project.description && <p className="text-muted-foreground">{project.description}</p>}
|
||||||
<p className="text-muted-foreground">{project.description}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quick Actions */}
|
{/* Quick Actions */}
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{onSettings && (
|
{onSettings && (
|
||||||
<Button
|
<Button variant="ghost" size="icon" onClick={onSettings} aria-label="Project settings">
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={onSettings}
|
|
||||||
aria-label="Project settings"
|
|
||||||
>
|
|
||||||
<Settings className="h-4 w-4" />
|
<Settings className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -19,12 +19,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from '@/components/ui/card';
|
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import type { ActivityItem } from './types';
|
import type { ActivityItem } from './types';
|
||||||
|
|
||||||
@@ -104,9 +99,7 @@ function ActivityItemRow({ activity, onActionClick }: ActivityItemRowProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
{activity.agent && (
|
{activity.agent && <span className="font-medium">{activity.agent}</span>}{' '}
|
||||||
<span className="font-medium">{activity.agent}</span>
|
|
||||||
)}{' '}
|
|
||||||
<span className="text-muted-foreground">{activity.message}</span>
|
<span className="text-muted-foreground">{activity.message}</span>
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground">{timestamp}</p>
|
<p className="text-xs text-muted-foreground">{timestamp}</p>
|
||||||
|
|||||||
@@ -9,13 +9,7 @@
|
|||||||
import { TrendingUp, Calendar } from 'lucide-react';
|
import { TrendingUp, Calendar } from 'lucide-react';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import {
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from '@/components/ui/card';
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -188,10 +182,7 @@ export function SprintProgress({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{availableSprints.length > 1 && onSprintChange && (
|
{availableSprints.length > 1 && onSprintChange && (
|
||||||
<Select
|
<Select value={selectedSprintId || sprint.id} onValueChange={onSprintChange}>
|
||||||
value={selectedSprintId || sprint.id}
|
|
||||||
onValueChange={onSprintChange}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-32" aria-label="Select sprint">
|
<SelectTrigger className="w-32" aria-label="Select sprint">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@@ -231,16 +222,8 @@ export function SprintProgress({
|
|||||||
label="In Progress"
|
label="In Progress"
|
||||||
colorClass="text-blue-600"
|
colorClass="text-blue-600"
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard value={sprint.blocked_issues} label="Blocked" colorClass="text-red-600" />
|
||||||
value={sprint.blocked_issues}
|
<StatCard value={sprint.todo_issues} label="To Do" colorClass="text-gray-600" />
|
||||||
label="Blocked"
|
|
||||||
colorClass="text-red-600"
|
|
||||||
/>
|
|
||||||
<StatCard
|
|
||||||
value={sprint.todo_issues}
|
|
||||||
label="To Do"
|
|
||||||
colorClass="text-gray-600"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Burndown Chart */}
|
{/* 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}>
|
<Badge variant="secondary" className={cn('gap-1', className)} title={config.description}>
|
||||||
<CircleDot className="h-3 w-3" aria-hidden="true" />
|
<CircleDot className="h-3 w-3" aria-hidden="true" />
|
||||||
{config.label}
|
{config.label}
|
||||||
{showDescription && (
|
{showDescription && <span className="text-muted-foreground"> - {config.description}</span>}
|
||||||
<span className="text-muted-foreground"> - {config.description}</span>
|
|
||||||
)}
|
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,7 +123,13 @@ export interface IssueCountSummary {
|
|||||||
|
|
||||||
export interface ActivityItem {
|
export interface ActivityItem {
|
||||||
id: string;
|
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;
|
agent?: string;
|
||||||
message: string;
|
message: string;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
|
|||||||
@@ -73,16 +73,13 @@ export function ProjectWizard({ locale, className }: ProjectWizardProps) {
|
|||||||
mutationFn: async (projectData: ProjectCreateData): Promise<ProjectResponse> => {
|
mutationFn: async (projectData: ProjectCreateData): Promise<ProjectResponse> => {
|
||||||
// Call the projects API endpoint
|
// Call the projects API endpoint
|
||||||
// Note: The API client already handles authentication via interceptors
|
// Note: The API client already handles authentication via interceptors
|
||||||
const response = await apiClient.instance.post<ProjectResponse>(
|
const response = await apiClient.instance.post<ProjectResponse>('/api/v1/projects', {
|
||||||
'/api/v1/projects',
|
name: projectData.name,
|
||||||
{
|
slug: projectData.slug,
|
||||||
name: projectData.name,
|
description: projectData.description,
|
||||||
slug: projectData.slug,
|
autonomy_level: projectData.autonomy_level,
|
||||||
description: projectData.description,
|
settings: projectData.settings,
|
||||||
autonomy_level: projectData.autonomy_level,
|
});
|
||||||
settings: projectData.settings,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
@@ -123,7 +120,10 @@ export function ProjectWizard({ locale, className }: ProjectWizardProps) {
|
|||||||
<Card className="text-center">
|
<Card className="text-center">
|
||||||
<CardContent className="space-y-6 p-8">
|
<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">
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold">Project Created Successfully!</h2>
|
<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" />
|
<ArrowRight className="ml-2 h-4 w-4" aria-hidden="true" />
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button onClick={handleCreate} disabled={createProjectMutation.isPending}>
|
||||||
onClick={handleCreate}
|
|
||||||
disabled={createProjectMutation.isPending}
|
|
||||||
>
|
|
||||||
{createProjectMutation.isPending ? (
|
{createProjectMutation.isPending ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden="true" />
|
<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>
|
||||||
<span>{steps[displayStep - 1]}</span>
|
<span>{steps[displayStep - 1]}</span>
|
||||||
</div>
|
</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) => (
|
{Array.from({ length: totalSteps }, (_, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
|
|||||||
@@ -18,9 +18,4 @@ export type {
|
|||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
// Re-export constants
|
// Re-export constants
|
||||||
export {
|
export { complexityOptions, clientModeOptions, autonomyOptions, WIZARD_STEPS } from './constants';
|
||||||
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 { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from '@/components/ui/card';
|
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
|||||||
@@ -11,12 +11,7 @@
|
|||||||
import { Check, AlertCircle } from 'lucide-react';
|
import { Check, AlertCircle } from 'lucide-react';
|
||||||
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import {
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from '@/components/ui/card';
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { SelectableCard } from '../SelectableCard';
|
import { SelectableCard } from '../SelectableCard';
|
||||||
import { autonomyOptions } from '../constants';
|
import { autonomyOptions } from '../constants';
|
||||||
|
|||||||
@@ -34,7 +34,11 @@ export function ClientModeStep({ state, updateState }: ClientModeStepProps) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</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) => {
|
{clientModeOptions.map((option) => {
|
||||||
const Icon = option.icon;
|
const Icon = option.icon;
|
||||||
const isSelected = state.clientMode === option.id;
|
const isSelected = state.clientMode === option.id;
|
||||||
|
|||||||
@@ -39,7 +39,11 @@ export function ComplexityStep({ state, updateState }: ComplexityStepProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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) => {
|
{complexityOptions.map((option) => {
|
||||||
const Icon = option.icon;
|
const Icon = option.icon;
|
||||||
const isSelected = state.complexity === option.id;
|
const isSelected = state.complexity === option.id;
|
||||||
|
|||||||
@@ -8,12 +8,7 @@
|
|||||||
|
|
||||||
import { CheckCircle2 } from 'lucide-react';
|
import { CheckCircle2 } from 'lucide-react';
|
||||||
|
|
||||||
import {
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from '@/components/ui/card';
|
|
||||||
import { complexityOptions, clientModeOptions, autonomyOptions } from '../constants';
|
import { complexityOptions, clientModeOptions, autonomyOptions } from '../constants';
|
||||||
import type { WizardState } from '../types';
|
import type { WizardState } from '../types';
|
||||||
|
|
||||||
|
|||||||
@@ -20,11 +20,7 @@ interface ActivityTimelineProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ActivityTimeline({
|
export function ActivityTimeline({ activities, onAddComment, className }: ActivityTimelineProps) {
|
||||||
activities,
|
|
||||||
onAddComment,
|
|
||||||
className,
|
|
||||||
}: ActivityTimelineProps) {
|
|
||||||
return (
|
return (
|
||||||
<Card className={className}>
|
<Card className={className}>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -43,11 +39,7 @@ export function ActivityTimeline({
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-6" role="list" aria-label="Issue activity">
|
<div className="space-y-6" role="list" aria-label="Issue activity">
|
||||||
{activities.map((item, index) => (
|
{activities.map((item, index) => (
|
||||||
<div
|
<div key={item.id} className="flex gap-4" role="listitem">
|
||||||
key={item.id}
|
|
||||||
className="flex gap-4"
|
|
||||||
role="listitem"
|
|
||||||
>
|
|
||||||
<div className="relative flex flex-col items-center">
|
<div className="relative flex flex-col items-center">
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-muted">
|
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-muted">
|
||||||
{item.actor.type === 'agent' ? (
|
{item.actor.type === 'agent' ? (
|
||||||
@@ -74,9 +66,7 @@ export function ActivityTimeline({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activities.length === 0 && (
|
{activities.length === 0 && (
|
||||||
<div className="py-8 text-center text-muted-foreground">
|
<div className="py-8 text-center text-muted-foreground">No activity yet</div>
|
||||||
No activity yet
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -34,16 +34,11 @@ export function BulkActions({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn('flex items-center gap-4 rounded-lg border bg-muted/50 p-3', className)}
|
||||||
'flex items-center gap-4 rounded-lg border bg-muted/50 p-3',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
role="toolbar"
|
role="toolbar"
|
||||||
aria-label="Bulk actions for selected issues"
|
aria-label="Bulk actions for selected issues"
|
||||||
>
|
>
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">{selectedCount} selected</span>
|
||||||
{selectedCount} selected
|
|
||||||
</span>
|
|
||||||
<Separator orientation="vertical" className="h-6" />
|
<Separator orientation="vertical" className="h-6" />
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button variant="outline" size="sm" onClick={onChangeStatus}>
|
<Button variant="outline" size="sm" onClick={onChangeStatus}>
|
||||||
|
|||||||
@@ -44,9 +44,7 @@ export function IssueDetailPanel({ issue, className }: IssueDetailPanelProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">{issue.assignee.name}</p>
|
<p className="font-medium">{issue.assignee.name}</p>
|
||||||
<p className="text-xs text-muted-foreground capitalize">
|
<p className="text-xs text-muted-foreground capitalize">{issue.assignee.type}</p>
|
||||||
{issue.assignee.type}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -92,9 +90,7 @@ export function IssueDetailPanel({ issue, className }: IssueDetailPanelProps) {
|
|||||||
{issue.due_date && (
|
{issue.due_date && (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground">Due Date</p>
|
<p className="text-sm text-muted-foreground">Due Date</p>
|
||||||
<p className="font-medium">
|
<p className="font-medium">{new Date(issue.due_date).toLocaleDateString()}</p>
|
||||||
{new Date(issue.due_date).toLocaleDateString()}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -136,19 +132,13 @@ export function IssueDetailPanel({ issue, className }: IssueDetailPanelProps) {
|
|||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
{issue.branch && (
|
{issue.branch && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<GitBranch
|
<GitBranch className="h-4 w-4 text-muted-foreground" aria-hidden="true" />
|
||||||
className="h-4 w-4 text-muted-foreground"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<span className="font-mono text-sm">{issue.branch}</span>
|
<span className="font-mono text-sm">{issue.branch}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{issue.pull_request && (
|
{issue.pull_request && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<GitPullRequest
|
<GitPullRequest className="h-4 w-4 text-muted-foreground" aria-hidden="true" />
|
||||||
className="h-4 w-4 text-muted-foreground"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<span className="text-sm">{issue.pull_request}</span>
|
<span className="text-sm">{issue.pull_request}</span>
|
||||||
<Badge variant="outline" className="text-xs">
|
<Badge variant="outline" className="text-xs">
|
||||||
Open
|
Open
|
||||||
|
|||||||
@@ -136,10 +136,7 @@ export function IssueFilters({ filters, onFiltersChange, className }: IssueFilte
|
|||||||
<div className="grid gap-4 sm:grid-cols-4">
|
<div className="grid gap-4 sm:grid-cols-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="priority-filter">Priority</Label>
|
<Label htmlFor="priority-filter">Priority</Label>
|
||||||
<Select
|
<Select value={filters.priority || 'all'} onValueChange={handlePriorityChange}>
|
||||||
value={filters.priority || 'all'}
|
|
||||||
onValueChange={handlePriorityChange}
|
|
||||||
>
|
|
||||||
<SelectTrigger id="priority-filter">
|
<SelectTrigger id="priority-filter">
|
||||||
<SelectValue placeholder="All" />
|
<SelectValue placeholder="All" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@@ -172,10 +169,7 @@ export function IssueFilters({ filters, onFiltersChange, className }: IssueFilte
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="assignee-filter">Assignee</Label>
|
<Label htmlFor="assignee-filter">Assignee</Label>
|
||||||
<Select
|
<Select value={filters.assignee || 'all'} onValueChange={handleAssigneeChange}>
|
||||||
value={filters.assignee || 'all'}
|
|
||||||
onValueChange={handleAssigneeChange}
|
|
||||||
>
|
|
||||||
<SelectTrigger id="assignee-filter">
|
<SelectTrigger id="assignee-filter">
|
||||||
<SelectValue placeholder="All" />
|
<SelectValue placeholder="All" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|||||||
@@ -8,14 +8,7 @@
|
|||||||
* @module features/issues/components/StatusBadge
|
* @module features/issues/components/StatusBadge
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { CircleDot, PlayCircle, Clock, AlertCircle, CheckCircle2, XCircle } from 'lucide-react';
|
||||||
CircleDot,
|
|
||||||
PlayCircle,
|
|
||||||
Clock,
|
|
||||||
AlertCircle,
|
|
||||||
CheckCircle2,
|
|
||||||
XCircle,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import type { IssueStatus } from '../types';
|
import type { IssueStatus } from '../types';
|
||||||
import { STATUS_CONFIG } from '../constants';
|
import { STATUS_CONFIG } from '../constants';
|
||||||
@@ -42,9 +35,7 @@ export function StatusBadge({ status, className, showLabel = true }: StatusBadge
|
|||||||
return (
|
return (
|
||||||
<div className={cn('flex items-center gap-1.5', config.color, className)}>
|
<div className={cn('flex items-center gap-1.5', config.color, className)}>
|
||||||
<Icon className="h-4 w-4" aria-hidden="true" />
|
<Icon className="h-4 w-4" aria-hidden="true" />
|
||||||
{showLabel && (
|
{showLabel && <span className="text-sm font-medium">{config.label}</span>}
|
||||||
<span className="text-sm font-medium">{config.label}</span>
|
|
||||||
)}
|
|
||||||
<span className="sr-only">{config.label}</span>
|
<span className="sr-only">{config.label}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,14 +8,7 @@
|
|||||||
* @module features/issues/components/StatusWorkflow
|
* @module features/issues/components/StatusWorkflow
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { CircleDot, PlayCircle, Clock, AlertCircle, CheckCircle2, XCircle } from 'lucide-react';
|
||||||
CircleDot,
|
|
||||||
PlayCircle,
|
|
||||||
Clock,
|
|
||||||
AlertCircle,
|
|
||||||
CheckCircle2,
|
|
||||||
XCircle,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import type { IssueStatus } from '../types';
|
import type { IssueStatus } from '../types';
|
||||||
@@ -63,18 +56,14 @@ export function StatusWorkflow({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex w-full items-center gap-2 rounded-lg p-2 text-left transition-colors',
|
'flex w-full items-center gap-2 rounded-lg p-2 text-left transition-colors',
|
||||||
isActive
|
isActive ? 'bg-primary/10 text-primary' : 'hover:bg-muted',
|
||||||
? 'bg-primary/10 text-primary'
|
|
||||||
: 'hover:bg-muted',
|
|
||||||
disabled && 'cursor-not-allowed opacity-50'
|
disabled && 'cursor-not-allowed opacity-50'
|
||||||
)}
|
)}
|
||||||
onClick={() => !disabled && onStatusChange(status)}
|
onClick={() => !disabled && onStatusChange(status)}
|
||||||
>
|
>
|
||||||
<Icon className={cn('h-4 w-4', config.color)} aria-hidden="true" />
|
<Icon className={cn('h-4 w-4', config.color)} aria-hidden="true" />
|
||||||
<span className="text-sm">{config.label}</span>
|
<span className="text-sm">{config.label}</span>
|
||||||
{isActive && (
|
{isActive && <CheckCircle2 className="ml-auto h-4 w-4" aria-hidden="true" />}
|
||||||
<CheckCircle2 className="ml-auto h-4 w-4" aria-hidden="true" />
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -18,8 +18,7 @@ export const mockIssues: IssueSummary[] = [
|
|||||||
number: 42,
|
number: 42,
|
||||||
type: 'story',
|
type: 'story',
|
||||||
title: 'Implement user authentication flow',
|
title: 'Implement user authentication flow',
|
||||||
description:
|
description: 'Create complete authentication flow with login, register, and password reset.',
|
||||||
'Create complete authentication flow with login, register, and password reset.',
|
|
||||||
status: 'in_progress',
|
status: 'in_progress',
|
||||||
priority: 'high',
|
priority: 'high',
|
||||||
labels: ['feature', 'auth', 'backend'],
|
labels: ['feature', 'auth', 'backend'],
|
||||||
|
|||||||
@@ -44,12 +44,7 @@ const DEFAULT_PAGE_LIMIT = 20;
|
|||||||
export function useAgentTypes(params: AgentTypeListParams = {}) {
|
export function useAgentTypes(params: AgentTypeListParams = {}) {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
const {
|
const { page = 1, limit = DEFAULT_PAGE_LIMIT, is_active = true, search } = params;
|
||||||
page = 1,
|
|
||||||
limit = DEFAULT_PAGE_LIMIT,
|
|
||||||
is_active = true,
|
|
||||||
search,
|
|
||||||
} = params;
|
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: agentTypeKeys.list({ page, limit, is_active, search }),
|
queryKey: agentTypeKeys.list({ page, limit, is_active, search }),
|
||||||
@@ -152,10 +147,7 @@ export function useUpdateAgentType() {
|
|||||||
},
|
},
|
||||||
onSuccess: (updatedAgentType) => {
|
onSuccess: (updatedAgentType) => {
|
||||||
// Update the cache for this specific agent type
|
// Update the cache for this specific agent type
|
||||||
queryClient.setQueryData(
|
queryClient.setQueryData(agentTypeKeys.detail(updatedAgentType.id), updatedAgentType);
|
||||||
agentTypeKeys.detail(updatedAgentType.id),
|
|
||||||
updatedAgentType
|
|
||||||
);
|
|
||||||
// Invalidate lists to reflect changes
|
// Invalidate lists to reflect changes
|
||||||
queryClient.invalidateQueries({ queryKey: agentTypeKeys.lists() });
|
queryClient.invalidateQueries({ queryKey: agentTypeKeys.lists() });
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,4 +5,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export { useDebounce } from './useDebounce';
|
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;
|
mountedRef.current = false;
|
||||||
cleanup();
|
cleanup();
|
||||||
};
|
};
|
||||||
}, [autoConnect, isAuthenticated, accessToken, projectId, connectionState, connect, disconnect, cleanup]);
|
}, [
|
||||||
|
autoConnect,
|
||||||
|
isAuthenticated,
|
||||||
|
accessToken,
|
||||||
|
projectId,
|
||||||
|
connectionState,
|
||||||
|
connect,
|
||||||
|
disconnect,
|
||||||
|
cleanup,
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
events,
|
events,
|
||||||
|
|||||||
@@ -53,10 +53,7 @@ const modelParamsSchema = z.object({
|
|||||||
* Schema for agent type form fields
|
* Schema for agent type form fields
|
||||||
*/
|
*/
|
||||||
export const agentTypeFormSchema = z.object({
|
export const agentTypeFormSchema = z.object({
|
||||||
name: z
|
name: z.string().min(1, 'Name is required').max(255, 'Name must be less than 255 characters'),
|
||||||
.string()
|
|
||||||
.min(1, 'Name is required')
|
|
||||||
.max(255, 'Name must be less than 255 characters'),
|
|
||||||
|
|
||||||
slug: z
|
slug: z
|
||||||
.string()
|
.string()
|
||||||
|
|||||||
@@ -245,7 +245,10 @@ describe('HomePage', () => {
|
|||||||
const githubLinks = screen.getAllByRole('link', { name: /GitHub/i });
|
const githubLinks = screen.getAllByRole('link', { name: /GitHub/i });
|
||||||
expect(githubLinks.length).toBeGreaterThan(0);
|
expect(githubLinks.length).toBeGreaterThan(0);
|
||||||
// Syndarix uses Gitea for version control
|
// 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'));
|
await user.click(within(eventItem).getByTestId('approve-button'));
|
||||||
|
|
||||||
expect(onApprove).toHaveBeenCalledTimes(1);
|
expect(onApprove).toHaveBeenCalledTimes(1);
|
||||||
expect(onApprove).toHaveBeenCalledWith(
|
expect(onApprove).toHaveBeenCalledWith(expect.objectContaining({ id: 'event-001' }));
|
||||||
expect.objectContaining({ id: 'event-001' })
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls onReject when reject button clicked', async () => {
|
it('calls onReject when reject button clicked', async () => {
|
||||||
@@ -370,9 +368,7 @@ describe('ActivityFeed', () => {
|
|||||||
await user.click(within(eventItem).getByTestId('reject-button'));
|
await user.click(within(eventItem).getByTestId('reject-button'));
|
||||||
|
|
||||||
expect(onReject).toHaveBeenCalledTimes(1);
|
expect(onReject).toHaveBeenCalledTimes(1);
|
||||||
expect(onReject).toHaveBeenCalledWith(
|
expect(onReject).toHaveBeenCalledWith(expect.objectContaining({ id: 'event-001' }));
|
||||||
expect.objectContaining({ id: 'event-001' })
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows pending count badge', () => {
|
it('shows pending count badge', () => {
|
||||||
@@ -440,9 +436,7 @@ describe('ActivityFeed', () => {
|
|||||||
await user.click(screen.getByTestId('event-item-event-001'));
|
await user.click(screen.getByTestId('event-item-event-001'));
|
||||||
|
|
||||||
expect(onEventClick).toHaveBeenCalledTimes(1);
|
expect(onEventClick).toHaveBeenCalledTimes(1);
|
||||||
expect(onEventClick).toHaveBeenCalledWith(
|
expect(onEventClick).toHaveBeenCalledWith(expect.objectContaining({ id: 'event-001' }));
|
||||||
expect.objectContaining({ id: 'event-001' })
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -459,7 +453,9 @@ describe('ActivityFeed', () => {
|
|||||||
|
|
||||||
describe('Accessibility', () => {
|
describe('Accessibility', () => {
|
||||||
it('has proper ARIA labels for interactive elements', () => {
|
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();
|
expect(screen.getByLabelText('Reconnect')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -160,9 +160,7 @@ describe('AgentTypeDetail', () => {
|
|||||||
it('shows not found state when agentType is null', () => {
|
it('shows not found state when agentType is null', () => {
|
||||||
render(<AgentTypeDetail {...defaultProps} agentType={null} isLoading={false} />);
|
render(<AgentTypeDetail {...defaultProps} agentType={null} isLoading={false} />);
|
||||||
expect(screen.getByText('Agent type not found')).toBeInTheDocument();
|
expect(screen.getByText('Agent type not found')).toBeInTheDocument();
|
||||||
expect(
|
expect(screen.getByText('The requested agent type could not be found')).toBeInTheDocument();
|
||||||
screen.getByText('The requested agent type could not be found')
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows danger zone with deactivate button', () => {
|
it('shows danger zone with deactivate button', () => {
|
||||||
@@ -191,9 +189,7 @@ describe('AgentTypeDetail', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('applies custom className', () => {
|
it('applies custom className', () => {
|
||||||
const { container } = render(
|
const { container } = render(<AgentTypeDetail {...defaultProps} className="custom-class" />);
|
||||||
<AgentTypeDetail {...defaultProps} className="custom-class" />
|
|
||||||
);
|
|
||||||
expect(container.firstChild).toHaveClass('custom-class');
|
expect(container.firstChild).toHaveClass('custom-class');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -205,18 +201,13 @@ describe('AgentTypeDetail', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('shows no expertise message when expertise is empty', () => {
|
it('shows no expertise message when expertise is empty', () => {
|
||||||
render(
|
render(<AgentTypeDetail {...defaultProps} agentType={{ ...mockAgentType, expertise: [] }} />);
|
||||||
<AgentTypeDetail {...defaultProps} agentType={{ ...mockAgentType, expertise: [] }} />
|
|
||||||
);
|
|
||||||
expect(screen.getByText('No expertise areas defined')).toBeInTheDocument();
|
expect(screen.getByText('No expertise areas defined')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows "None configured" when no fallback model', () => {
|
it('shows "None configured" when no fallback model', () => {
|
||||||
render(
|
render(
|
||||||
<AgentTypeDetail
|
<AgentTypeDetail {...defaultProps} agentType={{ ...mockAgentType, fallback_models: [] }} />
|
||||||
{...defaultProps}
|
|
||||||
agentType={{ ...mockAgentType, fallback_models: [] }}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
expect(screen.getByText('None configured')).toBeInTheDocument();
|
expect(screen.getByText('None configured')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,9 +36,7 @@ describe('AgentTypeForm', () => {
|
|||||||
it('renders create form title', () => {
|
it('renders create form title', () => {
|
||||||
render(<AgentTypeForm {...defaultProps} />);
|
render(<AgentTypeForm {...defaultProps} />);
|
||||||
expect(screen.getByText('Create Agent Type')).toBeInTheDocument();
|
expect(screen.getByText('Create Agent Type')).toBeInTheDocument();
|
||||||
expect(
|
expect(screen.getByText('Define a new agent type template')).toBeInTheDocument();
|
||||||
screen.getByText('Define a new agent type template')
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders all tabs', () => {
|
it('renders all tabs', () => {
|
||||||
@@ -233,9 +231,7 @@ describe('AgentTypeForm', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('applies custom className', () => {
|
it('applies custom className', () => {
|
||||||
const { container } = render(
|
const { container } = render(<AgentTypeForm {...defaultProps} className="custom-class" />);
|
||||||
<AgentTypeForm {...defaultProps} className="custom-class" />
|
|
||||||
);
|
|
||||||
expect(container.querySelector('form')).toHaveClass('custom-class');
|
expect(container.querySelector('form')).toHaveClass('custom-class');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -143,15 +143,11 @@ describe('AgentTypeList', () => {
|
|||||||
it('shows empty state when no agent types', () => {
|
it('shows empty state when no agent types', () => {
|
||||||
render(<AgentTypeList {...defaultProps} agentTypes={[]} />);
|
render(<AgentTypeList {...defaultProps} agentTypes={[]} />);
|
||||||
expect(screen.getByText('No agent types found')).toBeInTheDocument();
|
expect(screen.getByText('No agent types found')).toBeInTheDocument();
|
||||||
expect(
|
expect(screen.getByText('Create your first agent type to get started')).toBeInTheDocument();
|
||||||
screen.getByText('Create your first agent type to get started')
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows filter hint in empty state when filters are applied', () => {
|
it('shows filter hint in empty state when filters are applied', () => {
|
||||||
render(
|
render(<AgentTypeList {...defaultProps} agentTypes={[]} searchQuery="nonexistent" />);
|
||||||
<AgentTypeList {...defaultProps} agentTypes={[]} searchQuery="nonexistent" />
|
|
||||||
);
|
|
||||||
expect(screen.getByText('Try adjusting your search or filters')).toBeInTheDocument();
|
expect(screen.getByText('Try adjusting your search or filters')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -175,9 +171,7 @@ describe('AgentTypeList', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('applies custom className', () => {
|
it('applies custom className', () => {
|
||||||
const { container } = render(
|
const { container } = render(<AgentTypeList {...defaultProps} className="custom-class" />);
|
||||||
<AgentTypeList {...defaultProps} className="custom-class" />
|
|
||||||
);
|
|
||||||
expect(container.firstChild).toHaveClass('custom-class');
|
expect(container.firstChild).toHaveClass('custom-class');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -182,9 +182,7 @@ describe('ConnectionStatus', () => {
|
|||||||
|
|
||||||
describe('className prop', () => {
|
describe('className prop', () => {
|
||||||
it('applies custom className', () => {
|
it('applies custom className', () => {
|
||||||
const { container } = render(
|
const { container } = render(<ConnectionStatus state="connected" className="custom-class" />);
|
||||||
<ConnectionStatus state="connected" className="custom-class" />
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(container.querySelector('.custom-class')).toBeInTheDocument();
|
expect(container.querySelector('.custom-class')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -71,7 +71,10 @@ describe('CTASection', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const githubLink = screen.getByRole('link', { name: /get started on github/i });
|
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('target', '_blank');
|
||||||
expect(githubLink).toHaveAttribute('rel', 'noopener noreferrer');
|
expect(githubLink).toHaveAttribute('rel', 'noopener noreferrer');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -221,7 +221,10 @@ describe('Header', () => {
|
|||||||
const mobileGithubLink = githubLinks[1];
|
const mobileGithubLink = githubLinks[1];
|
||||||
fireEvent.click(mobileGithubLink);
|
fireEvent.click(mobileGithubLink);
|
||||||
// Syndarix uses Gitea for version control
|
// 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 });
|
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('target', '_blank');
|
||||||
expect(githubLink).toHaveAttribute('rel', 'noopener noreferrer');
|
expect(githubLink).toHaveAttribute('rel', 'noopener noreferrer');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -123,12 +123,7 @@ describe('AppHeader', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('displays current project name', () => {
|
it('displays current project name', () => {
|
||||||
render(
|
render(<AppHeader projects={mockProjects} currentProject={mockProjects[0]} />);
|
||||||
<AppHeader
|
|
||||||
projects={mockProjects}
|
|
||||||
currentProject={mockProjects[0]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
// Multiple instances may show the project name
|
// Multiple instances may show the project name
|
||||||
expect(screen.getAllByText('Project One').length).toBeGreaterThan(0);
|
expect(screen.getAllByText('Project One').length).toBeGreaterThan(0);
|
||||||
@@ -137,12 +132,7 @@ describe('AppHeader', () => {
|
|||||||
it('calls onProjectChange when project is changed', async () => {
|
it('calls onProjectChange when project is changed', async () => {
|
||||||
const mockOnChange = jest.fn();
|
const mockOnChange = jest.fn();
|
||||||
|
|
||||||
render(
|
render(<AppHeader projects={mockProjects} onProjectChange={mockOnChange} />);
|
||||||
<AppHeader
|
|
||||||
projects={mockProjects}
|
|
||||||
onProjectChange={mockOnChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
// The actual test of project switching is in ProjectSwitcher.test.tsx
|
// The actual test of project switching is in ProjectSwitcher.test.tsx
|
||||||
// Here we just verify the prop is passed by checking switcher exists
|
// Here we just verify the prop is passed by checking switcher exists
|
||||||
|
|||||||
@@ -145,9 +145,7 @@ describe('AppLayout', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('passes custom breadcrumbs to AppBreadcrumbs', () => {
|
it('passes custom breadcrumbs to AppBreadcrumbs', () => {
|
||||||
const customBreadcrumbs = [
|
const customBreadcrumbs = [{ label: 'Custom', href: '/custom', current: true }];
|
||||||
{ label: 'Custom', href: '/custom', current: true },
|
|
||||||
];
|
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<AppLayout breadcrumbs={customBreadcrumbs}>
|
<AppLayout breadcrumbs={customBreadcrumbs}>
|
||||||
@@ -344,10 +342,7 @@ describe('PageHeader', () => {
|
|||||||
|
|
||||||
it('renders actions when provided', () => {
|
it('renders actions when provided', () => {
|
||||||
render(
|
render(
|
||||||
<PageHeader
|
<PageHeader title="Title" actions={<button data-testid="action-button">Action</button>} />
|
||||||
title="Title"
|
|
||||||
actions={<button data-testid="action-button">Action</button>}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByTestId('action-button')).toBeInTheDocument();
|
expect(screen.getByTestId('action-button')).toBeInTheDocument();
|
||||||
|
|||||||
@@ -45,12 +45,7 @@ describe('ProjectSwitcher', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('displays current project name', () => {
|
it('displays current project name', () => {
|
||||||
render(
|
render(<ProjectSwitcher projects={mockProjects} currentProject={mockProjects[0]} />);
|
||||||
<ProjectSwitcher
|
|
||||||
projects={mockProjects}
|
|
||||||
currentProject={mockProjects[0]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(screen.getByText('Project One')).toBeInTheDocument();
|
expect(screen.getByText('Project One')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@@ -94,12 +89,7 @@ describe('ProjectSwitcher', () => {
|
|||||||
it('shows current indicator on selected project', async () => {
|
it('shows current indicator on selected project', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
|
|
||||||
render(
|
render(<ProjectSwitcher projects={mockProjects} currentProject={mockProjects[0]} />);
|
||||||
<ProjectSwitcher
|
|
||||||
projects={mockProjects}
|
|
||||||
currentProject={mockProjects[0]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const trigger = screen.getByTestId('project-switcher-trigger');
|
const trigger = screen.getByTestId('project-switcher-trigger');
|
||||||
await user.click(trigger);
|
await user.click(trigger);
|
||||||
@@ -144,12 +134,7 @@ describe('ProjectSwitcher', () => {
|
|||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const mockOnChange = jest.fn();
|
const mockOnChange = jest.fn();
|
||||||
|
|
||||||
render(
|
render(<ProjectSwitcher projects={mockProjects} onProjectChange={mockOnChange} />);
|
||||||
<ProjectSwitcher
|
|
||||||
projects={mockProjects}
|
|
||||||
onProjectChange={mockOnChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const trigger = screen.getByTestId('project-switcher-trigger');
|
const trigger = screen.getByTestId('project-switcher-trigger');
|
||||||
await user.click(trigger);
|
await user.click(trigger);
|
||||||
@@ -189,18 +174,10 @@ describe('ProjectSwitcher', () => {
|
|||||||
|
|
||||||
describe('Accessibility', () => {
|
describe('Accessibility', () => {
|
||||||
it('has accessible label on trigger', () => {
|
it('has accessible label on trigger', () => {
|
||||||
render(
|
render(<ProjectSwitcher projects={mockProjects} currentProject={mockProjects[0]} />);
|
||||||
<ProjectSwitcher
|
|
||||||
projects={mockProjects}
|
|
||||||
currentProject={mockProjects[0]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const trigger = screen.getByTestId('project-switcher-trigger');
|
const trigger = screen.getByTestId('project-switcher-trigger');
|
||||||
expect(trigger).toHaveAttribute(
|
expect(trigger).toHaveAttribute('aria-label', 'Switch project, current: Project One');
|
||||||
'aria-label',
|
|
||||||
'Switch project, current: Project One'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has accessible label when no current project', () => {
|
it('has accessible label when no current project', () => {
|
||||||
@@ -220,12 +197,7 @@ describe('ProjectSelect', () => {
|
|||||||
|
|
||||||
describe('Rendering', () => {
|
describe('Rendering', () => {
|
||||||
it('renders select component', () => {
|
it('renders select component', () => {
|
||||||
render(
|
render(<ProjectSelect projects={mockProjects} onValueChange={jest.fn()} />);
|
||||||
<ProjectSelect
|
|
||||||
projects={mockProjects}
|
|
||||||
onValueChange={jest.fn()}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(screen.getByTestId('project-select')).toBeInTheDocument();
|
expect(screen.getByTestId('project-select')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@@ -243,23 +215,14 @@ describe('ProjectSelect', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('has combobox role', () => {
|
it('has combobox role', () => {
|
||||||
render(
|
render(<ProjectSelect projects={mockProjects} onValueChange={jest.fn()} />);
|
||||||
<ProjectSelect
|
|
||||||
projects={mockProjects}
|
|
||||||
onValueChange={jest.fn()}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('applies custom className', () => {
|
it('applies custom className', () => {
|
||||||
render(
|
render(
|
||||||
<ProjectSelect
|
<ProjectSelect projects={mockProjects} onValueChange={jest.fn()} className="custom-class" />
|
||||||
projects={mockProjects}
|
|
||||||
onValueChange={jest.fn()}
|
|
||||||
className="custom-class"
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const select = screen.getByTestId('project-select');
|
const select = screen.getByTestId('project-select');
|
||||||
|
|||||||
@@ -74,7 +74,12 @@ const mockUseProjectEventsDefault = {
|
|||||||
events: [] as ProjectEvent[],
|
events: [] as ProjectEvent[],
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
connectionState: 'connected' as ConnectionState,
|
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,
|
retryCount: 0,
|
||||||
reconnect: mockReconnect,
|
reconnect: mockReconnect,
|
||||||
disconnect: mockDisconnect,
|
disconnect: mockDisconnect,
|
||||||
@@ -389,11 +394,7 @@ describe('Event to Activity Conversion', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('handles system actor type', () => {
|
it('handles system actor type', () => {
|
||||||
const event = createMockEvent(
|
const event = createMockEvent(EventType.SPRINT_STARTED, { sprint_name: 'Sprint 5' }, 'system');
|
||||||
EventType.SPRINT_STARTED,
|
|
||||||
{ sprint_name: 'Sprint 5' },
|
|
||||||
'system'
|
|
||||||
);
|
|
||||||
mockUseProjectEventsResult.events = [event];
|
mockUseProjectEventsResult.events = [event];
|
||||||
render(<ProjectDashboard projectId="test" />);
|
render(<ProjectDashboard projectId="test" />);
|
||||||
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();
|
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', () => {
|
it('shows pause button when canPause is true and project is active', () => {
|
||||||
const onPauseProject = jest.fn();
|
const onPauseProject = jest.fn();
|
||||||
render(
|
render(<ProjectHeader project={mockProject} canPause={true} onPauseProject={onPauseProject} />);
|
||||||
<ProjectHeader
|
|
||||||
project={mockProject}
|
|
||||||
canPause={true}
|
|
||||||
onPauseProject={onPauseProject}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
expect(screen.getByRole('button', { name: /pause project/i })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: /pause project/i })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -64,13 +58,7 @@ describe('ProjectHeader', () => {
|
|||||||
|
|
||||||
it('shows run sprint button when canStart is true', () => {
|
it('shows run sprint button when canStart is true', () => {
|
||||||
const onStartSprint = jest.fn();
|
const onStartSprint = jest.fn();
|
||||||
render(
|
render(<ProjectHeader project={mockProject} canStart={true} onStartSprint={onStartSprint} />);
|
||||||
<ProjectHeader
|
|
||||||
project={mockProject}
|
|
||||||
canStart={true}
|
|
||||||
onStartSprint={onStartSprint}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
expect(screen.getByRole('button', { name: /run sprint/i })).toBeInTheDocument();
|
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 () => {
|
it('calls onStartSprint when run sprint button is clicked', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const onStartSprint = jest.fn();
|
const onStartSprint = jest.fn();
|
||||||
render(
|
render(<ProjectHeader project={mockProject} canStart={true} onStartSprint={onStartSprint} />);
|
||||||
<ProjectHeader
|
|
||||||
project={mockProject}
|
|
||||||
canStart={true}
|
|
||||||
onStartSprint={onStartSprint}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
await user.click(screen.getByRole('button', { name: /run sprint/i }));
|
await user.click(screen.getByRole('button', { name: /run sprint/i }));
|
||||||
expect(onStartSprint).toHaveBeenCalledTimes(1);
|
expect(onStartSprint).toHaveBeenCalledTimes(1);
|
||||||
@@ -98,13 +80,7 @@ describe('ProjectHeader', () => {
|
|||||||
it('calls onPauseProject when pause button is clicked', async () => {
|
it('calls onPauseProject when pause button is clicked', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const onPauseProject = jest.fn();
|
const onPauseProject = jest.fn();
|
||||||
render(
|
render(<ProjectHeader project={mockProject} canPause={true} onPauseProject={onPauseProject} />);
|
||||||
<ProjectHeader
|
|
||||||
project={mockProject}
|
|
||||||
canPause={true}
|
|
||||||
onPauseProject={onPauseProject}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
await user.click(screen.getByRole('button', { name: /pause project/i }));
|
await user.click(screen.getByRole('button', { name: /pause project/i }));
|
||||||
expect(onPauseProject).toHaveBeenCalledTimes(1);
|
expect(onPauseProject).toHaveBeenCalledTimes(1);
|
||||||
@@ -113,12 +89,7 @@ describe('ProjectHeader', () => {
|
|||||||
it('calls onCreateSprint when new sprint button is clicked', async () => {
|
it('calls onCreateSprint when new sprint button is clicked', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const onCreateSprint = jest.fn();
|
const onCreateSprint = jest.fn();
|
||||||
render(
|
render(<ProjectHeader project={mockProject} onCreateSprint={onCreateSprint} />);
|
||||||
<ProjectHeader
|
|
||||||
project={mockProject}
|
|
||||||
onCreateSprint={onCreateSprint}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
await user.click(screen.getByRole('button', { name: /new sprint/i }));
|
await user.click(screen.getByRole('button', { name: /new sprint/i }));
|
||||||
expect(onCreateSprint).toHaveBeenCalledTimes(1);
|
expect(onCreateSprint).toHaveBeenCalledTimes(1);
|
||||||
@@ -127,12 +98,7 @@ describe('ProjectHeader', () => {
|
|||||||
it('calls onSettings when settings button is clicked', async () => {
|
it('calls onSettings when settings button is clicked', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const onSettings = jest.fn();
|
const onSettings = jest.fn();
|
||||||
render(
|
render(<ProjectHeader project={mockProject} onSettings={onSettings} />);
|
||||||
<ProjectHeader
|
|
||||||
project={mockProject}
|
|
||||||
onSettings={onSettings}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
await user.click(screen.getByRole('button', { name: /project settings/i }));
|
await user.click(screen.getByRole('button', { name: /project settings/i }));
|
||||||
expect(onSettings).toHaveBeenCalledTimes(1);
|
expect(onSettings).toHaveBeenCalledTimes(1);
|
||||||
|
|||||||
@@ -65,38 +65,20 @@ describe('RecentActivity', () => {
|
|||||||
|
|
||||||
it('shows View All button when there are more activities than maxItems', () => {
|
it('shows View All button when there are more activities than maxItems', () => {
|
||||||
const onViewAll = jest.fn();
|
const onViewAll = jest.fn();
|
||||||
render(
|
render(<RecentActivity activities={mockActivities} maxItems={2} onViewAll={onViewAll} />);
|
||||||
<RecentActivity
|
|
||||||
activities={mockActivities}
|
|
||||||
maxItems={2}
|
|
||||||
onViewAll={onViewAll}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
expect(screen.getByRole('button', { name: /view all/i })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: /view all/i })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not show View All button when all activities are shown', () => {
|
it('does not show View All button when all activities are shown', () => {
|
||||||
const onViewAll = jest.fn();
|
const onViewAll = jest.fn();
|
||||||
render(
|
render(<RecentActivity activities={mockActivities} maxItems={5} onViewAll={onViewAll} />);
|
||||||
<RecentActivity
|
|
||||||
activities={mockActivities}
|
|
||||||
maxItems={5}
|
|
||||||
onViewAll={onViewAll}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
expect(screen.queryByRole('button', { name: /view all/i })).not.toBeInTheDocument();
|
expect(screen.queryByRole('button', { name: /view all/i })).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls onViewAll when View All button is clicked', async () => {
|
it('calls onViewAll when View All button is clicked', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const onViewAll = jest.fn();
|
const onViewAll = jest.fn();
|
||||||
render(
|
render(<RecentActivity activities={mockActivities} maxItems={2} onViewAll={onViewAll} />);
|
||||||
<RecentActivity
|
|
||||||
activities={mockActivities}
|
|
||||||
maxItems={2}
|
|
||||||
onViewAll={onViewAll}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
await user.click(screen.getByRole('button', { name: /view all/i }));
|
await user.click(screen.getByRole('button', { name: /view all/i }));
|
||||||
expect(onViewAll).toHaveBeenCalledTimes(1);
|
expect(onViewAll).toHaveBeenCalledTimes(1);
|
||||||
@@ -104,24 +86,14 @@ describe('RecentActivity', () => {
|
|||||||
|
|
||||||
it('shows Review Request button for items requiring action', () => {
|
it('shows Review Request button for items requiring action', () => {
|
||||||
const onActionClick = jest.fn();
|
const onActionClick = jest.fn();
|
||||||
render(
|
render(<RecentActivity activities={mockActivities} onActionClick={onActionClick} />);
|
||||||
<RecentActivity
|
|
||||||
activities={mockActivities}
|
|
||||||
onActionClick={onActionClick}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
expect(screen.getByRole('button', { name: /review request/i })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: /review request/i })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls onActionClick when Review Request button is clicked', async () => {
|
it('calls onActionClick when Review Request button is clicked', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const onActionClick = jest.fn();
|
const onActionClick = jest.fn();
|
||||||
render(
|
render(<RecentActivity activities={mockActivities} onActionClick={onActionClick} />);
|
||||||
<RecentActivity
|
|
||||||
activities={mockActivities}
|
|
||||||
onActionClick={onActionClick}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
await user.click(screen.getByRole('button', { name: /review request/i }));
|
await user.click(screen.getByRole('button', { name: /review request/i }));
|
||||||
expect(onActionClick).toHaveBeenCalledWith('act-003');
|
expect(onActionClick).toHaveBeenCalledWith('act-003');
|
||||||
|
|||||||
@@ -23,9 +23,7 @@ describe('ProjectStatusBadge', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('applies custom className', () => {
|
it('applies custom className', () => {
|
||||||
const { container } = render(
|
const { container } = render(<ProjectStatusBadge status="active" className="custom-class" />);
|
||||||
<ProjectStatusBadge status="active" className="custom-class" />
|
|
||||||
);
|
|
||||||
expect(container.firstChild).toHaveClass('custom-class');
|
expect(container.firstChild).toHaveClass('custom-class');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -54,9 +52,7 @@ describe('AutonomyBadge', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('applies custom className', () => {
|
it('applies custom className', () => {
|
||||||
const { container } = render(
|
const { container } = render(<AutonomyBadge level="milestone" className="custom-class" />);
|
||||||
<AutonomyBadge level="milestone" className="custom-class" />
|
|
||||||
);
|
|
||||||
expect(container.firstChild).toHaveClass('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 user.click(screen.getByRole('button', { name: /create project/i }));
|
||||||
|
|
||||||
await waitFor(() => {
|
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 }));
|
await user.click(screen.getByRole('button', { name: /go to project dashboard/i }));
|
||||||
|
|||||||
@@ -60,7 +60,9 @@ describe('AutonomyStep', () => {
|
|||||||
|
|
||||||
it('has accessible radiogroup role', () => {
|
it('has accessible radiogroup role', () => {
|
||||||
render(<AutonomyStep state={defaultState} updateState={mockUpdateState} />);
|
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();
|
const user = userEvent.setup();
|
||||||
render(<AutonomyStep state={defaultState} updateState={mockUpdateState} />);
|
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);
|
await user.click(fullControlOption);
|
||||||
|
|
||||||
expect(mockUpdateState).toHaveBeenCalledWith({ autonomyLevel: 'full_control' });
|
expect(mockUpdateState).toHaveBeenCalledWith({ autonomyLevel: 'full_control' });
|
||||||
@@ -89,7 +93,9 @@ describe('AutonomyStep', () => {
|
|||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
render(<AutonomyStep state={defaultState} updateState={mockUpdateState} />);
|
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);
|
await user.click(autonomousOption);
|
||||||
|
|
||||||
expect(mockUpdateState).toHaveBeenCalledWith({ autonomyLevel: 'autonomous' });
|
expect(mockUpdateState).toHaveBeenCalledWith({ autonomyLevel: 'autonomous' });
|
||||||
@@ -180,7 +186,7 @@ describe('AutonomyStep', () => {
|
|||||||
|
|
||||||
autonomyOptions.forEach((option) => {
|
autonomyOptions.forEach((option) => {
|
||||||
const button = screen.getByRole('button', {
|
const button = screen.getByRole('button', {
|
||||||
name: new RegExp(`${option.label}.*${option.description}`, 'i')
|
name: new RegExp(`${option.label}.*${option.description}`, 'i'),
|
||||||
});
|
});
|
||||||
expect(button).toBeInTheDocument();
|
expect(button).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@@ -207,7 +213,9 @@ describe('AutonomyStep', () => {
|
|||||||
};
|
};
|
||||||
render(<AutonomyStep state={stateWithFullControl} updateState={mockUpdateState} />);
|
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);
|
await user.click(autonomousOption);
|
||||||
|
|
||||||
expect(mockUpdateState).toHaveBeenCalledWith({ autonomyLevel: 'autonomous' });
|
expect(mockUpdateState).toHaveBeenCalledWith({ autonomyLevel: 'autonomous' });
|
||||||
|
|||||||
@@ -59,7 +59,9 @@ describe('ClientModeStep', () => {
|
|||||||
|
|
||||||
it('has accessible radiogroup role', () => {
|
it('has accessible radiogroup role', () => {
|
||||||
render(<ClientModeStep state={defaultState} updateState={mockUpdateState} />);
|
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();
|
const user = userEvent.setup();
|
||||||
render(<ClientModeStep state={defaultState} updateState={mockUpdateState} />);
|
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);
|
await user.click(technicalOption);
|
||||||
|
|
||||||
expect(mockUpdateState).toHaveBeenCalledWith({ clientMode: 'technical' });
|
expect(mockUpdateState).toHaveBeenCalledWith({ clientMode: 'technical' });
|
||||||
@@ -123,7 +127,7 @@ describe('ClientModeStep', () => {
|
|||||||
|
|
||||||
clientModeOptions.forEach((option) => {
|
clientModeOptions.forEach((option) => {
|
||||||
const button = screen.getByRole('button', {
|
const button = screen.getByRole('button', {
|
||||||
name: new RegExp(`${option.label}.*${option.description}`, 'i')
|
name: new RegExp(`${option.label}.*${option.description}`, 'i'),
|
||||||
});
|
});
|
||||||
expect(button).toBeInTheDocument();
|
expect(button).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@@ -168,7 +172,9 @@ describe('ClientModeStep', () => {
|
|||||||
};
|
};
|
||||||
render(<ClientModeStep state={stateWithTechnical} updateState={mockUpdateState} />);
|
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);
|
await user.click(technicalOption);
|
||||||
|
|
||||||
// Should still call updateState
|
// Should still call updateState
|
||||||
|
|||||||
@@ -65,7 +65,9 @@ describe('ComplexityStep', () => {
|
|||||||
|
|
||||||
it('has accessible radiogroup role', () => {
|
it('has accessible radiogroup role', () => {
|
||||||
render(<ComplexityStep state={defaultState} updateState={mockUpdateState} />);
|
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) => {
|
complexityOptions.forEach((option) => {
|
||||||
const button = screen.getByRole('button', {
|
const button = screen.getByRole('button', {
|
||||||
name: new RegExp(`${option.label}.*${option.description}`, 'i')
|
name: new RegExp(`${option.label}.*${option.description}`, 'i'),
|
||||||
});
|
});
|
||||||
expect(button).toBeInTheDocument();
|
expect(button).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -211,9 +211,7 @@ describe('IssueDetailPanel', () => {
|
|||||||
it('renders labels without color property', () => {
|
it('renders labels without color property', () => {
|
||||||
const issueWithColorlessLabels: IssueDetail = {
|
const issueWithColorlessLabels: IssueDetail = {
|
||||||
...defaultIssue,
|
...defaultIssue,
|
||||||
labels: [
|
labels: [{ id: 'lbl-1', name: 'colorless-label' }],
|
||||||
{ id: 'lbl-1', name: 'colorless-label' },
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
render(<IssueDetailPanel issue={issueWithColorlessLabels} />);
|
render(<IssueDetailPanel issue={issueWithColorlessLabels} />);
|
||||||
expect(screen.getByText('colorless-label')).toBeInTheDocument();
|
expect(screen.getByText('colorless-label')).toBeInTheDocument();
|
||||||
|
|||||||
@@ -14,9 +14,7 @@ describe('StatusWorkflow', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders all status options', () => {
|
it('renders all status options', () => {
|
||||||
render(
|
render(<StatusWorkflow currentStatus="open" onStatusChange={mockOnStatusChange} />);
|
||||||
<StatusWorkflow currentStatus="open" onStatusChange={mockOnStatusChange} />
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(screen.getByText('Open')).toBeInTheDocument();
|
expect(screen.getByText('Open')).toBeInTheDocument();
|
||||||
expect(screen.getByText('In Progress')).toBeInTheDocument();
|
expect(screen.getByText('In Progress')).toBeInTheDocument();
|
||||||
@@ -26,9 +24,7 @@ describe('StatusWorkflow', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('highlights current status', () => {
|
it('highlights current status', () => {
|
||||||
render(
|
render(<StatusWorkflow currentStatus="in_progress" onStatusChange={mockOnStatusChange} />);
|
||||||
<StatusWorkflow currentStatus="in_progress" onStatusChange={mockOnStatusChange} />
|
|
||||||
);
|
|
||||||
|
|
||||||
const inProgressButton = screen.getByRole('radio', { name: /in progress/i });
|
const inProgressButton = screen.getByRole('radio', { name: /in progress/i });
|
||||||
expect(inProgressButton).toHaveAttribute('aria-checked', 'true');
|
expect(inProgressButton).toHaveAttribute('aria-checked', 'true');
|
||||||
@@ -36,9 +32,7 @@ describe('StatusWorkflow', () => {
|
|||||||
|
|
||||||
it('calls onStatusChange when status is clicked', async () => {
|
it('calls onStatusChange when status is clicked', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
render(
|
render(<StatusWorkflow currentStatus="open" onStatusChange={mockOnStatusChange} />);
|
||||||
<StatusWorkflow currentStatus="open" onStatusChange={mockOnStatusChange} />
|
|
||||||
);
|
|
||||||
|
|
||||||
const inProgressButton = screen.getByRole('radio', { name: /in progress/i });
|
const inProgressButton = screen.getByRole('radio', { name: /in progress/i });
|
||||||
await user.click(inProgressButton);
|
await user.click(inProgressButton);
|
||||||
@@ -48,9 +42,7 @@ describe('StatusWorkflow', () => {
|
|||||||
|
|
||||||
it('disables status buttons when disabled prop is true', async () => {
|
it('disables status buttons when disabled prop is true', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
render(
|
render(<StatusWorkflow currentStatus="open" onStatusChange={mockOnStatusChange} disabled />);
|
||||||
<StatusWorkflow currentStatus="open" onStatusChange={mockOnStatusChange} disabled />
|
|
||||||
);
|
|
||||||
|
|
||||||
const inProgressButton = screen.getByRole('radio', { name: /in progress/i });
|
const inProgressButton = screen.getByRole('radio', { name: /in progress/i });
|
||||||
expect(inProgressButton).toBeDisabled();
|
expect(inProgressButton).toBeDisabled();
|
||||||
@@ -72,9 +64,7 @@ describe('StatusWorkflow', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('has proper radiogroup role', () => {
|
it('has proper radiogroup role', () => {
|
||||||
render(
|
render(<StatusWorkflow currentStatus="open" onStatusChange={mockOnStatusChange} />);
|
||||||
<StatusWorkflow currentStatus="open" onStatusChange={mockOnStatusChange} />
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(screen.getByRole('radiogroup', { name: /issue status/i })).toBeInTheDocument();
|
expect(screen.getByRole('radiogroup', { name: /issue status/i })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,10 +16,9 @@ describe('useDebounce', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('updates the debounced value after the delay', () => {
|
it('updates the debounced value after the delay', () => {
|
||||||
const { result, rerender } = renderHook(
|
const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), {
|
||||||
({ value, delay }) => useDebounce(value, delay),
|
initialProps: { value: 'initial', delay: 500 },
|
||||||
{ initialProps: { value: 'initial', delay: 500 } }
|
});
|
||||||
);
|
|
||||||
|
|
||||||
// Change the value
|
// Change the value
|
||||||
rerender({ value: 'updated', delay: 500 });
|
rerender({ value: 'updated', delay: 500 });
|
||||||
@@ -37,10 +36,9 @@ describe('useDebounce', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('does not update the value before the delay', () => {
|
it('does not update the value before the delay', () => {
|
||||||
const { result, rerender } = renderHook(
|
const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), {
|
||||||
({ value, delay }) => useDebounce(value, delay),
|
initialProps: { value: 'initial', delay: 500 },
|
||||||
{ initialProps: { value: 'initial', delay: 500 } }
|
});
|
||||||
);
|
|
||||||
|
|
||||||
rerender({ value: 'updated', delay: 500 });
|
rerender({ value: 'updated', delay: 500 });
|
||||||
|
|
||||||
@@ -53,10 +51,9 @@ describe('useDebounce', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('resets the timer when value changes rapidly', () => {
|
it('resets the timer when value changes rapidly', () => {
|
||||||
const { result, rerender } = renderHook(
|
const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), {
|
||||||
({ value, delay }) => useDebounce(value, delay),
|
initialProps: { value: 'initial', delay: 500 },
|
||||||
{ initialProps: { value: 'initial', delay: 500 } }
|
});
|
||||||
);
|
|
||||||
|
|
||||||
// First change
|
// First change
|
||||||
rerender({ value: 'first', delay: 500 });
|
rerender({ value: 'first', delay: 500 });
|
||||||
@@ -89,10 +86,9 @@ describe('useDebounce', () => {
|
|||||||
it('cleans up timeout on unmount', () => {
|
it('cleans up timeout on unmount', () => {
|
||||||
const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout');
|
const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout');
|
||||||
|
|
||||||
const { unmount, rerender } = renderHook(
|
const { unmount, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), {
|
||||||
({ value, delay }) => useDebounce(value, delay),
|
initialProps: { value: 'initial', delay: 500 },
|
||||||
{ initialProps: { value: 'initial', delay: 500 } }
|
});
|
||||||
);
|
|
||||||
|
|
||||||
rerender({ value: 'updated', delay: 500 });
|
rerender({ value: 'updated', delay: 500 });
|
||||||
unmount();
|
unmount();
|
||||||
@@ -102,10 +98,9 @@ describe('useDebounce', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('works with different delay values', () => {
|
it('works with different delay values', () => {
|
||||||
const { result, rerender } = renderHook(
|
const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), {
|
||||||
({ value, delay }) => useDebounce(value, delay),
|
initialProps: { value: 'initial', delay: 1000 },
|
||||||
{ initialProps: { value: 'initial', delay: 1000 } }
|
});
|
||||||
);
|
|
||||||
|
|
||||||
rerender({ value: 'updated', delay: 1000 });
|
rerender({ value: 'updated', delay: 1000 });
|
||||||
|
|
||||||
@@ -138,10 +133,9 @@ describe('useDebounce', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('handles zero delay', () => {
|
it('handles zero delay', () => {
|
||||||
const { result, rerender } = renderHook(
|
const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), {
|
||||||
({ value, delay }) => useDebounce(value, delay),
|
initialProps: { value: 'initial', delay: 0 },
|
||||||
{ initialProps: { value: 'initial', delay: 0 } }
|
});
|
||||||
);
|
|
||||||
|
|
||||||
rerender({ value: 'updated', delay: 0 });
|
rerender({ value: 'updated', delay: 0 });
|
||||||
|
|
||||||
|
|||||||
@@ -119,9 +119,7 @@ describe('useProjectEvents', () => {
|
|||||||
|
|
||||||
describe('initialization', () => {
|
describe('initialization', () => {
|
||||||
it('should start disconnected', () => {
|
it('should start disconnected', () => {
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() => useProjectEvents('project-123', { autoConnect: false }));
|
||||||
useProjectEvents('project-123', { autoConnect: false })
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result.current.connectionState).toBe('disconnected');
|
expect(result.current.connectionState).toBe('disconnected');
|
||||||
expect(result.current.isConnected).toBe(false);
|
expect(result.current.isConnected).toBe(false);
|
||||||
|
|||||||
@@ -189,10 +189,9 @@ describe('Event Store', () => {
|
|||||||
|
|
||||||
useEventStore.getState().addEvents([agentEvent, issueEvent, sprintEvent]);
|
useEventStore.getState().addEvents([agentEvent, issueEvent, sprintEvent]);
|
||||||
|
|
||||||
const filtered = useEventStore.getState().getFilteredEvents('project-123', [
|
const filtered = useEventStore
|
||||||
EventType.AGENT_MESSAGE,
|
.getState()
|
||||||
EventType.ISSUE_CREATED,
|
.getFilteredEvents('project-123', [EventType.AGENT_MESSAGE, EventType.ISSUE_CREATED]);
|
||||||
]);
|
|
||||||
|
|
||||||
expect(filtered).toHaveLength(2);
|
expect(filtered).toHaveLength(2);
|
||||||
expect(filtered.map((e) => e.type)).toContain(EventType.AGENT_MESSAGE);
|
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', () => {
|
it('should return empty array for non-existent project', () => {
|
||||||
const filtered = useEventStore.getState().getFilteredEvents('non-existent', [
|
const filtered = useEventStore
|
||||||
EventType.AGENT_MESSAGE,
|
.getState()
|
||||||
]);
|
.getFilteredEvents('non-existent', [EventType.AGENT_MESSAGE]);
|
||||||
expect(filtered).toEqual([]);
|
expect(filtered).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user