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.
- package/CHANGELOG.md +7 -0
- package/README.md +1 -1
- package/bin/adapter/entry-points/console/consoleReadApi.js +11 -1
- package/bin/adapter/entry-points/console/consoleReadApi.js.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/{index-CEJmPNRK.css → index-Blb_xqIx.css} +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-DQyZ8KpG.js +101 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapter/entry-points/console/consoleReadApi.test.ts +82 -0
- package/src/adapter/entry-points/console/consoleReadApi.ts +21 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMarkdownContent.stories.tsx +16 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMarkdownContent.test.tsx +61 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMarkdownContent.tsx +59 -5
- package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleReferenceLink.stories.tsx +76 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleReferenceLink.test.tsx +56 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleReferenceLink.tsx +39 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentList.tsx +4 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.stories.tsx +24 -4
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.test.tsx +13 -3
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.tsx +6 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.tsx +4 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleItemDetailData.test.ts +1 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleItemDetailData.ts +1 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleReferenceLink.test.ts +48 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleReferenceLink.ts +39 -0
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +25 -2
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +2 -1
- package/src/adapter/entry-points/console/ui/src/features/console/logic/references.test.ts +80 -0
- package/src/adapter/entry-points/console/ui/src/features/console/logic/references.ts +44 -0
- package/src/adapter/entry-points/console/ui/src/features/console/logic/types.ts +1 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.test.tsx +1 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +12 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleReferenceLinkContainer.tsx +25 -0
- package/src/adapter/entry-points/console/ui/src/features/console/testing/fixtures.ts +40 -0
- package/src/adapter/entry-points/console/ui/src/index.css +23 -0
- package/src/adapter/entry-points/console/ui-dist/assets/{index-CEJmPNRK.css → index-Blb_xqIx.css} +1 -1
- package/src/adapter/entry-points/console/ui-dist/assets/index-DQyZ8KpG.js +101 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/types/adapter/entry-points/console/consoleReadApi.d.ts +1 -0
- package/types/adapter/entry-points/console/consoleReadApi.d.ts.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-DfNrA5uV.js +0 -101
- 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-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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,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
|
|
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
|
|
38
|
-
|
|
72
|
+
if (container === null) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
container.innerHTML = html;
|
|
76
|
+
if (renderReferenceLink === undefined) {
|
|
77
|
+
setReferenceMounts([]);
|
|
78
|
+
return;
|
|
39
79
|
}
|
|
40
|
-
|
|
80
|
+
setReferenceMounts(collectReferenceMounts(container));
|
|
81
|
+
}, [html, renderReferenceLink]);
|
|
41
82
|
|
|
42
|
-
return
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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={{
|
|
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={{
|
|
172
|
+
state={{
|
|
173
|
+
state: 'open',
|
|
174
|
+
merged: false,
|
|
175
|
+
isPullRequest: false,
|
|
176
|
+
title: '',
|
|
177
|
+
}}
|
|
168
178
|
pullRequestStatus={null}
|
|
169
179
|
/>,
|
|
170
180
|
);
|