Skip to content

Commit 109dc7a

Browse files
lucaslylclaude
andauthored
Add listing field with CardPill and change action to create PR modal (#4287)
* feat: add listing field with CardPill and change action to create PR modal Replace static listing name and realm fields with a CardPill component that links to the listing, and add a "Change" button to select a different listing via the catalog chooser. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * update guard --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 417c292 commit 109dc7a

2 files changed

Lines changed: 147 additions & 38 deletions

File tree

packages/host/app/components/operator-mode/create-pr-modal.gts

Lines changed: 71 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@ import { service } from '@ember/service';
44
import Component from '@glimmer/component';
55
import { tracked } from '@glimmer/tracking';
66

7-
import { task } from 'ember-concurrency';
7+
import { restartableTask, task } from 'ember-concurrency';
88
import perform from 'ember-concurrency/helpers/perform';
99
import onKeyMod from 'ember-keyboard/modifiers/on-key';
1010

11-
import {
12-
Button,
13-
FieldContainer,
14-
RealmIcon,
15-
} from '@cardstack/boxel-ui/components';
11+
import { Button, FieldContainer } from '@cardstack/boxel-ui/components';
12+
13+
import { chooseCard } from '@cardstack/runtime-common';
1614

1715
import CreateListingPRRequestCommand from '@cardstack/host/commands/bot-requests/create-listing-pr-request';
16+
import CardPill from '@cardstack/host/components/card-pill';
1817
import ModalContainer from '@cardstack/host/components/modal-container';
18+
import { catalogRealm } from '@cardstack/host/lib/utils';
1919

2020
import type CommandService from '@cardstack/host/services/command-service';
2121
import type OperatorModeStateService from '@cardstack/host/services/operator-mode-state-service';
@@ -31,6 +31,7 @@ export default class CreatePRModal extends Component<Signature> {
3131
@service declare private realm: RealmService;
3232

3333
@tracked private isSubmitted = false;
34+
@tracked private selectedListingId?: string;
3435

3536
private get payload() {
3637
return this.operatorModeStateService.createPRModalPayload;
@@ -40,16 +41,19 @@ export default class CreatePRModal extends Component<Signature> {
4041
return Boolean(this.payload);
4142
}
4243

43-
private get realmInfo() {
44-
let payload = this.payload;
45-
if (!payload) {
44+
private get listingId() {
45+
return this.selectedListingId ?? this.payload?.listingId;
46+
}
47+
48+
private get listingTitle(): string | undefined {
49+
if (this.selectedListingId) {
4650
return undefined;
4751
}
48-
return this.realm.info(payload.realm);
52+
return this.payload?.listingName;
4953
}
5054

51-
private get listingName(): string {
52-
return this.payload?.listingName ?? 'Listing';
55+
private get canChangeListing() {
56+
return Boolean(catalogRealm);
5357
}
5458

5559
private createPR = task(async () => {
@@ -58,18 +62,44 @@ export default class CreatePRModal extends Component<Signature> {
5862
throw new Error('Cannot create PR without a modal payload');
5963
}
6064

65+
let currentListingId = this.listingId;
66+
if (!currentListingId) {
67+
throw new Error('Cannot create PR without a listing');
68+
}
69+
70+
let realm =
71+
this.realm.realmOfURL(new URL(currentListingId))?.href ?? payload.realm;
72+
6173
await new CreateListingPRRequestCommand(
6274
this.commandService.commandContext,
6375
).execute({
64-
listingId: payload.listingId,
65-
realm: payload.realm,
76+
listingId: currentListingId,
77+
realm,
6678
});
6779

6880
this.isSubmitted = true;
6981
});
7082

83+
private changeListing = restartableTask(async () => {
84+
if (!catalogRealm) {
85+
throw new Error('Cannot find catalog realm');
86+
}
87+
let listingId = await chooseCard({
88+
filter: {
89+
type: {
90+
module: `${catalogRealm.url}catalog-app/listing/listing`,
91+
name: 'Listing',
92+
},
93+
},
94+
});
95+
if (listingId) {
96+
this.selectedListingId = listingId;
97+
}
98+
});
99+
71100
@action private onClose() {
72101
this.isSubmitted = false;
102+
this.selectedListingId = undefined;
73103
this.operatorModeStateService.dismissCreatePRModal();
74104
}
75105

@@ -78,7 +108,7 @@ export default class CreatePRModal extends Component<Signature> {
78108
<ModalContainer
79109
class='create-pr-modal'
80110
@cardContainerClass='create-pr'
81-
@title={{if this.isSubmitted 'Listing Submitted 🎉 ! ' 'Make a PR'}}
111+
@title={{if this.isSubmitted 'Listing Submitted 🎉!' 'Make a PR'}}
82112
@size='small'
83113
@isOpen={{this.isModalOpen}}
84114
@onClose={{this.onClose}}
@@ -87,12 +117,10 @@ export default class CreatePRModal extends Component<Signature> {
87117
<:content>
88118
{{#if this.isSubmitted}}
89119
<div class='submitted-container' data-test-create-pr-success>
90-
<p class='submitted-message'>
91-
Your listing
92-
<strong>{{this.listingName}}</strong>
93-
has been submitted for review. A PR will be created on GitHub
94-
and you will be notified once it is approved.
95-
</p>
120+
<div class='submitted-message'>
121+
Your listing has been submitted for review. A PR will be created
122+
on GitHub and you will be notified once it is approved.
123+
</div>
96124
<Button
97125
@as='anchor'
98126
@kind='secondary'
@@ -111,15 +139,24 @@ export default class CreatePRModal extends Component<Signature> {
111139
</p>
112140
<FieldContainer @label='Listing' class='field'>
113141
<div class='field-contents' data-test-create-pr-listing-name>
114-
<span>{{this.listingName}}</span>
115-
</div>
116-
</FieldContainer>
117-
118-
<FieldContainer @label='Realm' @tag='label' class='field'>
119-
<div class='field-contents' data-test-create-pr-realm>
120-
{{#if this.realmInfo}}
121-
<RealmIcon class='realm-icon' @realmInfo={{this.realmInfo}} />
122-
<span>{{this.realmInfo.name}}</span>
142+
{{#if this.listingId}}
143+
<CardPill
144+
@cardId={{this.listingId}}
145+
@urlForRealmLookup={{this.listingId}}
146+
@displayTitle={{this.listingTitle}}
147+
class='listing-pill'
148+
/>
149+
{{/if}}
150+
{{#if this.canChangeListing}}
151+
<Button
152+
@kind='text-only'
153+
@size='small'
154+
@disabled={{this.createPR.isRunning}}
155+
{{on 'click' (perform this.changeListing)}}
156+
data-test-create-pr-change-listing-button
157+
>
158+
Change
159+
</Button>
123160
{{/if}}
124161
</div>
125162
</FieldContainer>
@@ -139,8 +176,7 @@ export default class CreatePRModal extends Component<Signature> {
139176
</Button>
140177
{{else if this.createPR.isRunning}}
141178
<p class='footer-loading-message' data-test-create-pr-loading>
142-
Submitting
143-
<strong>{{this.listingName}}</strong>. This may take a moment...
179+
Submitting your listing. This may take a moment...
144180
</p>
145181
<Button
146182
@kind='primary'
@@ -243,10 +279,11 @@ export default class CreatePRModal extends Component<Signature> {
243279
.field-contents {
244280
display: flex;
245281
align-items: center;
282+
justify-content: space-between;
246283
gap: var(--horizontal-gap);
247284
}
248-
.realm-icon {
249-
--boxel-realm-icon-size: 1rem;
285+
.listing-pill :deep(figure.icon:last-child) {
286+
display: none;
250287
}
251288
.footer-buttons {
252289
display: flex;

packages/host/tests/integration/components/create-pr-modal-test.gts

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { waitFor } from '@ember/test-helpers';
1+
import { click, waitFor, waitUntil } from '@ember/test-helpers';
22
import GlimmerComponent from '@glimmer/component';
33

44
import { module, test } from 'qunit';
@@ -34,7 +34,7 @@ module('Integration | components | create-pr-modal', function (hooks) {
3434
assert.dom('[data-test-create-pr-modal]').includesText('Make a PR');
3535
});
3636

37-
test('shows listing name in modal', async function (assert) {
37+
test('shows the listing pill in modal', async function (assert) {
3838
await renderComponent(
3939
class TestDriver extends GlimmerComponent {
4040
<template><OperatorMode @onClose={{noop}} /></template>
@@ -52,7 +52,7 @@ module('Integration | components | create-pr-modal', function (hooks) {
5252
assert.dom('[data-test-create-pr-listing-name]').includesText('My Listing');
5353
});
5454

55-
test('shows realm info in modal', async function (assert) {
55+
test('does not show change action when catalog chooser is unavailable', async function (assert) {
5656
await renderComponent(
5757
class TestDriver extends GlimmerComponent {
5858
<template><OperatorMode @onClose={{noop}} /></template>
@@ -67,6 +67,78 @@ module('Integration | components | create-pr-modal', function (hooks) {
6767

6868
await waitFor('[data-test-create-pr-modal]');
6969

70-
assert.dom('[data-test-create-pr-realm]').exists();
70+
assert.dom('[data-test-create-pr-change-listing-button]').doesNotExist();
71+
});
72+
73+
test('does not show a separate realm field in modal', async function (assert) {
74+
await renderComponent(
75+
class TestDriver extends GlimmerComponent {
76+
<template><OperatorMode @onClose={{noop}} /></template>
77+
},
78+
);
79+
80+
ctx.operatorModeStateService.showCreatePRModal({
81+
realm: testRealmURL,
82+
listingId: `${testRealmURL}Listing/1`,
83+
listingName: 'My Listing',
84+
});
85+
86+
await waitFor('[data-test-create-pr-modal]');
87+
88+
assert.dom('[data-test-create-pr-realm]').doesNotExist();
89+
});
90+
91+
test('cancel button dismisses the modal', async function (assert) {
92+
await renderComponent(
93+
class TestDriver extends GlimmerComponent {
94+
<template><OperatorMode @onClose={{noop}} /></template>
95+
},
96+
);
97+
98+
ctx.operatorModeStateService.showCreatePRModal({
99+
realm: testRealmURL,
100+
listingId: `${testRealmURL}Listing/1`,
101+
listingName: 'My Listing',
102+
});
103+
104+
await waitFor('[data-test-create-pr-modal]');
105+
assert.dom('[data-test-create-pr-modal]').exists();
106+
107+
await click('[data-test-create-pr-cancel-button]');
108+
109+
await waitUntil(
110+
() => !document.querySelector('[data-test-create-pr-modal]'),
111+
);
112+
assert.dom('[data-test-create-pr-modal]').doesNotExist();
113+
assert.strictEqual(
114+
ctx.operatorModeStateService.createPRModalPayload,
115+
undefined,
116+
'modal payload is cleared after cancel',
117+
);
118+
});
119+
120+
test('submit shows success state', async function (assert) {
121+
await renderComponent(
122+
class TestDriver extends GlimmerComponent {
123+
<template><OperatorMode @onClose={{noop}} /></template>
124+
},
125+
);
126+
127+
ctx.operatorModeStateService.showCreatePRModal({
128+
realm: testRealmURL,
129+
listingId: `${testRealmURL}Listing/1`,
130+
listingName: 'My Listing',
131+
});
132+
133+
await waitFor('[data-test-create-pr-modal]');
134+
135+
await click('[data-test-create-pr-confirm-button]');
136+
137+
await waitFor('[data-test-create-pr-success]');
138+
139+
assert
140+
.dom('[data-test-create-pr-success]')
141+
.includesText('has been submitted for review.');
142+
assert.dom('[data-test-create-pr-done-button]').exists();
71143
});
72144
});

0 commit comments

Comments
 (0)