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

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 (78) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +2 -2
  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/bin/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.js +36 -27
  9. package/bin/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.js.map +1 -1
  10. package/bin/adapter/repositories/FileSystemSessionRecordReader.js +69 -0
  11. package/bin/adapter/repositories/FileSystemSessionRecordReader.js.map +1 -0
  12. package/bin/adapter/repositories/LocalProcessLiveSessionProcessSnapshotProvider.js +9 -2
  13. package/bin/adapter/repositories/LocalProcessLiveSessionProcessSnapshotProvider.js.map +1 -1
  14. package/bin/domain/usecases/ResolveInteractiveLiveSessionsUseCase.js +15 -0
  15. package/bin/domain/usecases/ResolveInteractiveLiveSessionsUseCase.js.map +1 -1
  16. package/bin/domain/usecases/adapter-interfaces/SessionRecordReader.js +3 -0
  17. package/bin/domain/usecases/adapter-interfaces/SessionRecordReader.js.map +1 -0
  18. package/package.json +1 -1
  19. package/src/adapter/entry-points/console/consoleReadApi.test.ts +82 -0
  20. package/src/adapter/entry-points/console/consoleReadApi.ts +21 -1
  21. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMarkdownContent.stories.tsx +16 -0
  22. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMarkdownContent.test.tsx +61 -0
  23. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMarkdownContent.tsx +59 -5
  24. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleReferenceLink.stories.tsx +76 -0
  25. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleReferenceLink.test.tsx +56 -0
  26. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleReferenceLink.tsx +39 -0
  27. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentList.tsx +4 -0
  28. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.stories.tsx +24 -4
  29. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.test.tsx +13 -3
  30. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.tsx +6 -0
  31. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.tsx +4 -0
  32. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleItemDetailData.test.ts +1 -0
  33. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleItemDetailData.ts +1 -0
  34. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleReferenceLink.test.ts +48 -0
  35. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleReferenceLink.ts +39 -0
  36. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +25 -2
  37. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +2 -1
  38. package/src/adapter/entry-points/console/ui/src/features/console/logic/references.test.ts +80 -0
  39. package/src/adapter/entry-points/console/ui/src/features/console/logic/references.ts +44 -0
  40. package/src/adapter/entry-points/console/ui/src/features/console/logic/types.ts +1 -0
  41. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.test.tsx +1 -0
  42. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +12 -0
  43. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleReferenceLinkContainer.tsx +25 -0
  44. package/src/adapter/entry-points/console/ui/src/features/console/testing/fixtures.ts +40 -0
  45. package/src/adapter/entry-points/console/ui/src/index.css +23 -0
  46. package/src/adapter/entry-points/console/ui-dist/assets/{index-CEJmPNRK.css → index-Blb_xqIx.css} +1 -1
  47. package/src/adapter/entry-points/console/ui-dist/assets/index-DQyZ8KpG.js +101 -0
  48. package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
  49. package/src/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.test.ts +107 -26
  50. package/src/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.ts +49 -29
  51. package/src/adapter/repositories/FileSystemSessionRecordReader.test.ts +59 -0
  52. package/src/adapter/repositories/FileSystemSessionRecordReader.ts +31 -0
  53. package/src/adapter/repositories/LocalProcessLiveSessionProcessSnapshotProvider.test.ts +51 -0
  54. package/src/adapter/repositories/LocalProcessLiveSessionProcessSnapshotProvider.ts +10 -1
  55. package/src/domain/entities/InteractiveLiveSession.ts +1 -0
  56. package/src/domain/entities/LiveSessionProcessSnapshot.ts +1 -0
  57. package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +4 -0
  58. package/src/domain/usecases/ResolveInteractiveLiveSessionsUseCase.test.ts +66 -59
  59. package/src/domain/usecases/ResolveInteractiveLiveSessionsUseCase.ts +19 -0
  60. package/src/domain/usecases/adapter-interfaces/SessionRecordReader.ts +3 -0
  61. package/types/adapter/entry-points/console/consoleReadApi.d.ts +1 -0
  62. package/types/adapter/entry-points/console/consoleReadApi.d.ts.map +1 -1
  63. package/types/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.d.ts +4 -11
  64. package/types/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.d.ts.map +1 -1
  65. package/types/adapter/repositories/FileSystemSessionRecordReader.d.ts +5 -0
  66. package/types/adapter/repositories/FileSystemSessionRecordReader.d.ts.map +1 -0
  67. package/types/adapter/repositories/LocalProcessLiveSessionProcessSnapshotProvider.d.ts +3 -1
  68. package/types/adapter/repositories/LocalProcessLiveSessionProcessSnapshotProvider.d.ts.map +1 -1
  69. package/types/domain/entities/InteractiveLiveSession.d.ts +1 -0
  70. package/types/domain/entities/InteractiveLiveSession.d.ts.map +1 -1
  71. package/types/domain/entities/LiveSessionProcessSnapshot.d.ts +1 -0
  72. package/types/domain/entities/LiveSessionProcessSnapshot.d.ts.map +1 -1
  73. package/types/domain/usecases/ResolveInteractiveLiveSessionsUseCase.d.ts +1 -0
  74. package/types/domain/usecases/ResolveInteractiveLiveSessionsUseCase.d.ts.map +1 -1
  75. package/types/domain/usecases/adapter-interfaces/SessionRecordReader.d.ts +4 -0
  76. package/types/domain/usecases/adapter-interfaces/SessionRecordReader.d.ts.map +1 -0
  77. package/bin/adapter/entry-points/console/ui-dist/assets/index-DfNrA5uV.js +0 -101
  78. package/src/adapter/entry-points/console/ui-dist/assets/index-DfNrA5uV.js +0 -101
