github-issue-tower-defence-management 1.113.0 → 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 (42) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +1 -1
  3. package/bin/adapter/entry-points/console/consoleReadApi.js +11 -1
  4. package/bin/adapter/entry-points/console/consoleReadApi.js.map +1 -1
  5. package/bin/adapter/entry-points/console/ui-dist/assets/{index-CEJmPNRK.css → index-Blb_xqIx.css} +1 -1
  6. package/bin/adapter/entry-points/console/ui-dist/assets/index-DQyZ8KpG.js +101 -0
  7. package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
  8. package/package.json +1 -1
  9. package/src/adapter/entry-points/console/consoleReadApi.test.ts +82 -0
  10. package/src/adapter/entry-points/console/consoleReadApi.ts +21 -1
  11. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMarkdownContent.stories.tsx +16 -0
  12. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMarkdownContent.test.tsx +61 -0
  13. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMarkdownContent.tsx +59 -5
  14. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleReferenceLink.stories.tsx +76 -0
  15. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleReferenceLink.test.tsx +56 -0
  16. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleReferenceLink.tsx +39 -0
  17. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentList.tsx +4 -0
  18. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.stories.tsx +24 -4
  19. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.test.tsx +13 -3
  20. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.tsx +6 -0
  21. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.tsx +4 -0
  22. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleItemDetailData.test.ts +1 -0
  23. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleItemDetailData.ts +1 -0
  24. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleReferenceLink.test.ts +48 -0
  25. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleReferenceLink.ts +39 -0
  26. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +25 -2
  27. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +2 -1
  28. package/src/adapter/entry-points/console/ui/src/features/console/logic/references.test.ts +80 -0
  29. package/src/adapter/entry-points/console/ui/src/features/console/logic/references.ts +44 -0
  30. package/src/adapter/entry-points/console/ui/src/features/console/logic/types.ts +1 -0
  31. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.test.tsx +1 -0
  32. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +12 -0
  33. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleReferenceLinkContainer.tsx +25 -0
  34. package/src/adapter/entry-points/console/ui/src/features/console/testing/fixtures.ts +40 -0
  35. package/src/adapter/entry-points/console/ui/src/index.css +23 -0
  36. package/src/adapter/entry-points/console/ui-dist/assets/{index-CEJmPNRK.css → index-Blb_xqIx.css} +1 -1
  37. package/src/adapter/entry-points/console/ui-dist/assets/index-DQyZ8KpG.js +101 -0
  38. package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
  39. package/types/adapter/entry-points/console/consoleReadApi.d.ts +1 -0
  40. package/types/adapter/entry-points/console/consoleReadApi.d.ts.map +1 -1
  41. package/bin/adapter/entry-points/console/ui-dist/assets/index-DfNrA5uV.js +0 -101
  42. package/src/adapter/entry-points/console/ui-dist/assets/index-DfNrA5uV.js +0 -101
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>TDPM Console</title>
7
- <script type="module" crossorigin src="/assets/index-DfNrA5uV.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-CEJmPNRK.css">
7
+ <script type="module" crossorigin src="/assets/index-DQyZ8KpG.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-Blb_xqIx.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-issue-tower-defence-management",
3
- "version": "1.113.0",
3
+ "version": "1.114.0",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "scripts": {
@@ -1,5 +1,33 @@
1
1
  import { mock } from 'jest-mock-extended';
2
+ import { Issue } from '../../../domain/entities/Issue';
2
3
  import { IssueRepository } from '../../../domain/usecases/adapter-interfaces/IssueRepository';
4
+
5
+ const buildIssueFixture = (title: string): Issue => ({
6
+ nameWithOwner: 'o/r',
7
+ number: 1,
8
+ title,
9
+ state: 'OPEN',
10
+ status: null,
11
+ story: null,
12
+ nextActionDate: null,
13
+ nextActionHour: null,
14
+ estimationMinutes: null,
15
+ dependedIssueUrls: [],
16
+ completionDate50PercentConfidence: null,
17
+ url: 'https://github.com/o/r/issues/1',
18
+ assignees: [],
19
+ labels: [],
20
+ org: 'o',
21
+ repo: 'r',
22
+ body: '',
23
+ itemId: 'itemId',
24
+ isPr: false,
25
+ isInProgress: false,
26
+ isClosed: false,
27
+ createdAt: new Date('2026-01-01T00:00:00Z'),
28
+ author: 'octocat',
29
+ closingIssueReferenceUrls: [],
30
+ });
3
31
  import {
4
32
  ISSUE_TITLE_CACHE_TTL_MS,
5
33
  IssueTitleStateCache,
@@ -241,6 +269,9 @@ describe('consoleReadApi', () => {
241
269
  merged: false,
242
270
  isPullRequest: false,
243
271
  });
272
+ issueRepository.getIssueByUrl.mockResolvedValue(
273
+ buildIssueFixture('Issue title from repository'),
274
+ );
244
275
  const cache = new IssueTitleStateCache(() => 0);
245
276
  const url = 'https://github.com/o/r/issues/1';
246
277
  const first = await handleIssueTitle(issueRepository, cache, url);
@@ -248,12 +279,63 @@ describe('consoleReadApi', () => {
248
279
  state: 'OPEN',
249
280
  merged: false,
250
281
  isPullRequest: false,
282
+ title: 'Issue title from repository',
251
283
  });
252
284
  const second = await handleIssueTitle(issueRepository, cache, url);
253
285
  expect(second.body).toEqual(first.body);
254
286
  expect(issueRepository.getIssueOrPullRequestState).toHaveBeenCalledTimes(
255
287
  1,
256
288
  );
289
+ expect(issueRepository.getIssueByUrl).toHaveBeenCalledTimes(1);
290
+ });
291
+
292
+ it('returns the pull request title from the summary for pull request urls', async () => {
293
+ const issueRepository = mock<IssueRepository>();
294
+ issueRepository.getIssueOrPullRequestState.mockResolvedValue({
295
+ state: 'CLOSED',
296
+ merged: true,
297
+ isPullRequest: true,
298
+ });
299
+ issueRepository.getPullRequestSummary.mockResolvedValue({
300
+ title: 'Pull request title from summary',
301
+ body: 'body',
302
+ additions: 1,
303
+ deletions: 0,
304
+ changedFiles: 1,
305
+ });
306
+ const cache = new IssueTitleStateCache(() => 0);
307
+ const url = 'https://github.com/o/r/pull/2';
308
+ const response = await handleIssueTitle(issueRepository, cache, url);
309
+ expect(response.body).toEqual({
310
+ state: 'CLOSED',
311
+ merged: true,
312
+ isPullRequest: true,
313
+ title: 'Pull request title from summary',
314
+ });
315
+ expect(issueRepository.getPullRequestSummary).toHaveBeenCalledWith(url);
316
+ expect(issueRepository.getIssueByUrl).not.toHaveBeenCalled();
317
+ });
318
+
319
+ it('falls back to an empty title when the title source returns nothing', async () => {
320
+ const issueRepository = mock<IssueRepository>();
321
+ issueRepository.getIssueOrPullRequestState.mockResolvedValue({
322
+ state: 'OPEN',
323
+ merged: false,
324
+ isPullRequest: false,
325
+ });
326
+ issueRepository.getIssueByUrl.mockResolvedValue(null);
327
+ const cache = new IssueTitleStateCache(() => 0);
328
+ const response = await handleIssueTitle(
329
+ issueRepository,
330
+ cache,
331
+ 'https://github.com/o/r/issues/3',
332
+ );
333
+ expect(response.body).toEqual({
334
+ state: 'OPEN',
335
+ merged: false,
336
+ isPullRequest: false,
337
+ title: '',
338
+ });
257
339
  });
