github-issue-tower-defence-management 1.112.6 → 1.114.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 (59) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -1
  3. package/bin/adapter/entry-points/cli/index.js +1 -0
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/console/consoleReadApi.js +58 -2
  6. package/bin/adapter/entry-points/console/consoleReadApi.js.map +1 -1
  7. package/bin/adapter/entry-points/console/ui-dist/assets/{index-DKFbU8lr.css → index-Blb_xqIx.css} +1 -1
  8. package/bin/adapter/entry-points/console/ui-dist/assets/index-DQyZ8KpG.js +101 -0
  9. package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
  10. package/bin/adapter/entry-points/console/webServer.js +6 -0
  11. package/bin/adapter/entry-points/console/webServer.js.map +1 -1
  12. package/package.json +1 -1
  13. package/src/adapter/entry-points/cli/index.ts +5 -1
  14. package/src/adapter/entry-points/console/consoleReadApi.test.ts +171 -0
  15. package/src/adapter/entry-points/console/consoleReadApi.ts +92 -1
  16. package/src/adapter/entry-points/console/ui/e2e/consoleScenario.spec.ts +44 -0
  17. package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +31 -8
  18. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMarkdownContent.stories.tsx +16 -0
  19. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMarkdownContent.test.tsx +61 -0
  20. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMarkdownContent.tsx +59 -5
  21. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleReferenceLink.stories.tsx +76 -0
  22. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleReferenceLink.test.tsx +56 -0
  23. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleReferenceLink.tsx +39 -0
  24. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentList.tsx +4 -0
  25. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.stories.tsx +64 -3
  26. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.test.tsx +68 -2
  27. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.tsx +21 -0
  28. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.test.tsx +46 -0
  29. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.tsx +12 -0
  30. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestStatusBadges.stories.tsx +41 -0
  31. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestStatusBadges.test.tsx +72 -0
  32. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestStatusBadges.tsx +71 -0
  33. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleCaches.ts +5 -0
  34. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleItemDetailData.test.ts +21 -0
  35. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleItemDetailData.ts +18 -0
  36. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleReferenceLink.test.ts +48 -0
  37. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleReferenceLink.ts +39 -0
  38. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +66 -2
  39. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +38 -6
  40. package/src/adapter/entry-points/console/ui/src/features/console/logic/references.test.ts +80 -0
  41. package/src/adapter/entry-points/console/ui/src/features/console/logic/references.ts +44 -0
  42. package/src/adapter/entry-points/console/ui/src/features/console/logic/types.ts +10 -0
  43. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.test.tsx +10 -0
  44. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +13 -0
  45. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleReferenceLinkContainer.tsx +25 -0
  46. package/src/adapter/entry-points/console/ui/src/features/console/testing/fixtures.ts +40 -0
  47. package/src/adapter/entry-points/console/ui/src/index.css +35 -0
  48. package/src/adapter/entry-points/console/ui-dist/assets/{index-DKFbU8lr.css → index-Blb_xqIx.css} +1 -1
  49. package/src/adapter/entry-points/console/ui-dist/assets/index-DQyZ8KpG.js +101 -0
  50. package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
  51. package/src/adapter/entry-points/console/webServer.test.ts +52 -1
  52. package/src/adapter/entry-points/console/webServer.ts +13 -0
  53. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  54. package/types/adapter/entry-points/console/consoleReadApi.d.ts +21 -0
  55. package/types/adapter/entry-points/console/consoleReadApi.d.ts.map +1 -1
  56. package/types/adapter/entry-points/console/webServer.d.ts +2 -1
  57. package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
  58. package/bin/adapter/entry-points/console/ui-dist/assets/index-DUygHx5u.js +0 -101
  59. package/src/adapter/entry-points/console/ui-dist/assets/index-DUygHx5u.js +0 -101
@@ -33,4 +33,65 @@ describe('ConsoleMarkdownContent', () => {
33
33
  ).not.toBeNull();
34
34
  });
35
35
  });
