github-issue-tower-defence-management 1.112.5 → 1.113.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/bin/adapter/entry-points/cli/index.js +1 -0
  3. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  4. package/bin/adapter/entry-points/console/consoleReadApi.js +47 -1
  5. package/bin/adapter/entry-points/console/consoleReadApi.js.map +1 -1
  6. package/bin/adapter/entry-points/console/ui-dist/assets/{index-BvWrFBsC.css → index-CEJmPNRK.css} +1 -1
  7. package/bin/adapter/entry-points/console/ui-dist/assets/index-DfNrA5uV.js +101 -0
  8. package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
  9. package/bin/adapter/entry-points/console/webServer.js +6 -0
  10. package/bin/adapter/entry-points/console/webServer.js.map +1 -1
  11. package/package.json +1 -1
  12. package/src/adapter/entry-points/cli/index.ts +5 -1
  13. package/src/adapter/entry-points/console/consoleReadApi.test.ts +89 -0
  14. package/src/adapter/entry-points/console/consoleReadApi.ts +71 -0
  15. package/src/adapter/entry-points/console/ui/e2e/consoleScenario.spec.ts +98 -0
  16. package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +135 -10
  17. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.stories.tsx +44 -0
  18. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.test.tsx +56 -0
  19. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.tsx +22 -0
  20. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.stories.tsx +17 -0
  21. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.test.tsx +46 -0
  22. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.tsx +12 -0
  23. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestStatusBadges.stories.tsx +41 -0
  24. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestStatusBadges.test.tsx +72 -0
  25. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestStatusBadges.tsx +71 -0
  26. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleCaches.ts +5 -0
  27. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleItemDetailData.test.ts +20 -0
  28. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleItemDetailData.ts +17 -0
  29. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +41 -0
  30. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +36 -5
  31. package/src/adapter/entry-points/console/ui/src/features/console/logic/types.ts +9 -0
  32. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.test.tsx +77 -3
  33. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +17 -9
  34. package/src/adapter/entry-points/console/ui/src/index.css +15 -1
  35. package/src/adapter/entry-points/console/ui-dist/assets/{index-BvWrFBsC.css → index-CEJmPNRK.css} +1 -1
  36. package/src/adapter/entry-points/console/ui-dist/assets/index-DfNrA5uV.js +101 -0
  37. package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
  38. package/src/adapter/entry-points/console/webServer.test.ts +52 -1
  39. package/src/adapter/entry-points/console/webServer.ts +13 -0
  40. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  41. package/types/adapter/entry-points/console/consoleReadApi.d.ts +20 -0
  42. package/types/adapter/entry-points/console/consoleReadApi.d.ts.map +1 -1
  43. package/types/adapter/entry-points/console/webServer.d.ts +2 -1
  44. package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
  45. package/bin/adapter/entry-points/console/ui-dist/assets/index-DNBjBIvK.js +0 -101
  46. package/src/adapter/entry-points/console/ui-dist/assets/index-DNBjBIvK.js +0 -101
@@ -9,15 +9,27 @@ import type {
9
9
  IssueRepository,
10
10
  PullRequestCommit,
11
11
  PullRequestDetail,
12
+ PullRequestFile,
12
13
  RelatedPullRequest,
13
14
  } from '../../../../../domain/usecases/adapter-interfaces/IssueRepository';
14
15
  import type { ConsoleProjectBinding } from '../../consoleOperationApi';
15
- import { IssueTitleStateCache } from '../../consoleReadApi';
16
+ import {
17
+ IssueTitleStateCache,
18
+ PullRequestStatusCache,
19
+ } from '../../consoleReadApi';
16
20
  import { startWebServer } from '../../webServer';
17
21
 
18
22
  export const CONSOLE_E2E_PJCODE = 'umino';
19
23
  export const CONSOLE_E2E_TOKEN = 'console-e2e-fixture-token-3f9c1a';
20
24
 