@@ -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
  );
@@ -16,6 +16,7 @@ import type {
16
16
  ConsolePullRequestStatus,
17
17
  ConsoleRelatedPullRequest,
18
18
  } from '../../logic/types';
19
+ import type { ConsoleReferenceLinkRenderer } from '../content/ConsoleMarkdownContent';
19
20
  import { ConsoleMarkdownContent } from '../content/ConsoleMarkdownContent';
20
21
  import { ConsolePanel } from '../layout/ConsolePanel';
21
22
  import { ConsoleChangedFileList } from './ConsoleChangedFileList';
@@ -61,6 +62,7 @@ export type ConsoleItemDetailProps = {
61
62
  commentComposer: ReactNode;
62
63
  operationBar: ReactNode;
63
64
  buildImageProxyUrl?: ImageProxyUrlBuilder;
65
+ renderReferenceLink?: ConsoleReferenceLinkRenderer;
64
66
  onAddInlineComment?: ConsoleAddInlineComment;
65
67
  buildAddInlineComment?: (prUrl: string) => ConsoleAddInlineComment;
66
68
  };
@@ -89,6 +91,7 @@ export const ConsoleItemDetail = ({
89
91
  commentComposer,
90
92
  operationBar,
91
93
  buildImageProxyUrl,
94
+ renderReferenceLink,
92
95
  onAddInlineComment,
93
96
  buildAddInlineComment,
94
97
  }: ConsoleItemDetailProps) => {
@@ -219,6 +222,7 @@ export const ConsoleItemDetail = ({
219
222
  <ConsoleMarkdownContent
220
223
  body={body}
221
224
  buildImageProxyUrl={buildImageProxyUrl}
225
+ renderReferenceLink={renderReferenceLink}
222
226
  />
223
227
  )}
224
228
  </ConsolePanel>
@@ -245,6 +249,7 @@ export const ConsoleItemDetail = ({
245
249
  error={commentsError}
246
250
  now={now}
247
251
  buildImageProxyUrl={buildImageProxyUrl}
252
+ renderReferenceLink={renderReferenceLink}
248
253
  />
249
254
  </ConsolePanel>
250
255
 
@@ -274,6 +279,7 @@ export const ConsoleItemDetail = ({
274
279
  commitsError={related.commitsError}
275
280
  now={now}
276
281
  buildImageProxyUrl={buildImageProxyUrl}
282
+ renderReferenceLink={renderReferenceLink}
277
283
  onAddInlineComment={
278
284
  buildAddInlineComment
279
285
  ? buildAddInlineComment(related.pullRequest.url)
@@ -4,6 +4,7 @@ import type {
4
4
  ConsoleCommit,
5
5
  ConsoleRelatedPullRequest,
6
6
  } from '../../logic/types';
7
+ import type { ConsoleReferenceLinkRenderer } from '../content/ConsoleMarkdownContent';
7
8
  import { ConsoleMarkdownContent } from '../content/ConsoleMarkdownContent';
8
9
  import { ConsolePanel } from '../layout/ConsolePanel';
9
10
  import { ConsoleChangedFileList } from './ConsoleChangedFileList';
@@ -24,6 +25,7 @@ export type ConsolePullRequestSectionProps = {
24
25
  commitsError: string | null;
25
26
  now: number;
26
27
  buildImageProxyUrl?: ImageProxyUrlBuilder;
28
+ renderReferenceLink?: ConsoleReferenceLinkRenderer;
27
29
  onAddInlineComment?: ConsoleAddInlineComment;
28
30
  };
29
31
 
@@ -39,6 +41,7 @@ export const ConsolePullRequestDetail = ({
39
41
  commitsError,
40
42
  now,
41
43
  buildImageProxyUrl,
44
+ renderReferenceLink,
42
45
  onAddInlineComment,
43
46
  }: ConsolePullRequestSectionProps) => {
44
47
  const summary = pullRequest.summary;
@@ -90,6 +93,7 @@ export const ConsolePullRequestDetail = ({
90
93
  <ConsoleMarkdownContent
91
94
  body={summary?.body ?? body}
92
95
  buildImageProxyUrl={buildImageProxyUrl}
96
+ renderReferenceLink={renderReferenceLink}
93
97
  />
94
98
  )}
95
99
  </ConsolePanel>
@@ -63,6 +63,7 @@ const buildCaches = (related: ConsoleRelatedPullRequest[]): ConsoleCaches => {
63
63
  state: 'open',
64
64
  merged: false,
65
65
  isPullRequest: true,
66
+ title: 'Console item detail fixture title',
66
67
  }),
67
68
  fetchPullRequestStatus: async (): Promise<ConsolePullRequestStatus> => ({
68
69
  found: true,
@@ -20,6 +20,7 @@ const DEFAULT_STATE: ConsoleIssueState = {
20
20
  state: 'open',
21
21
  merged: false,
22
22
  isPullRequest: false,
23
+ title: '',
23
24
  };
24
25
  const DEFAULT_PR_STATUS: ConsolePullRequestStatus = {
25
26
  found: false,
@@ -0,0 +1,48 @@
1
+ import { renderHook, waitFor } from '@testing-library/react';
2
+ import { ResourceCache } from '../lib/resourceCache';
3
+ import type { ConsoleIssueState } from '../logic/types';
4
+ import { useConsoleReferenceLink } from './useConsoleReferenceLink';
5
+
6
+ const buildState = (title: string): ConsoleIssueState => ({
7
+ state: 'open',
8
+ merged: false,
9
+ isPullRequest: true,
10
+ title,
11
+ });
12
+
13
+ describe('useConsoleReferenceLink', () => {
14
+ const url = 'https://github.com/octo/repo/pull/7';
15
+
16
+ it('returns null then the resolved state', async () => {
17
+ const cache = new ResourceCache<ConsoleIssueState>(async () =>
18
+ buildState('Decorate links'),
19
+ );
20
+ const { result } = renderHook(() => useConsoleReferenceLink(cache, url));
21
+ expect(result.current).toBeNull();
22
+ await waitFor(() => {
23
+ expect(result.current?.title).toBe('Decorate links');
24
+ });
25
+ });
26
+
27
+ it('returns null when the fetch fails', async () => {
28
+ const cache = new ResourceCache<ConsoleIssueState>(async () => {
29
+ throw new Error('boom');
30
+ });
31
+ const { result } = renderHook(() => useConsoleReferenceLink(cache, url));
32
+ await waitFor(() => {
33
+ expect(cache.has(url)).toBe(false);
34
+ });
35
+ expect(result.current).toBeNull();
36
+ });
37
+
38
+ it('reuses an already cached value without refetching', () => {
39
+ const fetcher = jest.fn(async () => buildState('cached'));
40
+ const cache = new ResourceCache<ConsoleIssueState>(fetcher);
41
+ cache.load(url, url);
42
+ return cache.load(url, url).then(() => {
43
+ const { result } = renderHook(() => useConsoleReferenceLink(cache, url));
44
+ expect(result.current?.title).toBe('cached');
45
+ expect(fetcher).toHaveBeenCalledTimes(1);
46
+ });
47
+ });
48
+ });
@@ -0,0 +1,39 @@
1
+ import { useEffect, useState } from 'react';
2
+ import type { ResourceCache } from '../lib/resourceCache';
3
+ import type { ConsoleIssueState } from '../logic/types';
4
+
5
+ export const useConsoleReferenceLink = (
6
+ cache: ResourceCache<ConsoleIssueState>,
7
+ url: string,
8
+ ): ConsoleIssueState | null => {
9
+ const [state, setState] = useState<ConsoleIssueState | null>(
10
+ () => cache.peek(url) ?? null,
11
+ );
12
+
13
+ useEffect(() => {
14
+ const cached = cache.peek(url);
15
+ if (cached !== undefined) {
16
+ setState(cached);
17
+ return;
18
+ }
19
+ let cancelled = false;
20
+ setState(null);
21
+ cache
22
+ .load(url, url)
23
+ .then((value) => {
24
+ if (!cancelled) {
25
+ setState(value);
26
+ }
27
+ })
28
+ .catch(() => {
29
+ if (!cancelled) {
30
+ setState(null);
31
+ }
32
+ });
33
+ return () => {
34
+ cancelled = true;
35
+ };
36
+ }, [cache, url]);
37
+
38
+ return state;
39
+ };
@@ -105,11 +105,34 @@ describe('createConsoleApiClient', () => {
105
105
  });
106
106
 
107
107
  it('parses the issue state', async () => {
108
- mockFetchOnce({ state: 'closed', merged: true, isPullRequest: true });
108
+ mockFetchOnce({
109
+ state: 'closed',
110
+ merged: true,
111
+ isPullRequest: true,
112
+ title: 'Decorate PR links',
113
+ });
109
114
  const client = createConsoleApiClient(appendToken);
110
115
  expect(
111
116
  await client.fetchIssueState('https://github.com/o/r/pull/1'),
112
- ).toEqual({ state: 'closed', merged: true, isPullRequest: true });
117
+ ).toEqual({
118
+ state: 'closed',
119
+ merged: true,
120
+ isPullRequest: true,
121
+ title: 'Decorate PR links',
122
+ });
123
+ });
124
+
125
+ it('defaults the title to an empty string when absent', async () => {
126
+ mockFetchOnce({ state: 'open', merged: false, isPullRequest: false });
127
+ const client = createConsoleApiClient(appendToken);
128
+ expect(
129
+ await client.fetchIssueState('https://github.com/o/r/issues/1'),
130
+ ).toEqual({
131
+ state: 'open',
132
+ merged: false,
133
+ isPullRequest: false,
134
+ title: '',
135
+ });
113
136
  });
114
137
 
115
138
  it('parses pull request commits', async () => {
@@ -181,12 +181,13 @@ const parsePullRequestStatus = (payload: unknown): ConsolePullRequestStatus => {
181
181
 
182
182
  const parseState = (payload: unknown): ConsoleIssueState => {
183
183
  if (!isRecord(payload)) {
184
- return { state: 'open', merged: false, isPullRequest: false };
184
+ return { state: 'open', merged: false, isPullRequest: false, title: '' };
185
185
  }
186
186
  return {
187
187
  state: getString(payload.state) || 'open',
188
188
  merged: getBoolean(payload.merged),
189
189
  isPullRequest: getBoolean(payload.isPullRequest),
190
+ title: getString(payload.title),
190
191
  };
191
192
  };
192
193