36
+
37
+ it('decorates pull request and issue links via the reference renderer', async () => {
38
+ const body =
39
+ 'See https://github.com/octo/repo/pull/7 and https://github.com/octo/repo/issues/9 for details.';
40
+ const renderReferenceLink = jest.fn((href: string) => (
41
+ <span className="decorated-reference" data-href={href}>
42
+ decorated:{href}
43
+ </span>
44
+ ));
45
+ const { container } = render(
46
+ <ConsoleMarkdownContent
47
+ body={body}
48
+ renderReferenceLink={renderReferenceLink}
49
+ />,
50
+ );
51
+ await waitFor(() => {
52
+ expect(container.querySelectorAll('.decorated-reference').length).toBe(2);
53
+ });
54
+ const decoratedHrefs = Array.from(
55
+ container.querySelectorAll('.decorated-reference'),
56
+ ).map((node) => node.getAttribute('data-href'));
57
+ expect(decoratedHrefs).toEqual([
58
+ 'https://github.com/octo/repo/pull/7',
59
+ 'https://github.com/octo/repo/issues/9',
60
+ ]);
61
+ });
62
+
63
+ it('leaves non-issue links untouched', async () => {
64
+ const body =
65
+ 'Docs at https://example.com/page and source https://github.com/octo/repo/blob/main/file.ts';
66
+ const renderReferenceLink = jest.fn(() => (
67
+ <span className="decorated-reference">decorated</span>
68
+ ));
69
+ const { container } = render(
70
+ <ConsoleMarkdownContent
71
+ body={body}
72
+ renderReferenceLink={renderReferenceLink}
73
+ />,
74
+ );
75
+ await waitFor(() => {
76
+ const anchors = container.querySelectorAll('a[href]');
77
+ expect(anchors.length).toBe(2);
78
+ });
79
+ expect(renderReferenceLink).not.toHaveBeenCalled();
80
+ expect(container.querySelector('.decorated-reference')).toBeNull();
81
+ });
82
+
83
+ it('keeps reference links as plain anchors when no renderer is provided', () => {
84
+ const { container } = render(
85
+ <ConsoleMarkdownContent
86
+ body={'See https://github.com/octo/repo/pull/7 now.'}
87
+ />,
88
+ );
89
+ const anchor = container.querySelector('a[href]');
90
+ expect(anchor?.getAttribute('href')).toBe(
91
+ 'https://github.com/octo/repo/pull/7',
92
+ );
93
+ expect(
94
+ container.querySelector('.console-markdown-reference-host'),
95
+ ).toBeNull();
96
+ });
36
97
  });
