Skip to content
Merged
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
45 changes: 31 additions & 14 deletions packages/catalog-realm/pr-card/components/isolated/ci-section.gts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import GlimmerComponent from '@glimmer/component';
import { cached } from '@glimmer/tracking';
import type { CiStatus, CiGroup } from '../../utils';

// ── Sub-components ──────────────────────────────────────────────────────
Expand Down Expand Up @@ -144,31 +145,41 @@ class CiStatusLabel extends GlimmerComponent<CiStatusLabelSignature> {
interface CiSectionSignature {
Args: {
ciGroups: CiGroup[];
isLoading?: boolean;
};
}

export class CiSection extends GlimmerComponent<CiSectionSignature> {
@cached get flatItems() {
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>
{{else}}
<div class='empty-state'>
<span class='empty-state-icon' aria-hidden='true'>
Expand Down Expand Up @@ -257,6 +268,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);

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