Skip to content

Commit 607ff12

Browse files
committed
linked to pr card testing
1 parent 5a77d43 commit 607ff12

8 files changed

Lines changed: 209 additions & 72 deletions

File tree

packages/catalog-realm/SubmissionCardPortal/b535d5fb-8eef-44a6-8114-4bce6929b95a.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@
1010
"attributes": {
1111
"title": "Submission Card Portal",
1212
"cardInfo": {
13-
"name": null,
13+
"name": "Submission Card Portal",
1414
"notes": null,
1515
"summary": null,
1616
"cardThumbnailURL": null
1717
},
18-
"description": null
18+
"description": ""
1919
},
2020
"relationships": {
2121
"cardInfo.theme": {

packages/catalog-realm/commands/create-submission.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import SaveCardCommand from '@cardstack/boxel-host/commands/save-card';
2828
import SerializeCardCommand from '@cardstack/boxel-host/commands/serialize-card';
2929

3030
import type { Listing } from '../catalog-app/listing/listing';
31+
import CreatePrCardCommand from './create-pr-card';
3132
import {
3233
FileContentField,
3334
SubmissionCard,
@@ -61,8 +62,10 @@ export default class CreateSubmissionCommand extends Command<
6162
protected async run(input: CreateSubmissionInput): Promise<CardDef> {
6263
let { listingId, realm, roomId } = input;
6364
let realmUrl = new RealmPaths(new URL(realm)).url;
65+
let submissionsRealmUrl = new URL('/submissions/', realmUrl).href;
6466
let getCardCommand = new GetCardCommand(this.commandContext);
6567
let saveCardCommand = new SaveCardCommand(this.commandContext);
68+
let createPrCardCommand = new CreatePrCardCommand(this.commandContext);
6669

6770
if (!listingId) {
6871
throw new Error('Missing listingId for CreateSubmission');
@@ -113,11 +116,17 @@ export default class CreateSubmissionCommand extends Command<
113116
throw new Error('Missing listing.name for CreateSubmission');
114117
}
115118
let branchName = toBranchName(roomId, listing.name);
119+
let prCard = await createPrCardCommand.execute({
120+
realm: submissionsRealmUrl,
121+
branchName,
122+
prTitle: listing.name,
123+
});
116124

117125
let submission = new SubmissionCard({
118126
listing,
119127
roomId,
120128
branchName,
129+
prCard,
121130
allFileContents: filesWithContent.map(
122131
(file) =>
123132
new FileContentField({

packages/catalog-realm/commands/process-github-event.gts

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,26 +32,11 @@ export default class ProcessGithubEventCommand extends Command<
3232
payload,
3333
});
3434

35-
// When a PR is opened, create the PR card first
36-
if (eventType === 'pull_request' && payload?.action === 'opened') {
37-
let pr = payload.pull_request;
38-
if (pr) {
39-
await new CreatePrCardCommand(this.commandContext).execute({
40-
realm,
41-
prNumber: pr.number,
42-
prUrl: pr.html_url,
43-
prTitle: pr.title,
44-
branchName: pr.head?.ref,
45-
submittedBy: pr.user?.login,
46-
});
47-
}
48-
}
49-
50-
await new SaveCardCommand(this.commandContext).execute({
35+
let savedCard = await new SaveCardCommand(this.commandContext).execute({
5136
card,
5237
realm,
5338
});
5439

55-
return card;
40+
return savedCard;
5641
}
5742
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import GlimmerComponent from '@glimmer/component';
2+
import TriangleAlertIcon from '@cardstack/boxel-icons/triangle-alert';
3+
import CircleCheckIcon from '@cardstack/boxel-icons/circle-check';
4+
5+
interface MergeableSectionSignature {
6+
Args: {
7+
isMergeable: boolean;
8+
isClosedOrMerged: boolean;
9+
blockReasons: string[];
10+
};
11+
}
12+
13+
export class MergeableSection extends GlimmerComponent<MergeableSectionSignature> {
14+
<template>
15+
{{#unless @isClosedOrMerged}}
16+
{{#if @isMergeable}}
17+
<div class='mergeable-banner mergeable-banner--ok'>
18+
<span class='mergeable-icon-wrap mergeable-icon-wrap--ok'>
19+
<CircleCheckIcon class='mergeable-icon' />
20+
</span>
21+
<div class='mergeable-content'>
22+
<span class='mergeable-title'>This branch has no conflicts</span>
23+
<span class='mergeable-subtitle'>Merging can be performed
24+
automatically</span>
25+
</div>
26+
</div>
27+
{{else}}
28+
<div class='mergeable-banner mergeable-banner--blocked'>
29+
<span class='mergeable-icon-wrap mergeable-icon-wrap--blocked'>
30+
<TriangleAlertIcon class='mergeable-icon' />
31+
</span>
32+
<div class='mergeable-content'>
33+
<span class='mergeable-title'>Merging is blocked</span>
34+
{{#each @blockReasons as |reason|}}
35+
<span class='mergeable-reason'>{{reason}}</span>
36+
{{/each}}
37+
</div>
38+
</div>
39+
{{/if}}
40+
{{/unless}}
41+
42+
<style scoped>
43+
.mergeable-banner {
44+
display: flex;
45+
align-items: flex-start;
46+
gap: var(--boxel-sp-sm);
47+
padding: var(--boxel-sp-sm) var(--boxel-sp-lg);
48+
border-top: 1px solid var(--border, var(--boxel-border-color));
49+
border-bottom: 1px solid var(--border, var(--boxel-border-color));
50+
}
51+
.mergeable-banner--blocked {
52+
background: color-mix(
53+
in srgb,
54+
var(--destructive, #d73a49) 5%,
55+
var(--card, #ffffff)
56+
);
57+
}
58+
.mergeable-banner--ok {
59+
background: color-mix(
60+
in srgb,
61+
var(--chart-1, #28a745) 5%,
62+
var(--card, #ffffff)
63+
);
64+
}
65+
.mergeable-icon-wrap {
66+
width: 32px;
67+
height: 32px;
68+
border-radius: 50%;
69+
display: flex;
70+
align-items: center;
71+
justify-content: center;
72+
flex-shrink: 0;
73+
}
74+
.mergeable-icon-wrap--blocked {
75+
background: var(--destructive, #d73a49);
76+
}
77+
.mergeable-icon-wrap--ok {
78+
background: var(--chart-1, #28a745);
79+
}
80+
.mergeable-icon {
81+
width: 16px;
82+
height: 16px;
83+
color: #ffffff;
84+
}
85+
.mergeable-content {
86+
display: flex;
87+
flex-direction: column;
88+
gap: 2px;
89+
min-width: 0;
90+
}
91+
.mergeable-title {
92+
font-size: var(--boxel-font-sm);
93+
font-weight: 700;
94+
color: var(--foreground, #1f2328);
95+
line-height: 1.4;
96+
}
97+
.mergeable-reason,
98+
.mergeable-subtitle {
99+
font-size: var(--boxel-font-sm);
100+
color: var(--muted-foreground, #656d76);
101+
line-height: 1.5;
102+
}
103+
</style>
104+
</template>
105+
}

packages/catalog-realm/pr-card/pr-card.gts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { HeaderSection } from './components/isolated/header-section';
1919
import { CiSection } from './components/isolated/ci-section';
2020
import { ReviewSection } from './components/isolated/review-section';
2121
import { SummarySection } from './components/isolated/summary-section';
22+
import { MergeableSection } from './components/isolated/mergeable-section';
2223

2324
import {
2425
renderPrActionLabel,
@@ -196,6 +197,44 @@ class IsolatedTemplate extends Component<typeof PrCard> {
196197
return !!this.latestPrReviewCommentEventInstance;
197198
}
198199

200+
// ── Mergeability ──
201+
get isClosed() {
202+
let label = this.latestPrActionLabel;
203+
return label === 'Closed' || label === 'Merged';
204+
}
205+
206+
get isDraft() {
207+
return this.latestPrActionLabel === 'Draft';
208+
}
209+
210+
get mergeBlockReasons(): string[] {
211+
if (this.isClosed) return [];
212+
let reasons: string[] = [];
213+
if (this.isDraft) {
214+
reasons.push('This pull request is still a work in progress');
215+
}
216+
let { ciItems } = this;
217+
if (ciItems.some((i) => i.state === 'failure')) {
218+
reasons.push('Some checks were not successful');
219+
} else if (ciItems.some((i) => i.state === 'in_progress')) {
220+
reasons.push('Some checks are still in progress');
221+
}
222+
let reviewState = this.latestReviewState;
223+
if (reviewState === 'changes_requested') {
224+
reasons.push('Changes were requested by a reviewer');
225+
} else if (reviewState !== 'approved') {
226+
reasons.push(
227+
'At least 1 approving review is required by reviewers with write access',
228+
);
229+
}
230+
return reasons;
231+
}
232+
233+
get isMergeable() {
234+
if (this.isClosed) return false;
235+
return this.mergeBlockReasons.length === 0;
236+
}
237+
199238
<template>
200239
<article class='pr-card'>
201240
<HeaderSection
@@ -229,6 +268,12 @@ class IsolatedTemplate extends Component<typeof PrCard> {
229268
/>
230269
</section>
231270

271+
<MergeableSection
272+
@isMergeable={{this.isMergeable}}
273+
@isClosedOrMerged={{this.isClosed}}
274+
@blockReasons={{this.mergeBlockReasons}}
275+
/>
276+
232277
<SummarySection @summary={{this.prBodySummary}} />
233278
</div>
234279
</article>
@@ -446,6 +491,21 @@ class FittedTemplate extends Component<typeof PrCard> {
446491
return computeLatestReviewState(this.latestReviewByReviewer);
447492
}
448493

494+
// ── Mergeability ──
495+
get isClosed() {
496+
let label = this.latestPrActionLabel;
497+
return label === 'Closed' || label === 'Merged';
498+
}
499+
500+
get isMergeBlocked() {
501+
if (this.isClosed) return false;
502+
if (this.latestPrActionLabel === 'Draft') return true;
503+
if (this.ciItems.some((i) => i.state === 'failure')) return true;
504+
if (this.ciItems.some((i) => i.state === 'in_progress')) return true;
505+
if (this.latestReviewState !== 'approved') return true;
506+
return false;
507+
}
508+
449509
copyBranchName = async () => {
450510
let branchName = this.prBranchName?.trim();
451511
if (!branchName) {
@@ -527,14 +587,29 @@ class FittedTemplate extends Component<typeof PrCard> {
527587
{{#if (eq this.latestReviewState 'changes_requested')}}
528588
<div class='review-status-row review-status-row--changes'>
529589
<span class='review-status-label'>Changes Requested</span>
590+
{{#if this.isMergeBlocked}}
591+
<Pill class='merge-blocked-pill' @pillBackgroundColor='#d73a49'>
592+
<:default><span class='merge-blocked-label'>Merge blocked</span></:default>
593+
</Pill>
594+
{{/if}}
530595
</div>
531596
{{else if (eq this.latestReviewState 'approved')}}
532597
<div class='review-status-row review-status-row--approved'>
533598
<span class='review-status-label'>Approved</span>
599+
{{#if this.isMergeBlocked}}
600+
<Pill class='merge-blocked-pill' @pillBackgroundColor='#d73a49'>
601+
<:default><span class='merge-blocked-label'>Merge blocked</span></:default>
602+
</Pill>
603+
{{/if}}
534604
</div>
535605
{{else}}
536606
<div class='review-status-row review-status-row--pending'>
537607
<span class='review-status-label'>Pending Review</span>
608+
{{#if this.isMergeBlocked}}
609+
<Pill class='merge-blocked-pill' @pillBackgroundColor='#d73a49'>
610+
<:default><span class='merge-blocked-label'>Merge blocked</span></:default>
611+
</Pill>
612+
{{/if}}
538613
</div>
539614
{{/if}}
540615

@@ -729,6 +804,7 @@ class FittedTemplate extends Component<typeof PrCard> {
729804
.review-status-row {
730805
display: flex;
731806
align-items: center;
807+
justify-content: space-between;
732808
padding: var(--boxel-sp-xs) var(--boxel-sp-sm);
733809
border-bottom: 1px solid var(--border, var(--boxel-border-color));
734810
}
@@ -762,6 +838,15 @@ class FittedTemplate extends Component<typeof PrCard> {
762838
.review-status-row--approved .review-status-label {
763839
color: var(--chart-1, #28a745);
764840
}
841+
/* ── Merge blocked pill ── */
842+
.merge-blocked-pill {
843+
--boxel-pill-border-radius: 2em;
844+
}
845+
.merge-blocked-label {
846+
font-size: 10px;
847+
font-weight: 600;
848+
color: #fff;
849+
}
765850
/* ── Summary ── */
766851
.summary-section {
767852
display: flex;

packages/catalog-realm/submission-card/components/card/fitted-template.gts

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
import { on } from '@ember/modifier';
22

3-
import { Component, realmURL } from 'https://cardstack.com/base/card-api';
4-
import type { Query } from '@cardstack/runtime-common';
3+
import { Component } from 'https://cardstack.com/base/card-api';
54

65
import { BoxelButton } from '@cardstack/boxel-ui/components';
76

87
import GitBranchIcon from '@cardstack/boxel-icons/git-branch';
98
import GitPullRequestIcon from '@cardstack/boxel-icons/git-pull-request';
109
import MessageIcon from '@cardstack/boxel-icons/message';
1110

12-
import { buildRealmHrefs } from '../../../pr-card/utils';
13-
import type { PrCard } from '../../../pr-card/pr-card';
1411
import type { SubmissionCard } from '../../submission-card';
1512

1613
export class FittedTemplate extends Component<typeof SubmissionCard> {
@@ -30,33 +27,8 @@ export class FittedTemplate extends Component<typeof SubmissionCard> {
3027
}
3128
};
3229

33-
get realmHrefs() {
34-
return buildRealmHrefs(this.args.model[realmURL]?.href);
35-
}
36-
37-
get prCardQuery(): Query | undefined {
38-
if (!this.args.model.branchName) return undefined;
39-
return {
40-
filter: {
41-
on: {
42-
module: new URL('../../../pr-card/pr-card', import.meta.url).href,
43-
name: 'PrCard',
44-
},
45-
eq: { branchName: this.args.model.branchName },
46-
},
47-
sort: [{ by: 'lastModified', direction: 'desc' }],
48-
};
49-
}
50-
51-
prCardData = this.args.context?.getCards(
52-
this,
53-
() => this.prCardQuery,
54-
() => this.realmHrefs,
55-
{ isLive: true },
56-
);
57-
58-
get prCardInstance(): PrCard | null {
59-
return (this.prCardData?.instances?.[0] as PrCard) ?? null;
30+
get prCardInstance() {
31+
return this.args.model.prCard ?? null;
6032
}
6133

6234
openPrCard = (e: Event) => {

0 commit comments

Comments
 (0)