@@ -1,4 +1,5 @@
1
- import { useEffect, useMemo, useRef } from 'react';
1
+ import { type ReactNode, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { createPortal } from 'react-dom';
2
3
  import {
3
4
  type ImageProxyUrlBuilder,
4
5
  rewriteGitHubImageSources,
@@ -7,21 +8,54 @@ import {
7
8
  renderMarkdownToSafeHtml,
8
9
  splitMarkdownSegments,
9
10
  } from '../../lib/markdown';
11
+ import { parseGitHubReferenceUrl } from '../../logic/references';
10
12
  import { ConsoleMermaidDiagram } from './ConsoleMermaidDiagram';
11
13
 
14
+ export type ConsoleReferenceLinkRenderer = (
15
+ href: string,
16
+ fallbackText: string,
17
+ ) => ReactNode;
18
+
12
19
  export type ConsoleMarkdownViewProps = {
13
20
  body: string;
14
21
  buildImageProxyUrl?: ImageProxyUrlBuilder;
22
+ renderReferenceLink?: ConsoleReferenceLinkRenderer;
15
23
  };
16
24
 
17
25
  type ConsoleMarkdownHtmlBlockProps = {
18
26
  source: string;
19
27
  buildImageProxyUrl?: ImageProxyUrlBuilder;
28
+ renderReferenceLink?: ConsoleReferenceLinkRenderer;
29
+ };
30
+
31
+ type ReferenceMount = {
32
+ key: string;
33
+ host: HTMLElement;
34
+ href: string;
35
+ fallbackText: string;
36
+ };
37
+
38
+ const collectReferenceMounts = (container: HTMLElement): ReferenceMount[] => {
39
+ const anchors = container.querySelectorAll<HTMLAnchorElement>('a[href]');
40
+ const mounts: ReferenceMount[] = [];
41
+ anchors.forEach((anchor, index) => {
42
+ const href = anchor.getAttribute('href') ?? '';
43
+ if (parseGitHubReferenceUrl(href) === null) {
44
+ return;
45
+ }
46
+ const fallbackText = anchor.textContent ?? href;
47
+ const host = document.createElement('span');
48
+ host.className = 'console-markdown-reference-host';
49
+ anchor.replaceWith(host);
50
+ mounts.push({ key: `${index}:${href}`, host, href, fallbackText });
51
+ });
52
+ return mounts;
20
53
  };
21
54
 
22
55
  const ConsoleMarkdownHtmlBlock = ({
23
56
  source,
24
57
  buildImageProxyUrl,
58
+ renderReferenceLink,
25
59
  }: ConsoleMarkdownHtmlBlockProps) => {
26
60
  const html = useMemo(() => {
27
61
  const safeHtml = renderMarkdownToSafeHtml(source);
@@ -31,20 +65,39 @@ const ConsoleMarkdownHtmlBlock = ({
31
65
  return rewriteGitHubImageSources(safeHtml, buildImageProxyUrl);
32
66
  }, [source, buildImageProxyUrl]);
33
67
  const containerRef = useRef<HTMLDivElement>(null);
68
+ const [referenceMounts, setReferenceMounts] = useState<ReferenceMount[]>([]);
34
69
 
35
70
  useEffect(() => {
36
71
  const container = containerRef.current;
37
- if (container !== null) {
38
- container.innerHTML = html;
72
+ if (container === null) {
73
+ return;
74
+ }
75
+ container.innerHTML = html;
76
+ if (renderReferenceLink === undefined) {
77
+ setReferenceMounts([]);
78
+ return;
39
79
  }
40
- }, [html]);
80
+ setReferenceMounts(collectReferenceMounts(container));
81
+ }, [html, renderReferenceLink]);
41
82
 
42
- return <div ref={containerRef} className="console-markdown" />;
83
+ return (
84
+ <div ref={containerRef} className="console-markdown">
85
+ {renderReferenceLink !== undefined &&
86
+ referenceMounts.map((mount) =>
87
+ createPortal(
88
+ renderReferenceLink(mount.href, mount.fallbackText),
89
+ mount.host,
90
+ mount.key,
91
+ ),
92
+ )}
93
+ </div>
94
+ );
43
95
  };
44
96
 
45
97
  export const ConsoleMarkdownContent = ({
46
98
  body,
47
99
  buildImageProxyUrl,
100
+ renderReferenceLink,
48
101
  }: ConsoleMarkdownViewProps) => {
49
102
  const segments = useMemo(() => splitMarkdownSegments(body), [body]);
50
103
 
@@ -62,6 +115,7 @@ export const ConsoleMarkdownContent = ({
62
115
  key={segment.key}
63
116
  source={segment.source}
64
117
  buildImageProxyUrl={buildImageProxyUrl}
118
+ renderReferenceLink={renderReferenceLink}
65
119
  />
66
120
  ),
67
121
  )}
@@ -0,0 +1,76 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { ConsoleReferenceLink } from './ConsoleReferenceLink';
3
+
4
+ const meta: Meta<typeof ConsoleReferenceLink> = {
5
+ title: 'Console/ConsoleReferenceLink',
6
+ component: ConsoleReferenceLink,
7
+ };
8
+
9
+ export default meta;
10
+
11
+ type Story = StoryObj<typeof ConsoleReferenceLink>;
12
+
13
+ export const OpenPullRequest: Story = {
14
+ args: {
15
+ href: 'https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/pull/995',
16
+ fallbackText:
17
+ 'https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/pull/995',
18
+ state: {
19
+ state: 'open',
20
+ merged: false,
21
+ isPullRequest: true,
22
+ title: 'Rename serveConsole subcommand to serveWeb',
23
+ },
24
+ },
25
+ };
26
+
27
+ export const MergedPullRequest: Story = {
28
+ args: {
29
+ href: 'https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/pull/851',
30
+ fallbackText:
31
+ 'https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/pull/851',
32
+ state: {
33
+ state: 'closed',
34
+ merged: true,
35
+ isPullRequest: true,
36
+ title: 'Add serveConsole subcommand under entry-points',
37
+ },
38
+ },
39
+ };
40
+
41
+ export const OpenIssue: Story = {
42
+ args: {
43
+ href: 'https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/issues/845',
44
+ fallbackText:
45
+ 'https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/issues/845',
46
+ state: {
47
+ state: 'open',
48
+ merged: false,
49
+ isPullRequest: false,
50
+ title: 'Scaffold React console UI under entry-points with build bundling',
51
+ },
52
+ },
53
+ };
54
+
55
+ export const ClosedIssue: Story = {
56
+ args: {
57
+ href: 'https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/issues/692',
58
+ fallbackText:
59
+ 'https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/issues/692',
60
+ state: {
61
+ state: 'closed',
62
+ merged: false,
63
+ isPullRequest: false,
64
+ title: 'Publish the generated documentation site to GitHub Pages',
65
+ },
66
+ },
67
+ };
68
+
69
+ export const UnresolvedFallback: Story = {
70
+ args: {
71
+ href: 'https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/pull/995',
72
+ fallbackText:
73
+ 'https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/pull/995',
74
+ state: null,
75
+ },
76
+ };
@@ -0,0 +1,56 @@
1
+ import { render } from '@testing-library/react';
2
+ import { ConsoleReferenceLink } from './ConsoleReferenceLink';
3
+
4
+ describe('ConsoleReferenceLink', () => {
5
+ const href = 'https://github.com/octo/repo/pull/7';
6
+
7
+ it('renders the state icon and fetched title when state is resolved', () => {
8
+ const { container, getByText } = render(
9
+ <ConsoleReferenceLink
10
+ href={href}
11
+ fallbackText={href}
12
+ state={{
13
+ state: 'open',
14
+ merged: false,
15
+ isPullRequest: true,
16
+ title: 'Decorate PR links',
17
+ }}
18
+ />,
19
+ );
20
+ expect(getByText('Decorate PR links')).toBeInTheDocument();
21
+ expect(container.querySelector('.console-item-icon')).not.toBeNull();
22
+ const anchor = container.querySelector('a');
23
+ expect(anchor?.getAttribute('href')).toBe(href);
24
+ });
25
+
26
+ it('falls back to the plain link when state is null', () => {
27
+ const { container, getByText } = render(
28
+ <ConsoleReferenceLink
29
+ href={href}
30
+ fallbackText="plain text"
31
+ state={null}
32
+ />,
33
+ );
34
+ expect(getByText('plain text')).toBeInTheDocument();
35
+ expect(container.querySelector('.console-item-icon')).toBeNull();
36
+ });
37
+
38
+ it('falls back to the plain link when the resolved title is empty', () => {
39
+ const { container } = render(
40
+ <ConsoleReferenceLink
41
+ href={href}
42
+ fallbackText="plain text"
43
+ state={{
44
+ state: 'open',
45
+ merged: false,
46
+ isPullRequest: true,
47
+ title: '',
48
+ }}
49
+ />,
50
+ );
51
+ expect(container.querySelector('.console-item-icon')).toBeNull();
52
+ expect(
53
+ container.querySelector('.console-markdown-reference-plain'),
54
+ ).not.toBeNull();
55
+ });
56
+ });
@@ -0,0 +1,39 @@
1
+ import { referenceStateToIconInput } from '../../logic/references';
2
+ import type { ConsoleIssueState } from '../../logic/types';
3
+ import { ConsoleItemIcon } from '../detail/ConsoleItemIcon';
4
+
5
+ export type ConsoleReferenceLinkProps = {
6
+ href: string;
7
+ fallbackText: string;
8
+ state: ConsoleIssueState | null;
9
+ };
10
+
11
+ export const ConsoleReferenceLink = ({
12
+ href,
13
+ fallbackText,
14
+ state,
15
+ }: ConsoleReferenceLinkProps) => {
16
+ if (state === null || state.title.trim() === '') {
17
+ return (
18
+ <a
19
+ className="console-markdown-reference console-markdown-reference-plain"
20
+ href={href}
21
+ target="_blank"
22
+ rel="noopener noreferrer"
23
+ >
24
+ {fallbackText}
25
+ </a>
26
+ );
27
+ }
28
+ return (
29
+ <a
30
+ className="console-markdown-reference"
31
+ href={href}
32
+ target="_blank"
33
+ rel="noopener noreferrer"
34
+ >
35
+ <ConsoleItemIcon {...referenceStateToIconInput(state)} />
36
+ <span className="console-markdown-reference-title">{state.title}</span>
37
+ </a>
38
+ );
39
+ };
@@ -2,6 +2,7 @@ import { useState } from 'react';
2
2
  import type { ImageProxyUrlBuilder } from '../../lib/imageProxy';
3
3
  import { formatRelativeTime } from '../../logic/relativeTime';
4
4
  import type { ConsoleComment } from '../../logic/types';
5
+ import type { ConsoleReferenceLinkRenderer } from '../content/ConsoleMarkdownContent';
5
6
  import { ConsoleMarkdownContent } from '../content/ConsoleMarkdownContent';
6
7
 
7
8
  export type ConsoleCommentListProps = {
@@ -10,6 +11,7 @@ export type ConsoleCommentListProps = {
10
11
  error: string | null;
11
12
  now: number;
12
13
  buildImageProxyUrl?: ImageProxyUrlBuilder;
14
+ renderReferenceLink?: ConsoleReferenceLinkRenderer;
13
15
  };
14
16
 
15
17
  export const ConsoleCommentList = ({
@@ -18,6 +20,7 @@ export const ConsoleCommentList = ({
18
20
  error,
19
21
  now,
20
22
  buildImageProxyUrl,
23
+ renderReferenceLink,
21
24
  }: ConsoleCommentListProps) => {
22
25
  const [showAll, setShowAll] = useState<boolean>(false);
23
26
 
@@ -64,6 +67,7 @@ export const ConsoleCommentList = ({
64
67
  <ConsoleMarkdownContent
65
68
  body={comment.body}
66
69
  buildImageProxyUrl={buildImageProxyUrl}
70
+ renderReferenceLink={renderReferenceLink}
67
71
  />
68
72
  </article>
69
73
  ))}
@@ -68,7 +68,48 @@ export const PullRequestItem: Story = {
68
68
  storyName: 'TDPM Console port',
69
69
  storyColorEnum: 'BLUE',
70
70
  overlayStatus: { name: 'Awaiting Workspace', color: 'BLUE' },
71
- state: { state: 'open', merged: false, isPullRequest: true },
71
+ state: {
72
+ state: 'open',
73
+ merged: false,
74
+ isPullRequest: true,
75
+ title: 'Add serveConsole subcommand under entry-points',
76
+ },
77
+ body: consoleMermaidBodyFixture,
78
+ bodyIsLoading: false,
79
+ bodyError: null,
80
+ comments: consoleCommentsFixture,
81
+ commentsAreLoading: false,
82
+ commentsError: null,
83
+ files: consoleChangedFilesFixture,
84
+ filesAreLoading: false,
85
+ filesError: null,
86
+ commits: consoleCommitsFixture,
87
+ commitsAreLoading: false,
88
+ commitsError: null,
89
+ pullRequestStatus: {
90
+ found: true,
91
+ isConflicted: false,
92
+ isPassedAllCiJob: true,
93
+ isCiStateSuccess: true,
94
+ isBranchOutOfDate: false,
95
+ missingRequiredCheckNames: [],
96
+ },
97
+ relatedPullRequests: [],
98
+ },
99
+ };
100
+
101
+ export const PullRequestItemFailingCiWithConflict: Story = {
102
+ args: {
103
+ item: consoleListItemsFixture[0],
104
+ storyName: 'TDPM Console port',
105
+ storyColorEnum: 'BLUE',
106
+ overlayStatus: { name: 'Awaiting Quality Check', color: 'YELLOW' },
107
+ state: {
108
+ state: 'open',
109
+ merged: false,
110
+ isPullRequest: true,
111
+ title: 'Add serveConsole subcommand under entry-points',
112
+ },
72
113
  body: consoleMermaidBodyFixture,
73
114
  bodyIsLoading: false,
74
115
  bodyError: null,
@@ -81,6 +122,14 @@ export const PullRequestItem: Story = {
81
122
  commits: consoleCommitsFixture,
82
123
  commitsAreLoading: false,
83
124
  commitsError: null,
125
+ pullRequestStatus: {
126
+ found: true,
127
+ isConflicted: true,
128
+ isPassedAllCiJob: false,
129
+ isCiStateSuccess: false,
130
+ isBranchOutOfDate: true,
131
+ missingRequiredCheckNames: ['build', 'test'],
132
+ },
84
133
  relatedPullRequests: [],
85
134
  },
86
135
  };
@@ -91,7 +140,12 @@ export const IssueWithRichMarkdownBody: Story = {
91
140
  storyName: 'TDPM Console port',
92
141
  storyColorEnum: 'BLUE',
93
142
  overlayStatus: null,
94
- state: { state: 'open', merged: false, isPullRequest: false },
143
+ state: {
144
+ state: 'open',
145
+ merged: false,
146
+ isPullRequest: false,
147
+ title: 'Scaffold React console UI under entry-points with build bundling',
148
+ },
95
149
  body: richMarkdownBody,
96
150
  bodyIsLoading: false,
97
151
  bodyError: null,
@@ -104,6 +158,7 @@ export const IssueWithRichMarkdownBody: Story = {
104
158
  commits: [],
105
159
  commitsAreLoading: false,
106
160
  commitsError: null,
161
+ pullRequestStatus: null,
107
162
  relatedPullRequests: [],
108
163
  operationBar: (
109
164
  <ConsoleOperationMenu
@@ -124,7 +179,12 @@ export const IssueWithLinkedPullRequest: Story = {
124
179
  storyName: 'TDPM Console port',
125
180
  storyColorEnum: 'BLUE',
126
181
  overlayStatus: null,
127
- state: { state: 'open', merged: false, isPullRequest: false },
182
+ state: {
183
+ state: 'open',
184
+ merged: false,
185
+ isPullRequest: false,
186
+ title: 'Scaffold React console UI under entry-points with build bundling',
187
+ },
128
188
  body: '## Issue body\n\nThis issue has a linked pull request.',
129
189
  bodyIsLoading: false,
130
190
  bodyError: null,
@@ -137,6 +197,7 @@ export const IssueWithLinkedPullRequest: Story = {
137
197
  commits: [],
138
198
  commitsAreLoading: false,
139
199
  commitsError: null,
200
+ pullRequestStatus: null,
140
201
  relatedPullRequests: [
141
202
  {
142
203
  pullRequest: consoleRelatedPullRequestsFixture[0],
@@ -19,7 +19,7 @@ const baseProps = {
19
19
  storyName: 'TDPM Console port',
20
20
  storyColorEnum: 'BLUE' as const,
21
21
  overlayStatus: null,
22
- state: { state: 'open', merged: false, isPullRequest: true },
22
+ state: { state: 'open', merged: false, isPullRequest: true, title: '' },
23
23
  body: '## Body heading',
24
24
  bodyIsLoading: false,
25
25
  bodyError: null,
@@ -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>,
@@ -78,7 +79,12 @@ describe('ConsoleItemDetail', () => {
78
79
  <ConsoleItemDetail
79
80
  item={issueItem}
80
81
  {...baseProps}
81
- state={{ state: 'open', merged: false, isPullRequest: false }}
82
+ state={{
83
+ state: 'open',
84
+ merged: false,
85
+ isPullRequest: false,
86
+ title: '',
87
+ }}
82
88
  />,
83
89
  );
84
90
  expect(queryByText('Changed files')).toBeNull();
@@ -115,4 +121,64 @@ describe('ConsoleItemDetail', () => {
115
121
  expect(title).not.toBeNull();
116
122
  expect(title?.contains(getByText('Awaiting Workspace'))).toBe(true);
117
123
  });
124
+
125
+ it('renders failing CI, missing checks, and conflict badges in the PR header', () => {
126
+ const { getByText, container } = render(
127
+ <ConsoleItemDetail
128
+ item={prItem}
129
+ {...baseProps}
130
+ pullRequestStatus={{
131
+ found: true,
132
+ isConflicted: true,
133
+ isPassedAllCiJob: false,
134
+ isCiStateSuccess: false,
135
+ isBranchOutOfDate: true,
136
+ missingRequiredCheckNames: ['build', 'test'],
137
+ }}
138
+ />,
139
+ );
140
+ const title = container.querySelector('.console-detail-title');
141
+ expect(title?.contains(getByText('CI failing'))).toBe(true);
142
+ expect(getByText(/missing: build, test/)).toBeInTheDocument();
143
+ expect(getByText('Conflict')).toBeInTheDocument();
144
+ expect(getByText('Out of date')).toBeInTheDocument();
145
+ });
146
+
147
+ it('renders a passing CI badge and no conflict badge when the PR is healthy', () => {
148
+ const { getByText, queryByText } = render(
149
+ <ConsoleItemDetail
150
+ item={prItem}
151
+ {...baseProps}
152
+ pullRequestStatus={{
153
+ found: true,
154
+ isConflicted: false,
155
+ isPassedAllCiJob: true,
156
+ isCiStateSuccess: true,
157
+ isBranchOutOfDate: false,
158
+ missingRequiredCheckNames: [],
159
+ }}
160
+ />,
161
+ );
162
+ expect(getByText('CI passing')).toBeInTheDocument();
163
+ expect(queryByText('Conflict')).toBeNull();
164
+ expect(queryByText('Out of date')).toBeNull();
165
+ });
166
+
167
+ it('does not render PR status badges for an issue item', () => {
168
+ const { queryByText } = render(
169
+ <ConsoleItemDetail
170
+ item={issueItem}
171
+ {...baseProps}
172
+ state={{
173
+ state: 'open',
174
+ merged: false,
175
+ isPullRequest: false,
176
+ title: '',
177
+ }}
178
+ pullRequestStatus={null}
179
+ />,
180
+ );
181
+ expect(queryByText('CI passing')).toBeNull();
182
+ expect(queryByText('CI failing')).toBeNull();
183
+ });
118
184
  });
@@ -13,8 +13,10 @@ import type {
13
13
  ConsoleIssueState,
14
14
  ConsoleListItem,
15
15
  ConsoleOverlayStatus,
16
+ ConsolePullRequestStatus,
16
17
  ConsoleRelatedPullRequest,
17
18
  } from '../../logic/types';
19
+ import type { ConsoleReferenceLinkRenderer } from '../content/ConsoleMarkdownContent';
18
20
  import { ConsoleMarkdownContent } from '../content/ConsoleMarkdownContent';
19
21
  import { ConsolePanel } from '../layout/ConsolePanel';
20
22
  import { ConsoleChangedFileList } from './ConsoleChangedFileList';
@@ -24,6 +26,7 @@ import { ConsoleCopyUrlButton } from './ConsoleCopyUrlButton';
24
26
  import type { ConsoleAddInlineComment } from './ConsoleFileDiff';
25
27
  import { ConsoleItemIcon } from './ConsoleItemIcon';
26
28
  import { ConsolePullRequestDetail } from './ConsolePullRequestDetail';
29
+ import { ConsolePullRequestStatusBadges } from './ConsolePullRequestStatusBadges';
27
30
 
28
31
  export type ConsoleRelatedPullRequestView = {
29
32
  pullRequest: ConsoleRelatedPullRequest;
@@ -53,11 +56,13 @@ export type ConsoleItemDetailProps = {
53
56
  commits: ConsoleCommit[];
54
57
  commitsAreLoading: boolean;
55
58
  commitsError: string | null;
59
+ pullRequestStatus: ConsolePullRequestStatus | null;
56
60
  relatedPullRequests: ConsoleRelatedPullRequestView[];
57
61
  now: number;
58
62
  commentComposer: ReactNode;
59
63
  operationBar: ReactNode;
60
64
  buildImageProxyUrl?: ImageProxyUrlBuilder;
65
+ renderReferenceLink?: ConsoleReferenceLinkRenderer;
61
66
  onAddInlineComment?: ConsoleAddInlineComment;
62
67
  buildAddInlineComment?: (prUrl: string) => ConsoleAddInlineComment;
63
68
  };
@@ -80,11 +85,13 @@ export const ConsoleItemDetail = ({
80
85
  commits,
81
86
  commitsAreLoading,
82
87
  commitsError,
88
+ pullRequestStatus,
83
89
  relatedPullRequests,
84
90
  now,
85
91
  commentComposer,
86
92
  operationBar,
87
93
  buildImageProxyUrl,
94
+ renderReferenceLink,
88
95
  onAddInlineComment,
89
96
  buildAddInlineComment,
90
97
  }: ConsoleItemDetailProps) => {
@@ -130,6 +137,17 @@ export const ConsoleItemDetail = ({
130
137
  {overlayStatus.name}
131
138
  </span>
132
139
  )}
140
+ {item.isPr && pullRequestStatus?.found && (
141
+ <ConsolePullRequestStatusBadges
142
+ isConflicted={pullRequestStatus.isConflicted}
143
+ isPassedAllCiJob={pullRequestStatus.isPassedAllCiJob}
144
+ isCiStateSuccess={pullRequestStatus.isCiStateSuccess}
145
+ isBranchOutOfDate={pullRequestStatus.isBranchOutOfDate}
146
+ missingRequiredCheckNames={
147
+ pullRequestStatus.missingRequiredCheckNames
148
+ }
149
+ />
150
+ )}
133
151
  <ConsoleItemIcon
134
152
  isPr={item.isPr}
135
153
  state={resolvedState}
@@ -204,6 +222,7 @@ export const ConsoleItemDetail = ({
204
222
  <ConsoleMarkdownContent
205
223
  body={body}
206
224
  buildImageProxyUrl={buildImageProxyUrl}
225
+ renderReferenceLink={renderReferenceLink}
207
226
  />
208
227
  )}
209
228
  </ConsolePanel>
@@ -230,6 +249,7 @@ export const ConsoleItemDetail = ({
230
249
  error={commentsError}
231
250
  now={now}
232
251
  buildImageProxyUrl={buildImageProxyUrl}
252
+ renderReferenceLink={renderReferenceLink}
233
253
  />
234
254
  </ConsolePanel>
235
255
 
@@ -259,6 +279,7 @@ export const ConsoleItemDetail = ({
259
279
  commitsError={related.commitsError}
260
280
  now={now}
261
281
  buildImageProxyUrl={buildImageProxyUrl}
282
+ renderReferenceLink={renderReferenceLink}
262
283
  onAddInlineComment={
263
284
  buildAddInlineComment
264
285
  ? buildAddInlineComment(related.pullRequest.url)