25
+ export type ConsoleE2eReviewCommentCall = {
26
+ url: string;
27
+ path: string;
28
+ line: number;
29
+ side: string;
30
+ body: string;
31
+ };
32
+
21
33
  type ConsoleFixtureListItem = {
22
34
  number: number;
23
35
  title: string;
@@ -54,6 +66,26 @@ type ConsoleFixtureSnapshot = {
54
66
  const REPO_NAME_WITH_OWNER =
55
67
  'HiromiShikata/npm-cli-github-issue-tower-defence-management';
56
68
 
69
+ export const CONSOLE_E2E_AWAITING_QUALITY_CHECK_PR_URL = `https://github.com/${REPO_NAME_WITH_OWNER}/pull/867`;
70
+ export const CONSOLE_E2E_INLINE_COMMENT_ISSUE_URL = `https://github.com/${REPO_NAME_WITH_OWNER}/issues/911`;
71
+ export const CONSOLE_E2E_INLINE_COMMENT_PR_URL = `https://github.com/${REPO_NAME_WITH_OWNER}/pull/912`;
72
+
73
+ const INLINE_COMMENT_PR_FILE: PullRequestFile = {
74
+ filename: 'src/adapter/entry-points/console/ui/src/index.css',
75
+ status: 'modified',
76
+ additions: 3,
77
+ deletions: 1,
78
+ patch: `@@ -959,7 +959,9 @@
79
+ background: transparent;
80
+ color: #6e7681;
81
+ font-size: 0.875rem;
82
+ cursor: pointer;
83
+ - opacity: 0;
84
+ + opacity: 0.6;
85
+ + border-color: #30363d;
86
+ }`,
87
+ };
88
+
57
89
  const AWAITING_WORKSPACE_OPTION: ConsoleFixtureFieldOption = {
58
90
  id: 'd1c19cce',
59
91
  name: 'Awaiting Workspace',
@@ -198,7 +230,15 @@ export const CONSOLE_E2E_TAB_ITEMS: Record<string, ConsoleFixtureListItem[]> = {
198
230
  '2026-06-17T05:48:09.000Z',
199
231
  ),
200
232
  ],
201
- 'failed-preparation': [],
233
+ 'failed-preparation': [
234
+ issueItem(
235
+ 911,
236
+ 'Add inline review comments on the related pull request diff',
237
+ 'FPR00911',
238
+ 'TDPM Console port',
239
+ '2026-06-18T03:12:00.000Z',
240
+ ),
241
+ ],
202
242
  'todo-by-human': [
203
243
  issueItem(
204
244
  869,
@@ -286,7 +326,49 @@ const notImplemented = (method: string): never => {
286
326
  throw new Error(`console E2E stub does not implement ${method}`);
287
327
  };
288
328
 
289
- const createStubIssueRepository = (): IssueRepository => ({
329
+ const inlineCommentRelatedPullRequest: RelatedPullRequest = {
330
+ url: CONSOLE_E2E_INLINE_COMMENT_PR_URL,
331
+ branchName: 'feature/911-related-pr-inline-comments',
332
+ createdAt: new Date('2026-06-18T03:30:00.000Z'),
333
+ isDraft: false,
334
+ isConflicted: true,
335
+ isPassedAllCiJob: false,
336
+ isCiStateSuccess: false,
337
+ isResolvedAllReviewComments: false,
338
+ isBranchOutOfDate: true,
339
+ missingRequiredCheckNames: ['build', 'test'],
340
+ };
341
+
342
+ const awaitingQualityCheckPullRequest: RelatedPullRequest = {
343
+ url: CONSOLE_E2E_AWAITING_QUALITY_CHECK_PR_URL,
344
+ branchName: 'i867-serve-committed-console-ui-bundle',
345
+ createdAt: new Date('2026-06-17T23:41:08.000Z'),
346
+ isDraft: false,
347
+ isConflicted: true,
348
+ isPassedAllCiJob: false,
349
+ isCiStateSuccess: false,
350
+ isResolvedAllReviewComments: false,
351
+ isBranchOutOfDate: true,
352
+ missingRequiredCheckNames: ['build', 'test'],
353
+ };
354
+
355
+ const inlineCommentPullRequestDetail: PullRequestDetail = {
356
+ title: 'Add inline review comments on the related pull request diff',
357
+ state: 'open',
358
+ merged: false,
359
+ isDraft: false,
360
+ additions: 3,
361
+ deletions: 1,
362
+ changedFiles: 1,
363
+ headRefName: 'feature/911-related-pr-inline-comments',
364
+ baseRefName: 'main',
365
+ author: 'HiromiShikata',
366
+ files: [INLINE_COMMENT_PR_FILE],
367
+ };
368
+
369
+ const createStubIssueRepository = (
370
+ reviewCommentCalls: ConsoleE2eReviewCommentCall[],
371
+ ): IssueRepository => ({
290
372
  getAllIssues: () => notImplemented('getAllIssues'),
291
373
  getIssueByUrl: async (url: string): Promise<Issue | null> =>
292
374
  buildIssueForUrl(url),
@@ -306,12 +388,34 @@ const createStubIssueRepository = (): IssueRepository => ({
306
388
  get: async (issueUrl: string): Promise<Issue | null> =>
307
389
  buildIssueForUrl(issueUrl),
308
390
  update: () => notImplemented('update'),
309
- findRelatedOpenPRs: async (): Promise<RelatedPullRequest[]> => [],
310
- getOpenPullRequest: async (): Promise<RelatedPullRequest | null> => null,
391
+ findRelatedOpenPRs: async (url: string): Promise<RelatedPullRequest[]> =>
392
+ url === CONSOLE_E2E_INLINE_COMMENT_ISSUE_URL
393
+ ? [inlineCommentRelatedPullRequest]
394
+ : [],
395
+ getOpenPullRequest: async (
396
+ url: string,
397
+ ): Promise<RelatedPullRequest | null> =>
398
+ url === CONSOLE_E2E_AWAITING_QUALITY_CHECK_PR_URL
399
+ ? awaitingQualityCheckPullRequest
400
+ : null,
311
401
  getPullRequestChangedFilePaths: async (): Promise<string[]> => [],
312
402
  approvePullRequest: async (): Promise<void> => undefined,
313
403
  requestChangesWithInlineComment: async (): Promise<void> => undefined,
314
- createPullRequestReviewComment: async (): Promise<void> => undefined,
404
+ createPullRequestReviewComment: async (
405
+ prUrl: string,
406
+ filePath: string,
407
+ line: number,
408
+ side: string,
409
+ commentBody: string,
410
+ ): Promise<void> => {
411
+ reviewCommentCalls.push({
412
+ url: prUrl,
413
+ path: filePath,
414
+ line,
415
+ side,
416
+ body: commentBody,
417
+ });
418
+ },
315
419
  closePullRequest: async (): Promise<void> => undefined,
316
420
  closeIssueByUrl: async (): Promise<void> => undefined,
317
421
  deletePullRequestBranch: () => notImplemented('deletePullRequestBranch'),
@@ -323,7 +427,12 @@ const createStubIssueRepository = (): IssueRepository => ({
323
427
  getIssueOrPullRequestBody: async (): Promise<string> =>
324
428
  '## Console E2E fixture\n\nThis body is served by the isolated E2E stub.',
325
429
  getIssueOrPullRequestComments: async (): Promise<IssueComment[]> => [],
326
- getPullRequestDetail: async (): Promise<PullRequestDetail | null> => null,
430
+ getPullRequestDetail: async (
431
+ url: string,
432
+ ): Promise<PullRequestDetail | null> =>
433
+ url === CONSOLE_E2E_INLINE_COMMENT_PR_URL
434
+ ? inlineCommentPullRequestDetail
435
+ : null,
327
436
  getPullRequestCommits: async (): Promise<PullRequestCommit[]> => [],
328
437
  getIssueOrPullRequestState: async (
329
438
  url: string,
@@ -332,13 +441,24 @@ const createStubIssueRepository = (): IssueRepository => ({
332
441
  merged: false,
333
442
  isPullRequest: url.includes('/pull/'),
334
443
  }),
335
- getPullRequestSummary: async (): Promise<{
444
+ getPullRequestSummary: async (
445
+ url: string,
446
+ ): Promise<{
336
447
  title: string;
337
448
  body: string;
338
449
  additions: number;
339
450
  deletions: number;
340
451
  changedFiles: number;
341
- } | null> => null,
452
+ } | null> =>
453
+ url === CONSOLE_E2E_INLINE_COMMENT_PR_URL
454
+ ? {
455
+ title: 'Add inline review comments on the related pull request diff',
456
+ body: 'Wires the add-comment handler on the related pull request diff path.',
457
+ additions: 3,
458
+ deletions: 1,
459
+ changedFiles: 1,
460
+ }
461
+ : null,
342
462
  });
343
463
 
344
464
  export type ConsoleE2eHarness = {
@@ -346,6 +466,7 @@ export type ConsoleE2eHarness = {
346
466
  appUrl: string;
347
467
  appRootUrl: string;
348
468
  consoleDataOutputDir: string;
469
+ reviewCommentCalls: ConsoleE2eReviewCommentCall[];
349
470
  stop: () => Promise<void>;
350
471
  };
351
472
 
@@ -362,13 +483,16 @@ export const startConsoleE2eHarness = async (): Promise<ConsoleE2eHarness> => {
362
483
  ): Promise<ConsoleProjectBinding | null> =>
363
484
  pjcode === CONSOLE_E2E_PJCODE ? { pjcode, project } : null;
364
485
 
486
+ const reviewCommentCalls: ConsoleE2eReviewCommentCall[] = [];
487
+
365
488
  const server = await startWebServer({
366
489
  accessToken: CONSOLE_E2E_TOKEN,
367
490
  uiDistDir,
368
491
  consoleDataOutputDir,
369
- issueRepository: createStubIssueRepository(),
492
+ issueRepository: createStubIssueRepository(reviewCommentCalls),
370
493
  resolveProject,
371
494
  issueTitleStateCache: new IssueTitleStateCache(),
495
+ pullRequestStatusCache: new PullRequestStatusCache(),
372
496
  inTmuxDataDir: null,
373
497
  dashboardDir: null,
374
498
  dashboardDataDir: null,
@@ -391,6 +515,7 @@ export const startConsoleE2eHarness = async (): Promise<ConsoleE2eHarness> => {
391
515
  appUrl,
392
516
  appRootUrl,
393
517
  consoleDataOutputDir,
518
+ reviewCommentCalls,
394
519
  stop: async (): Promise<void> => {
395
520
  await closeServer(server);
396
521
  fs.rmSync(tmpRoot, { recursive: true, force: true });
@@ -81,6 +81,45 @@ export const PullRequestItem: Story = {
81
81
  commits: consoleCommitsFixture,
82
82
  commitsAreLoading: false,
83
83
  commitsError: null,
84
+ pullRequestStatus: {
85
+ found: true,
86
+ isConflicted: false,
87
+ isPassedAllCiJob: true,
88
+ isCiStateSuccess: true,
89
+ isBranchOutOfDate: false,
90
+ missingRequiredCheckNames: [],
91
+ },
92
+ relatedPullRequests: [],
93
+ },
94
+ };
95
+
96
+ export const PullRequestItemFailingCiWithConflict: Story = {
97
+ args: {
98
+ item: consoleListItemsFixture[0],
99
+ storyName: 'TDPM Console port',
100
+ storyColorEnum: 'BLUE',
101
+ overlayStatus: { name: 'Awaiting Quality Check', color: 'YELLOW' },
102
+ state: { state: 'open', merged: false, isPullRequest: true },
103
+ body: consoleMermaidBodyFixture,
104
+ bodyIsLoading: false,
105
+ bodyError: null,
106
+ comments: consoleCommentsFixture,
107
+ commentsAreLoading: false,
108
+ commentsError: null,
109
+ files: consoleChangedFilesFixture,
110
+ filesAreLoading: false,
111
+ filesError: null,
112
+ commits: consoleCommitsFixture,
113
+ commitsAreLoading: false,
114
+ commitsError: null,
115
+ pullRequestStatus: {
116
+ found: true,
117
+ isConflicted: true,
118
+ isPassedAllCiJob: false,
119
+ isCiStateSuccess: false,
120
+ isBranchOutOfDate: true,
121
+ missingRequiredCheckNames: ['build', 'test'],
122
+ },
84
123
  relatedPullRequests: [],
85
124
  },
86
125
  };
@@ -104,6 +143,7 @@ export const IssueWithRichMarkdownBody: Story = {
104
143
  commits: [],
105
144
  commitsAreLoading: false,
106
145
  commitsError: null,
146
+ pullRequestStatus: null,
107
147
  relatedPullRequests: [],
108
148
  operationBar: (
109
149
  <ConsoleOperationMenu
@@ -137,6 +177,7 @@ export const IssueWithLinkedPullRequest: Story = {
137
177
  commits: [],
138
178
  commitsAreLoading: false,
139
179
  commitsError: null,
180
+ pullRequestStatus: null,
140
181
  relatedPullRequests: [
141
182
  {
142
183
  pullRequest: consoleRelatedPullRequestsFixture[0],
@@ -148,5 +189,8 @@ export const IssueWithLinkedPullRequest: Story = {
148
189
  commitsError: null,
149
190
  },
150
191
  ],
192
+ buildAddInlineComment: (prUrl) => async (path, line, side, body) => {
193
+ window.alert(`comment on ${prUrl} ${path}:${line} (${side})\n${body}`);
194
+ },
151
195
  },
152
196
  };
@@ -32,6 +32,7 @@ const baseProps = {
32
32
  commits: consoleCommitsFixture,
33
33
  commitsAreLoading: false,
34
34
  commitsError: null,
35
+ pullRequestStatus: null,
35
36
  relatedPullRequests: [],
36
37
  now,
37
38
  commentComposer: <div>comment-composer</div>,
@@ -115,4 +116,59 @@ describe('ConsoleItemDetail', () => {
115
116
  expect(title).not.toBeNull();
116
117
  expect(title?.contains(getByText('Awaiting Workspace'))).toBe(true);
117
118
  });
119
+
120
+ it('renders failing CI, missing checks, and conflict badges in the PR header', () => {
121
+ const { getByText, container } = render(
122
+ <ConsoleItemDetail
123
+ item={prItem}
124
+ {...baseProps}
125
+ pullRequestStatus={{
126
+ found: true,
127
+ isConflicted: true,
128
+ isPassedAllCiJob: false,
129
+ isCiStateSuccess: false,
130
+ isBranchOutOfDate: true,
131
+ missingRequiredCheckNames: ['build', 'test'],
132
+ }}
133
+ />,
134
+ );
135
+ const title = container.querySelector('.console-detail-title');
136
+ expect(title?.contains(getByText('CI failing'))).toBe(true);
137
+ expect(getByText(/missing: build, test/)).toBeInTheDocument();
138
+ expect(getByText('Conflict')).toBeInTheDocument();
139
+ expect(getByText('Out of date')).toBeInTheDocument();
140
+ });
141
+
142
+ it('renders a passing CI badge and no conflict badge when the PR is healthy', () => {
143
+ const { getByText, queryByText } = render(
144
+ <ConsoleItemDetail
145
+ item={prItem}
146
+ {...baseProps}
147
+ pullRequestStatus={{
148
+ found: true,
149
+ isConflicted: false,
150
+ isPassedAllCiJob: true,
151
+ isCiStateSuccess: true,
152
+ isBranchOutOfDate: false,
153
+ missingRequiredCheckNames: [],
154
+ }}
155
+ />,
156
+ );
157
+ expect(getByText('CI passing')).toBeInTheDocument();
158
+ expect(queryByText('Conflict')).toBeNull();
159
+ expect(queryByText('Out of date')).toBeNull();
160
+ });
161
+
162
+ it('does not render PR status badges for an issue item', () => {
163
+ const { queryByText } = render(
164
+ <ConsoleItemDetail
165
+ item={issueItem}
166
+ {...baseProps}
167
+ state={{ state: 'open', merged: false, isPullRequest: false }}
168
+ pullRequestStatus={null}
169
+ />,
170
+ );
171
+ expect(queryByText('CI passing')).toBeNull();
172
+ expect(queryByText('CI failing')).toBeNull();
173
+ });
118
174
  });
@@ -13,6 +13,7 @@ import type {
13
13
  ConsoleIssueState,
14
14
  ConsoleListItem,
15
15
  ConsoleOverlayStatus,
16
+ ConsolePullRequestStatus,
16
17
  ConsoleRelatedPullRequest,
17
18
  } from '../../logic/types';
18
19
  import { ConsoleMarkdownContent } from '../content/ConsoleMarkdownContent';
@@ -24,6 +25,7 @@ import { ConsoleCopyUrlButton } from './ConsoleCopyUrlButton';
24
25
  import type { ConsoleAddInlineComment } from './ConsoleFileDiff';
25
26
  import { ConsoleItemIcon } from './ConsoleItemIcon';
26
27
  import { ConsolePullRequestDetail } from './ConsolePullRequestDetail';
28
+ import { ConsolePullRequestStatusBadges } from './ConsolePullRequestStatusBadges';
27
29
 
28
30
  export type ConsoleRelatedPullRequestView = {
29
31
  pullRequest: ConsoleRelatedPullRequest;
@@ -53,12 +55,14 @@ export type ConsoleItemDetailProps = {
53
55
  commits: ConsoleCommit[];
54
56
  commitsAreLoading: boolean;
55
57
  commitsError: string | null;
58
+ pullRequestStatus: ConsolePullRequestStatus | null;
56
59
  relatedPullRequests: ConsoleRelatedPullRequestView[];
57
60
  now: number;
58
61
  commentComposer: ReactNode;
59
62
  operationBar: ReactNode;
60
63
  buildImageProxyUrl?: ImageProxyUrlBuilder;
61
64
  onAddInlineComment?: ConsoleAddInlineComment;
65
+ buildAddInlineComment?: (prUrl: string) => ConsoleAddInlineComment;
62
66
  };
63
67
 
64
68
  export const ConsoleItemDetail = ({
@@ -79,12 +83,14 @@ export const ConsoleItemDetail = ({
79
83
  commits,
80
84
  commitsAreLoading,
81
85
  commitsError,
86
+ pullRequestStatus,
82
87
  relatedPullRequests,
83
88
  now,
84
89
  commentComposer,
85
90
  operationBar,
86
91
  buildImageProxyUrl,
87
92
  onAddInlineComment,
93
+ buildAddInlineComment,
88
94
  }: ConsoleItemDetailProps) => {
89
95
  const resolvedState = state?.state ?? 'open';
90
96
  const merged = state?.merged ?? false;
@@ -128,6 +134,17 @@ export const ConsoleItemDetail = ({
128
134
  {overlayStatus.name}
129
135
  </span>
130
136
  )}
137
+ {item.isPr && pullRequestStatus?.found && (
138
+ <ConsolePullRequestStatusBadges
139
+ isConflicted={pullRequestStatus.isConflicted}
140
+ isPassedAllCiJob={pullRequestStatus.isPassedAllCiJob}
141
+ isCiStateSuccess={pullRequestStatus.isCiStateSuccess}
142
+ isBranchOutOfDate={pullRequestStatus.isBranchOutOfDate}
143
+ missingRequiredCheckNames={
144
+ pullRequestStatus.missingRequiredCheckNames
145
+ }
146
+ />
147
+ )}
131
148
  <ConsoleItemIcon
132
149
  isPr={item.isPr}
133
150
  state={resolvedState}
@@ -257,6 +274,11 @@ export const ConsoleItemDetail = ({
257
274
  commitsError={related.commitsError}
258
275
  now={now}
259
276
  buildImageProxyUrl={buildImageProxyUrl}
277
+ onAddInlineComment={
278
+ buildAddInlineComment
279
+ ? buildAddInlineComment(related.pullRequest.url)
280
+ : undefined
281
+ }
260
282
  />
261
283
  ))}
262
284
 
@@ -29,3 +29,20 @@ export const WithDetail: Story = {
29
29
  commitsError: null,
30
30
  },
31
31
  };
32
+
33
+ export const WithInlineComments: Story = {
34
+ args: {
35
+ pullRequest: consoleRelatedPullRequestsFixture[0],
36
+ body: consoleRelatedPullRequestsFixture[0].summary?.body ?? '',
37
+ bodyIsLoading: false,
38
+ files: consoleChangedFilesFixture,
39
+ filesAreLoading: false,
40
+ filesError: null,
41
+ commits: consoleCommitsFixture,
42
+ commitsAreLoading: false,
43
+ commitsError: null,
44
+ onAddInlineComment: async (path, line, side, body) => {
45
+ window.alert(`comment on ${path}:${line} (${side})\n${body}`);
46
+ },
47
+ },
48
+ };
@@ -38,6 +38,52 @@ describe('ConsolePullRequestDetail', () => {
38
38
  expect(getByText('27 files')).toBeInTheDocument();
39
39
  });
40
40
 
41
+ it('renders the CI status badge derived from the pull request fields', () => {
42
+ const { getByText } = render(
43
+ <ConsolePullRequestDetail
44
+ pullRequest={pullRequest}
45
+ body={pullRequest.summary?.body ?? ''}
46
+ bodyIsLoading={false}
47
+ files={consoleChangedFilesFixture}
48
+ filesAreLoading={false}
49
+ filesError={null}
50
+ commits={consoleCommitsFixture}
51
+ commitsAreLoading={false}
52
+ commitsError={null}
53
+ now={now}
54
+ />,
55
+ );
56
+ expect(getByText('CI passing')).toBeInTheDocument();
57
+ });
58
+
59
+ it('renders failing CI, conflict and out-of-date badges for an unhealthy pull request', () => {
60
+ const { getByText } = render(
61
+ <ConsolePullRequestDetail
62
+ pullRequest={{
63
+ ...pullRequest,
64
+ isConflicted: true,
65
+ isPassedAllCiJob: false,
66
+ isCiStateSuccess: false,
67
+ isBranchOutOfDate: true,
68
+ missingRequiredCheckNames: ['build'],
69
+ }}
70
+ body={pullRequest.summary?.body ?? ''}
71
+ bodyIsLoading={false}
72
+ files={consoleChangedFilesFixture}
73
+ filesAreLoading={false}
74
+ filesError={null}
75
+ commits={consoleCommitsFixture}
76
+ commitsAreLoading={false}
77
+ commitsError={null}
78
+ now={now}
79
+ />,
80
+ );
81
+ expect(getByText('CI failing')).toBeInTheDocument();
82
+ expect(getByText(/missing: build/)).toBeInTheDocument();
83
+ expect(getByText('Conflict')).toBeInTheDocument();
84
+ expect(getByText('Out of date')).toBeInTheDocument();
85
+ });
86
+
41
87
  it('renders a copy URL button for the pull request url', () => {
42
88
  const { getByRole } = render(
43
89
  <ConsolePullRequestDetail
@@ -9,6 +9,8 @@ import { ConsolePanel } from '../layout/ConsolePanel';
9
9
  import { ConsoleChangedFileList } from './ConsoleChangedFileList';
10
10
  import { ConsoleCommitList } from './ConsoleCommitList';
11
11
  import { ConsoleCopyUrlButton } from './ConsoleCopyUrlButton';
12
+ import type { ConsoleAddInlineComment } from './ConsoleFileDiff';
13
+ import { ConsolePullRequestStatusBadges } from './ConsolePullRequestStatusBadges';
12
14
 
13
15
  export type ConsolePullRequestSectionProps = {
14
16
  pullRequest: ConsoleRelatedPullRequest;
@@ -22,6 +24,7 @@ export type ConsolePullRequestSectionProps = {
22
24
  commitsError: string | null;
23
25
  now: number;
24
26
  buildImageProxyUrl?: ImageProxyUrlBuilder;
27
+ onAddInlineComment?: ConsoleAddInlineComment;
25
28
  };
26
29
 
27
30
  export const ConsolePullRequestDetail = ({
@@ -36,6 +39,7 @@ export const ConsolePullRequestDetail = ({
36
39
  commitsError,
37
40
  now,
38
41
  buildImageProxyUrl,
42
+ onAddInlineComment,
39
43
  }: ConsolePullRequestSectionProps) => {
40
44
  const summary = pullRequest.summary;
41
45
  const filesCount =
@@ -56,6 +60,13 @@ export const ConsolePullRequestDetail = ({
56
60
  {pullRequest.isDraft && (
57
61
  <span className="console-pr-section-state">draft</span>
58
62
  )}
63
+ <ConsolePullRequestStatusBadges
64
+ isConflicted={pullRequest.isConflicted}
65
+ isPassedAllCiJob={pullRequest.isPassedAllCiJob}
66
+ isCiStateSuccess={pullRequest.isCiStateSuccess}
67
+ isBranchOutOfDate={pullRequest.isBranchOutOfDate}
68
+ missingRequiredCheckNames={pullRequest.missingRequiredCheckNames}
69
+ />
59
70
  <ConsoleCopyUrlButton url={pullRequest.url} label="Copy PR URL" />
60
71
  <div className="console-pr-statbar">
61
72
  {pullRequest.branchName !== null && (
@@ -87,6 +98,7 @@ export const ConsolePullRequestDetail = ({
87
98
  files={files}
88
99
  isLoading={filesAreLoading}
89
100
  error={filesError}
101
+ onAddInlineComment={onAddInlineComment}
90
102
  />
91
103
  </ConsolePanel>
92
104
  <ConsolePanel title="Commits" count={commitsCount} defaultCollapsed>
@@ -0,0 +1,41 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { ConsolePullRequestStatusBadges } from './ConsolePullRequestStatusBadges';
3
+
4
+ const meta: Meta<typeof ConsolePullRequestStatusBadges> = {
5
+ title: 'Console/ConsolePullRequestStatusBadges',
6
+ component: ConsolePullRequestStatusBadges,
7
+ };
8
+
9
+ export default meta;
10
+
11
+ type Story = StoryObj<typeof ConsolePullRequestStatusBadges>;
12
+
13
+ export const Passing: Story = {
14
+ args: {
15
+ isConflicted: false,
16
+ isPassedAllCiJob: true,
17
+ isCiStateSuccess: true,
18
+ isBranchOutOfDate: false,
19
+ missingRequiredCheckNames: [],
20
+ },
21
+ };
22
+
23
+ export const FailingWithMissingChecks: Story = {
24
+ args: {
25
+ isConflicted: false,
26
+ isPassedAllCiJob: false,
27
+ isCiStateSuccess: false,
28
+ isBranchOutOfDate: false,
29
+ missingRequiredCheckNames: ['build', 'test'],
30
+ },
31
+ };
32
+
33
+ export const ConflictedAndOutOfDate: Story = {
34
+ args: {
35
+ isConflicted: true,
36
+ isPassedAllCiJob: false,
37
+ isCiStateSuccess: false,
38
+ isBranchOutOfDate: true,
39
+ missingRequiredCheckNames: ['build', 'test'],
40
+ },
41
+ };
@@ -0,0 +1,72 @@
1
+ import { render } from '@testing-library/react';
2
+ import { ConsolePullRequestStatusBadges } from './ConsolePullRequestStatusBadges';
3
+
4
+ describe('ConsolePullRequestStatusBadges', () => {
5
+ it('renders a passing CI badge when all CI jobs passed and the CI state is success', () => {
6
+ const { getByText, queryByText } = render(
7
+ <ConsolePullRequestStatusBadges
8
+ isConflicted={false}
9
+ isPassedAllCiJob={true}
10
+ isCiStateSuccess={true}
11
+ isBranchOutOfDate={false}
12
+ missingRequiredCheckNames={[]}
13
+ />,
14
+ );
15
+ expect(getByText('CI passing')).toBeInTheDocument();
16
+ expect(queryByText('Conflict')).toBeNull();
17
+ expect(queryByText('Out of date')).toBeNull();
18
+ });
19
+
20
+ it('renders a failing CI badge with missing required checks', () => {
21
+ const { getByText } = render(
22
+ <ConsolePullRequestStatusBadges
23
+ isConflicted={false}
24
+ isPassedAllCiJob={false}
25
+ isCiStateSuccess={true}
26
+ isBranchOutOfDate={false}
27
+ missingRequiredCheckNames={['build', 'test']}
28
+ />,
29
+ );
30
+ expect(getByText('CI failing')).toBeInTheDocument();
31
+ expect(getByText(/missing: build, test/)).toBeInTheDocument();
32
+ });
33
+
34
+ it('renders the conflict badge when the pull request is conflicted', () => {
35
+ const { getByText } = render(
36
+ <ConsolePullRequestStatusBadges
37
+ isConflicted={true}
38
+ isPassedAllCiJob={true}
39
+ isCiStateSuccess={true}
40
+ isBranchOutOfDate={false}
41
+ missingRequiredCheckNames={[]}
42
+ />,
43
+ );
44
+ expect(getByText('Conflict')).toBeInTheDocument();
45
+ });
46
+
47
+ it('renders the out-of-date badge when the branch is behind the base branch', () => {
48
+ const { getByText } = render(
49
+ <ConsolePullRequestStatusBadges
50
+ isConflicted={false}
51
+ isPassedAllCiJob={true}
52
+ isCiStateSuccess={true}
53
+ isBranchOutOfDate={true}
54
+ missingRequiredCheckNames={[]}
55
+ />,
56
+ );
57
+ expect(getByText('Out of date')).toBeInTheDocument();
58
+ });
59
+
60
+ it('treats a successful CI state but unfinished jobs as failing', () => {
61
+ const { getByText } = render(
62
+ <ConsolePullRequestStatusBadges
63
+ isConflicted={false}
64
+ isPassedAllCiJob={false}
65
+ isCiStateSuccess={true}
66
+ isBranchOutOfDate={false}
67
+ missingRequiredCheckNames={[]}
68
+ />,
69
+ );
70
+ expect(getByText('CI failing')).toBeInTheDocument();
71
+ });
72
+ });