Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 30 additions & 14 deletions packages/catalog-realm/pr-card/components/isolated/ci-section.gts
Original file line number Diff line number Diff line change
Expand Up @@ -144,31 +144,41 @@ class CiStatusLabel extends GlimmerComponent<CiStatusLabelSignature> {
interface CiSectionSignature {
Args: {
ciGroups: CiGroup[];
isLoading?: boolean;
};
}

export class CiSection extends GlimmerComponent<CiSectionSignature> {
get flatItems() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps you should use @cached so we don't rebuild this everytime this.flatItems is consumed, and only if ciGroups actually changes

return this.args.ciGroups.flatMap((g) => g.items);
}

<template>
<div class='ci-section'>
<h2 class='section-heading'>CI Checks</h2>

{{#if @ciGroups.length}}
{{#if this.flatItems.length}}
<ul class='ci-group' role='list'>
{{#each @ciGroups as |group|}}
{{#each group.items as |item|}}
<li class='ci-item'>
<CiDot @state={{item.state}} />
<div class='ci-item-detail'>
<span class='ci-item-name'>{{item.name}}</span>
<CiStatusLabel
@state={{item.state}}
@text={{item.statusText}}
/>
</div>
</li>
{{/each}}
{{#each this.flatItems key="name" as |item|}}
<li class='ci-item'>
<CiDot @state={{item.state}} />
<div class='ci-item-detail'>
<span class='ci-item-name'>{{item.name}}</span>
<CiStatusLabel
@state={{item.state}}
@text={{item.statusText}}
/>
</div>
</li>
{{/each}}
</ul>
{{else if @isLoading}}
<div class='ci-item loading-state'>
<CiDot @state='in_progress' />
<div class='ci-item-detail'>
<span class='ci-item-name loading-text'>Loading CI checks...</span>
</div>
</div>
Comment on lines +176 to +181
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The loading state renders a standalone <div class='ci-item ...'> instead of keeping the same list semantics used for actual items (<ul role='list'><li ...>). For more consistent accessibility and less layout/semantics churn, consider rendering a <ul> in the loading state as well (with a single <li>), or adding an explicit role="status" / aria-live to the loading container.

Suggested change
<div class='ci-item loading-state'>
<CiDot @state='in_progress' />
<div class='ci-item-detail'>
<span class='ci-item-name loading-text'>Loading CI checks...</span>
</div>
</div>
<ul class='ci-group' role='list'>
<li class='ci-item loading-state'>
<CiDot @state='in_progress' />
<div class='ci-item-detail'>
<span class='ci-item-name loading-text'>Loading CI checks...</span>
</div>
</li>
</ul>

Copilot uses AI. Check for mistakes.
{{else}}
<div class='empty-state'>
<span class='empty-state-icon' aria-hidden='true'>
Expand Down Expand Up @@ -257,6 +267,12 @@ export class CiSection extends GlimmerComponent<CiSectionSignature> {
font-size: var(--boxel-font-xs);
color: var(--muted-foreground, #656d76);
}
.loading-state {
border-radius: var(--radius, 6px);
}
.loading-text {
color: var(--muted-foreground, #656d76);
}
</style>
</template>
}
27 changes: 27 additions & 0 deletions packages/catalog-realm/pr-card/fields/ci-status-field.gts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ export class PrCiStatusField extends FieldDef {
return this.ciItems.length;
}

get isLoading() {
return (
this.checkRunEventData?.isLoading ||
this.checkSuiteEventData?.isLoading
) ?? false;
}

get ciHeadline() {
if (this.ciTotalCount === 0) return null;
if (this.ciFailedCount > 0) return 'Some checks were not successful';
Expand Down Expand Up @@ -127,6 +134,15 @@ export class PrCiStatusField extends FieldDef {
<span class='ci-subtitle'>{{this.ciSubtitle}}</span>
</div>
</div>
{{else if this.isLoading}}
<div class='ci-status-row ci-status-loading'>
<span class='ci-donut ci-donut-loading'>
<span class='ci-donut-hole'></span>
</span>
<div class='ci-status-text'>
<span class='ci-headline'>Loading CI checks...</span>
</div>
</div>
{{/if}}

<style scoped>
Expand Down Expand Up @@ -175,6 +191,17 @@ export class PrCiStatusField extends FieldDef {
overflow: hidden;
text-overflow: ellipsis;
}
.ci-donut-loading {
background: var(--muted-foreground, #656d76);
animation: ci-donut-pulse 1.2s ease-in-out infinite;
}
.ci-status-loading .ci-headline {
color: var(--muted-foreground, #656d76);
}
@keyframes ci-donut-pulse {
0%, 100% { opacity: 0.4; }
50% { opacity: 1; }
}
</style>
</template>
};
Expand Down
9 changes: 8 additions & 1 deletion packages/catalog-realm/pr-card/pr-card.gts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,13 @@ class IsolatedTemplate extends Component<typeof PrCard> {
return buildCiGroups(this.ciItems);
}

get ciIsLoading() {
return (
this.checkRunEventData?.isLoading ||
this.checkSuiteEventData?.isLoading
) ?? false;
}

// ── Reviews ──
get latestReviewByReviewer() {
return buildLatestReviewByReviewer(this.prReviewEventData?.instances ?? []);
Expand Down Expand Up @@ -266,7 +273,7 @@ class IsolatedTemplate extends Component<typeof PrCard> {
{{! ── Body ── }}
<div class='pr-body'>
<section class='pr-status-columns'>
<CiSection @ciGroups={{this.ciGroups}} />
<CiSection @ciGroups={{this.ciGroups}} @isLoading={{this.ciIsLoading}} />
<hr class='status-divider' />
<ReviewSection
@reviewState={{this.latestReviewState}}
Expand Down
10 changes: 8 additions & 2 deletions packages/catalog-realm/pr-card/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,9 @@ export function buildCiItemFromEvent(event: any, type: CiEventType): CiItem {
const statusText =
conclusion != null
? `${formatCiValue(status)} - ${formatCiValue(conclusion)}`
: formatCiValue(status);
: state === 'in_progress'
? 'In Progress'
: formatCiValue(status);
Comment on lines +125 to +127
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change collapses all non-completed check statuses (e.g. GitHub's queued / requested) into the display label "In Progress" whenever state === 'in_progress' and conclusion is null. If you want to preserve the more specific status text, consider only using the "In Progress" fallback when status is missing, otherwise keep formatCiValue(status).

Suggested change
: state === 'in_progress'
? 'In Progress'
: formatCiValue(status);
: status != null
? formatCiValue(status)
: state === 'in_progress'
? 'In Progress'
: formatCiValue(status);

Copilot uses AI. Check for mistakes.

return {
name,
Expand Down Expand Up @@ -192,7 +194,7 @@ function latestEventsByCheckId(

/**
* Build CI items from check_run and check_suite event instances,
* deduped by name and sorted by most recent.
* deduped by name and sorted alphabetically for stable ordering.
*/
export function buildCiItems(
checkRunInstances: any[],
Expand All @@ -216,6 +218,7 @@ export function buildCiItems(
events.push({ event, type: 'check_run' });
}

// Sort by most recent first so deduplication keeps the latest event per name
events.sort(
(a, b) => eventLastModified(b.event) - eventLastModified(a.event),
);
Expand All @@ -231,6 +234,9 @@ export function buildCiItems(
items.push(buildCiItemFromEvent(event, type));
}

// Sort alphabetically by name for stable display order across refreshes
items.sort((a, b) => a.name.localeCompare(b.name));

return items;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ function resolveSubmissionWorkflowState(
ciAllPassed: boolean,
ciHasFailure: boolean,
ciInProgress: boolean,
ciIsLoading: boolean,
reviewState: string | null,
isMerged: boolean,
isClosed: boolean,
Expand Down Expand Up @@ -119,6 +120,9 @@ function resolveSubmissionWorkflowState(
if (hasPr && ciInProgress) {
inProgress = true;
statusDetail = 'Checks are running...';
} else if (hasPr && ciIsLoading && !ciAllPassed && !ciHasFailure) {
inProgress = true;
statusDetail = 'Loading check status...';
}
break;
case 'reviewer-approve':
Expand Down Expand Up @@ -416,6 +420,13 @@ export class SubmissionWorkflowCard extends CardDef {
return this.ciItems.some((i) => i.state === 'in_progress');
}

get ciIsLoading() {
return (
this.checkRunEventData?.isLoading ||
this.checkSuiteEventData?.isLoading
) ?? false;
}

// ── Review state ──
get latestReviewByReviewer() {
return buildLatestReviewByReviewer(
Expand All @@ -436,6 +447,7 @@ export class SubmissionWorkflowCard extends CardDef {
this.ciAllPassed,
this.ciHasFailure,
this.ciInProgress,
this.ciIsLoading,
this.reviewState,
this.isMerged,
this.isClosed,
Expand Down Expand Up @@ -491,7 +503,7 @@ export class SubmissionWorkflowCard extends CardDef {

{{! ── Step tracker ── }}
<div class='sw-steps'>
{{#each this.workflowState.steps as |step idx|}}
{{#each this.workflowState.steps key="key" as |step idx|}}
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idx from {{#each ... as |step idx|}} is a number, but this.lastStepIndex is defined as a string (String(this.workflowState.steps.length - 1)). Since eq uses strict equality, the "last step" branch will never match and the connector will still render for the last step. Consider making lastStepIndex a number (or casting idx to string) so the comparison works as intended.

Copilot uses AI. Check for mistakes.
<div class={{concat 'sw-step ' step.status}}>
<div class='sw-step-indicator'>
{{#if (eq step.status 'completed')}}
Expand Down Expand Up @@ -611,7 +623,7 @@ export class SubmissionWorkflowCard extends CardDef {
{{! Step summary }}
<div class='sw-sidebar-section'>
<div class='sw-sidebar-heading'>Steps</div>
{{#each this.workflowState.steps as |step|}}
{{#each this.workflowState.steps key="key" as |step|}}
<div class={{concat 'sw-sidebar-step ' step.status}}>
{{#if (eq step.status 'completed')}}
<span class='sw-sidebar-icon completed'>
Expand Down
Loading