258
340
 
259
341
  it('re-fetches a non-merged result after the TTL elapses', async () => {
@@ -12,6 +12,7 @@ export type IssueOrPullRequestState = {
12
12
  state: string;
13
13
  merged: boolean;
14
14
  isPullRequest: boolean;
15
+ title: string;
15
16
  };
16
17
 
17
18
  export type PullRequestStatus = {
@@ -215,6 +216,19 @@ export const handleRelatedPrs = async (
215
216
  return ok({ relatedPullRequests: withSummaries });
216
217
  };
217
218
 
219
+ const resolveIssueOrPullRequestTitle = async (
220
+ issueRepository: IssueRepository,
221
+ url: string,
222
+ isPullRequest: boolean,
223
+ ): Promise<string> => {
224
+ if (isPullRequest) {
225
+ const summary = await issueRepository.getPullRequestSummary(url);
226
+ return summary?.title ?? '';
227
+ }
228
+ const issue = await issueRepository.getIssueByUrl(url);
229
+ return issue?.title ?? '';
230
+ };
231
+
218
232
  export const handleIssueTitle = async (
219
233
  issueRepository: IssueRepository,
220
234
  cache: IssueTitleStateCache,
@@ -227,7 +241,13 @@ export const handleIssueTitle = async (
227
241
  if (cached !== null) {
228
242
  return ok(cached);
229
243
  }
230
- const state = await issueRepository.getIssueOrPullRequestState(url);
244
+ const baseState = await issueRepository.getIssueOrPullRequestState(url);
245
+ const title = await resolveIssueOrPullRequestTitle(
246
+ issueRepository,
247
+ url,
248
+ baseState.isPullRequest,
249
+ );
250
+ const state: IssueOrPullRequestState = { ...baseState, title };
231
251
  cache.set(url, state);
232
252
  return ok(state);
233
253
  };
@@ -4,9 +4,12 @@ import {
4
4
  consoleCodeFenceBodyFixture,
5
5
  consoleMarkdownBodyFixture,
6
6
  consoleMarkdownImageBodyFixture,
7
+ consoleMarkdownReferenceBodyFixture,
7
8
  consoleMermaidBodyFixture,
9
+ consoleReferenceStatesFixture,
8
10
  } from '../../testing/fixtures';
9
11
  import { ConsoleMarkdownContent } from './ConsoleMarkdownContent';
12
+ import { ConsoleReferenceLink } from './ConsoleReferenceLink';
10
13
 
11
14
  const meta: Meta<typeof ConsoleMarkdownContent> = {
12
15
  title: 'Console/ConsoleMarkdownContent',
@@ -37,6 +40,19 @@ export const WithProxiedGitHubImages: Story = {
37
40
  },
38
41
  };
39
42
 
43
+ export const WithDecoratedReferenceLinks: Story = {
44
+ args: {
45
+ body: consoleMarkdownReferenceBodyFixture,
46
+ renderReferenceLink: (href, fallbackText) => (
47
+ <ConsoleReferenceLink
48
+ href={href}
49
+ fallbackText={fallbackText}
50
+ state={consoleReferenceStatesFixture[href] ?? null}
51
+ />
52
+ ),
53
+ },
54
+ };
55
+
40
56
  export const Empty: Story = {
41
57
  args: { body: '' },
42
58
  };
@@ -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,12 @@ 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
+ },
72
77
  body: consoleMermaidBodyFixture,
73
78
  bodyIsLoading: false,
74
79
  bodyError: null,
@@ -99,7 +104,12 @@ export const PullRequestItemFailingCiWithConflict: Story = {
99
104
  storyName: 'TDPM Console port',
100
105
  storyColorEnum: 'BLUE',
101
106
  overlayStatus: { name: 'Awaiting Quality Check', color: 'YELLOW' },
102
- state: { state: 'open', merged: false, isPullRequest: true },
107
+ state: {
108
+ state: 'open',
109
+ merged: false,
110
+ isPullRequest: true,
111
+ title: 'Add serveConsole subcommand under entry-points',
112
+ },
103
113
  body: consoleMermaidBodyFixture,
104
114
  bodyIsLoading: false,
105
115
  bodyError: null,
@@ -130,7 +140,12 @@ export const IssueWithRichMarkdownBody: Story = {
130
140
  storyName: 'TDPM Console port',
131
141
  storyColorEnum: 'BLUE',
132
142
  overlayStatus: null,
133
- 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
+ },
134
149
  body: richMarkdownBody,
135
150
  bodyIsLoading: false,
136
151
  bodyError: null,
@@ -164,7 +179,12 @@ export const IssueWithLinkedPullRequest: Story = {
164
179
  storyName: 'TDPM Console port',
165
180
  storyColorEnum: 'BLUE',
166
181
  overlayStatus: null,
167
- 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
+ },
168
188
  body: '## Issue body\n\nThis issue has a linked pull request.',
169
189
  bodyIsLoading: false,
170
190
  bodyError: null,
@@ -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,
@@ -79,7 +79,12 @@ describe('ConsoleItemDetail', () => {
79
79
  <ConsoleItemDetail
80
80
  item={issueItem}
81
81
  {...baseProps}
82
- state={{ state: 'open', merged: false, isPullRequest: false }}
82
+ state={{
83
+ state: 'open',
84
+ merged: false,
85
+ isPullRequest: false,
86
+ title: '',
87
+ }}
83
88
  />,
84
89
  );
85
90
  expect(queryByText('Changed files')).toBeNull();
@@ -164,7 +169,12 @@ describe('ConsoleItemDetail', () => {
164
169
  <ConsoleItemDetail
165
170
  item={issueItem}
166
171
  {...baseProps}
167
- state={{ state: 'open', merged: false, isPullRequest: false }}
172
+ state={{
173
+ state: 'open',
174
+ merged: false,
175
+ isPullRequest: false,
176
+ title: '',
177
+ }}
168
178
  pullRequestStatus={null}
169
179
  />,
170
180
  );