github-issue-tower-defence-management 1.112.4 → 1.112.6
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 +14 -0
- package/bin/adapter/entry-points/console/ui-dist/assets/index-DKFbU8lr.css +1 -0
- package/bin/adapter/entry-points/console/ui-dist/assets/index-DUygHx5u.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/ui/e2e/consoleScenario.spec.ts +54 -0
- package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +110 -8
- package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMarkdownContent.stories.tsx +5 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.stories.tsx +3 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.tsx +7 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.stories.tsx +17 -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/lib/markdown.test.ts +49 -0
- package/src/adapter/entry-points/console/ui/src/features/console/lib/markdown.ts +23 -2
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.test.tsx +68 -3
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +16 -9
- package/src/adapter/entry-points/console/ui/src/features/console/testing/fixtures.ts +33 -0
- package/src/adapter/entry-points/console/ui/src/index.css +3 -1
- package/src/adapter/entry-points/console/ui-dist/assets/index-DKFbU8lr.css +1 -0
- package/src/adapter/entry-points/console/ui-dist/assets/index-DUygHx5u.js +101 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/bin/adapter/entry-points/console/ui-dist/assets/index-CjYyb4uX.js +0 -101
- package/bin/adapter/entry-points/console/ui-dist/assets/index-D08V4Rj3.css +0 -1
- package/src/adapter/entry-points/console/ui-dist/assets/index-CjYyb4uX.js +0 -101
- package/src/adapter/entry-points/console/ui-dist/assets/index-D08V4Rj3.css +0 -1
|
@@ -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-DUygHx5u.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DKFbU8lr.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -120,3 +120,57 @@ test('renders the Workflow Blocker tab leftmost and shows its detail operations'
|
|
|
120
120
|
page.locator('.console-op-button', { hasText: '+1 day' }),
|
|
121
121
|
).toBeVisible();
|
|
122
122
|
});
|
|
123
|
+
|
|
124
|
+
test('adds an inline review comment on a related pull request diff without hover on a touch viewport', async ({
|
|
125
|
+
browser,
|
|
126
|
+
}) => {
|
|
127
|
+
const touchContext = await browser.newContext({
|
|
128
|
+
viewport: { width: 390, height: 844 },
|
|
129
|
+
hasTouch: true,
|
|
130
|
+
isMobile: true,
|
|
131
|
+
});
|
|
132
|
+
const page = await touchContext.newPage();
|
|
133
|
+
|
|
134
|
+
await page.goto(harness.appRootUrl);
|
|
135
|
+
|
|
136
|
+
await tabByLabel(page, 'Failed Preparation').click();
|
|
137
|
+
await itemRowByText(
|
|
138
|
+
page,
|
|
139
|
+
'Add inline review comments on the related pull request diff',
|
|
140
|
+
).click();
|
|
141
|
+
|
|
142
|
+
const changedFile = page
|
|
143
|
+
.locator('.console-file-row', {
|
|
144
|
+
hasText: 'src/adapter/entry-points/console/ui/src/index.css',
|
|
145
|
+
})
|
|
146
|
+
.first();
|
|
147
|
+
await expect(changedFile).toBeVisible();
|
|
148
|
+
await changedFile.click();
|
|
149
|
+
|
|
150
|
+
const commentButton = page.locator('.console-diff-comment-button').first();
|
|
151
|
+
await expect(commentButton).toBeVisible();
|
|
152
|
+
const opacity = await commentButton.evaluate(
|
|
153
|
+
(element) => window.getComputedStyle(element).opacity,
|
|
154
|
+
);
|
|
155
|
+
expect(Number(opacity)).toBeGreaterThan(0);
|
|
156
|
+
|
|
157
|
+
await commentButton.click();
|
|
158
|
+
await page
|
|
159
|
+
.locator('.console-diff-composer-input')
|
|
160
|
+
.fill('Please verify this opacity change on touch devices.');
|
|
161
|
+
await page.locator('.console-diff-composer-submit').click();
|
|
162
|
+
|
|
163
|
+
await expect(page.locator('.console-diff-composer-posted')).toHaveText(
|
|
164
|
+
'Comment posted.',
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
expect(harness.reviewCommentCalls).toHaveLength(1);
|
|
168
|
+
expect(harness.reviewCommentCalls[0].url).toBe(
|
|
169
|
+
'https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/pull/912',
|
|
170
|
+
);
|
|
171
|
+
expect(harness.reviewCommentCalls[0].body).toBe(
|
|
172
|
+
'Please verify this opacity change on touch devices.',
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
await touchContext.close();
|
|
176
|
+
});
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
IssueRepository,
|
|
10
10
|
PullRequestCommit,
|
|
11
11
|
PullRequestDetail,
|
|
12
|
+
PullRequestFile,
|
|
12
13
|
RelatedPullRequest,
|
|
13
14
|
} from '../../../../../domain/usecases/adapter-interfaces/IssueRepository';
|
|
14
15
|
import type { ConsoleProjectBinding } from '../../consoleOperationApi';
|
|
@@ -18,6 +19,14 @@ import { startWebServer } from '../../webServer';
|
|
|
18
19
|
export const CONSOLE_E2E_PJCODE = 'umino';
|
|
19
20
|
export const CONSOLE_E2E_TOKEN = 'console-e2e-fixture-token-3f9c1a';
|
|
20
21
|
|
|
22
|
+
export type ConsoleE2eReviewCommentCall = {
|
|
23
|
+
url: string;
|
|
24
|
+
path: string;
|
|
25
|
+
line: number;
|
|
26
|
+
side: string;
|
|
27
|
+
body: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
21
30
|
type ConsoleFixtureListItem = {
|
|
22
31
|
number: number;
|
|
23
32
|
title: string;
|
|
@@ -54,6 +63,25 @@ type ConsoleFixtureSnapshot = {
|
|
|
54
63
|
const REPO_NAME_WITH_OWNER =
|
|
55
64
|
'HiromiShikata/npm-cli-github-issue-tower-defence-management';
|
|
56
65
|
|
|
66
|
+
export const CONSOLE_E2E_INLINE_COMMENT_ISSUE_URL = `https://github.com/${REPO_NAME_WITH_OWNER}/issues/911`;
|
|
67
|
+
export const CONSOLE_E2E_INLINE_COMMENT_PR_URL = `https://github.com/${REPO_NAME_WITH_OWNER}/pull/912`;
|
|
68
|
+
|
|
69
|
+
const INLINE_COMMENT_PR_FILE: PullRequestFile = {
|
|
70
|
+
filename: 'src/adapter/entry-points/console/ui/src/index.css',
|
|
71
|
+
status: 'modified',
|
|
72
|
+
additions: 3,
|
|
73
|
+
deletions: 1,
|
|
74
|
+
patch: `@@ -959,7 +959,9 @@
|
|
75
|
+
background: transparent;
|
|
76
|
+
color: #6e7681;
|
|
77
|
+
font-size: 0.875rem;
|
|
78
|
+
cursor: pointer;
|
|
79
|
+
- opacity: 0;
|
|
80
|
+
+ opacity: 0.6;
|
|
81
|
+
+ border-color: #30363d;
|
|
82
|
+
}`,
|
|
83
|
+
};
|
|
84
|
+
|
|
57
85
|
const AWAITING_WORKSPACE_OPTION: ConsoleFixtureFieldOption = {
|
|
58
86
|
id: 'd1c19cce',
|
|
59
87
|
name: 'Awaiting Workspace',
|
|
@@ -198,7 +226,15 @@ export const CONSOLE_E2E_TAB_ITEMS: Record<string, ConsoleFixtureListItem[]> = {
|
|
|
198
226
|
'2026-06-17T05:48:09.000Z',
|
|
199
227
|
),
|
|
200
228
|
],
|
|
201
|
-
'failed-preparation': [
|
|
229
|
+
'failed-preparation': [
|
|
230
|
+
issueItem(
|
|
231
|
+
911,
|
|
232
|
+
'Add inline review comments on the related pull request diff',
|
|
233
|
+
'FPR00911',
|
|
234
|
+
'TDPM Console port',
|
|
235
|
+
'2026-06-18T03:12:00.000Z',
|
|
236
|
+
),
|
|
237
|
+
],
|
|
202
238
|
'todo-by-human': [
|
|
203
239
|
issueItem(
|
|
204
240
|
869,
|
|
@@ -286,7 +322,36 @@ const notImplemented = (method: string): never => {
|
|
|
286
322
|
throw new Error(`console E2E stub does not implement ${method}`);
|
|
287
323
|
};
|
|
288
324
|
|
|
289
|
-
const
|
|
325
|
+
const inlineCommentRelatedPullRequest: RelatedPullRequest = {
|
|
326
|
+
url: CONSOLE_E2E_INLINE_COMMENT_PR_URL,
|
|
327
|
+
branchName: 'feature/911-related-pr-inline-comments',
|
|
328
|
+
createdAt: new Date('2026-06-18T03:30:00.000Z'),
|
|
329
|
+
isDraft: false,
|
|
330
|
+
isConflicted: false,
|
|
331
|
+
isPassedAllCiJob: true,
|
|
332
|
+
isCiStateSuccess: true,
|
|
333
|
+
isResolvedAllReviewComments: true,
|
|
334
|
+
isBranchOutOfDate: false,
|
|
335
|
+
missingRequiredCheckNames: [],
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const inlineCommentPullRequestDetail: PullRequestDetail = {
|
|
339
|
+
title: 'Add inline review comments on the related pull request diff',
|
|
340
|
+
state: 'open',
|
|
341
|
+
merged: false,
|
|
342
|
+
isDraft: false,
|
|
343
|
+
additions: 3,
|
|
344
|
+
deletions: 1,
|
|
345
|
+
changedFiles: 1,
|
|
346
|
+
headRefName: 'feature/911-related-pr-inline-comments',
|
|
347
|
+
baseRefName: 'main',
|
|
348
|
+
author: 'HiromiShikata',
|
|
349
|
+
files: [INLINE_COMMENT_PR_FILE],
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const createStubIssueRepository = (
|
|
353
|
+
reviewCommentCalls: ConsoleE2eReviewCommentCall[],
|
|
354
|
+
): IssueRepository => ({
|
|
290
355
|
getAllIssues: () => notImplemented('getAllIssues'),
|
|
291
356
|
getIssueByUrl: async (url: string): Promise<Issue | null> =>
|
|
292
357
|
buildIssueForUrl(url),
|
|
@@ -306,12 +371,29 @@ const createStubIssueRepository = (): IssueRepository => ({
|
|
|
306
371
|
get: async (issueUrl: string): Promise<Issue | null> =>
|
|
307
372
|
buildIssueForUrl(issueUrl),
|
|
308
373
|
update: () => notImplemented('update'),
|
|
309
|
-
findRelatedOpenPRs: async (): Promise<RelatedPullRequest[]> =>
|
|
374
|
+
findRelatedOpenPRs: async (url: string): Promise<RelatedPullRequest[]> =>
|
|
375
|
+
url === CONSOLE_E2E_INLINE_COMMENT_ISSUE_URL
|
|
376
|
+
? [inlineCommentRelatedPullRequest]
|
|
377
|
+
: [],
|
|
310
378
|
getOpenPullRequest: async (): Promise<RelatedPullRequest | null> => null,
|
|
311
379
|
getPullRequestChangedFilePaths: async (): Promise<string[]> => [],
|
|
312
380
|
approvePullRequest: async (): Promise<void> => undefined,
|
|
313
381
|
requestChangesWithInlineComment: async (): Promise<void> => undefined,
|
|
314
|
-
createPullRequestReviewComment: async (
|
|
382
|
+
createPullRequestReviewComment: async (
|
|
383
|
+
prUrl: string,
|
|
384
|
+
filePath: string,
|
|
385
|
+
line: number,
|
|
386
|
+
side: string,
|
|
387
|
+
commentBody: string,
|
|
388
|
+
): Promise<void> => {
|
|
389
|
+
reviewCommentCalls.push({
|
|
390
|
+
url: prUrl,
|
|
391
|
+
path: filePath,
|
|
392
|
+
line,
|
|
393
|
+
side,
|
|
394
|
+
body: commentBody,
|
|
395
|
+
});
|
|
396
|
+
},
|
|
315
397
|
closePullRequest: async (): Promise<void> => undefined,
|
|
316
398
|
closeIssueByUrl: async (): Promise<void> => undefined,
|
|
317
399
|
deletePullRequestBranch: () => notImplemented('deletePullRequestBranch'),
|
|
@@ -323,7 +405,12 @@ const createStubIssueRepository = (): IssueRepository => ({
|
|
|
323
405
|
getIssueOrPullRequestBody: async (): Promise<string> =>
|
|
324
406
|
'## Console E2E fixture\n\nThis body is served by the isolated E2E stub.',
|
|
325
407
|
getIssueOrPullRequestComments: async (): Promise<IssueComment[]> => [],
|
|
326
|
-
getPullRequestDetail: async (
|
|
408
|
+
getPullRequestDetail: async (
|
|
409
|
+
url: string,
|
|
410
|
+
): Promise<PullRequestDetail | null> =>
|
|
411
|
+
url === CONSOLE_E2E_INLINE_COMMENT_PR_URL
|
|
412
|
+
? inlineCommentPullRequestDetail
|
|
413
|
+
: null,
|
|
327
414
|
getPullRequestCommits: async (): Promise<PullRequestCommit[]> => [],
|
|
328
415
|
getIssueOrPullRequestState: async (
|
|
329
416
|
url: string,
|
|
@@ -332,13 +419,24 @@ const createStubIssueRepository = (): IssueRepository => ({
|
|
|
332
419
|
merged: false,
|
|
333
420
|
isPullRequest: url.includes('/pull/'),
|
|
334
421
|
}),
|
|
335
|
-
getPullRequestSummary: async (
|
|
422
|
+
getPullRequestSummary: async (
|
|
423
|
+
url: string,
|
|
424
|
+
): Promise<{
|
|
336
425
|
title: string;
|
|
337
426
|
body: string;
|
|
338
427
|
additions: number;
|
|
339
428
|
deletions: number;
|
|
340
429
|
changedFiles: number;
|
|
341
|
-
} | null> =>
|
|
430
|
+
} | null> =>
|
|
431
|
+
url === CONSOLE_E2E_INLINE_COMMENT_PR_URL
|
|
432
|
+
? {
|
|
433
|
+
title: 'Add inline review comments on the related pull request diff',
|
|
434
|
+
body: 'Wires the add-comment handler on the related pull request diff path.',
|
|
435
|
+
additions: 3,
|
|
436
|
+
deletions: 1,
|
|
437
|
+
changedFiles: 1,
|
|
438
|
+
}
|
|
439
|
+
: null,
|
|
342
440
|
});
|
|
343
441
|
|
|
344
442
|
export type ConsoleE2eHarness = {
|
|
@@ -346,6 +444,7 @@ export type ConsoleE2eHarness = {
|
|
|
346
444
|
appUrl: string;
|
|
347
445
|
appRootUrl: string;
|
|
348
446
|
consoleDataOutputDir: string;
|
|
447
|
+
reviewCommentCalls: ConsoleE2eReviewCommentCall[];
|
|
349
448
|
stop: () => Promise<void>;
|
|
350
449
|
};
|
|
351
450
|
|
|
@@ -362,11 +461,13 @@ export const startConsoleE2eHarness = async (): Promise<ConsoleE2eHarness> => {
|
|
|
362
461
|
): Promise<ConsoleProjectBinding | null> =>
|
|
363
462
|
pjcode === CONSOLE_E2E_PJCODE ? { pjcode, project } : null;
|
|
364
463
|
|
|
464
|
+
const reviewCommentCalls: ConsoleE2eReviewCommentCall[] = [];
|
|
465
|
+
|
|
365
466
|
const server = await startWebServer({
|
|
366
467
|
accessToken: CONSOLE_E2E_TOKEN,
|
|
367
468
|
uiDistDir,
|
|
368
469
|
consoleDataOutputDir,
|
|
369
|
-
issueRepository: createStubIssueRepository(),
|
|
470
|
+
issueRepository: createStubIssueRepository(reviewCommentCalls),
|
|
370
471
|
resolveProject,
|
|
371
472
|
issueTitleStateCache: new IssueTitleStateCache(),
|
|
372
473
|
inTmuxDataDir: null,
|
|
@@ -391,6 +492,7 @@ export const startConsoleE2eHarness = async (): Promise<ConsoleE2eHarness> => {
|
|
|
391
492
|
appUrl,
|
|
392
493
|
appRootUrl,
|
|
393
494
|
consoleDataOutputDir,
|
|
495
|
+
reviewCommentCalls,
|
|
394
496
|
stop: async (): Promise<void> => {
|
|
395
497
|
await closeServer(server);
|
|
396
498
|
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
2
|
import { buildImageProxyUrl } from '../../lib/imageProxy';
|
|
3
3
|
import {
|
|
4
|
+
consoleCodeFenceBodyFixture,
|
|
4
5
|
consoleMarkdownBodyFixture,
|
|
5
6
|
consoleMarkdownImageBodyFixture,
|
|
6
7
|
consoleMermaidBodyFixture,
|
|
@@ -24,6 +25,10 @@ export const WithMermaidFence: Story = {
|
|
|
24
25
|
args: { body: consoleMermaidBodyFixture },
|
|
25
26
|
};
|
|
26
27
|
|
|
28
|
+
export const WithCodeFence: Story = {
|
|
29
|
+
args: { body: consoleCodeFenceBodyFixture },
|
|
30
|
+
};
|
|
31
|
+
|
|
27
32
|
export const WithProxiedGitHubImages: Story = {
|
|
28
33
|
args: {
|
|
29
34
|
body: consoleMarkdownImageBodyFixture,
|
|
@@ -148,5 +148,8 @@ export const IssueWithLinkedPullRequest: Story = {
|
|
|
148
148
|
commitsError: null,
|
|
149
149
|
},
|
|
150
150
|
],
|
|
151
|
+
buildAddInlineComment: (prUrl) => async (path, line, side, body) => {
|
|
152
|
+
window.alert(`comment on ${prUrl} ${path}:${line} (${side})\n${body}`);
|
|
153
|
+
},
|
|
151
154
|
},
|
|
152
155
|
};
|
|
@@ -59,6 +59,7 @@ export type ConsoleItemDetailProps = {
|
|
|
59
59
|
operationBar: ReactNode;
|
|
60
60
|
buildImageProxyUrl?: ImageProxyUrlBuilder;
|
|
61
61
|
onAddInlineComment?: ConsoleAddInlineComment;
|
|
62
|
+
buildAddInlineComment?: (prUrl: string) => ConsoleAddInlineComment;
|
|
62
63
|
};
|
|
63
64
|
|
|
64
65
|
export const ConsoleItemDetail = ({
|
|
@@ -85,6 +86,7 @@ export const ConsoleItemDetail = ({
|
|
|
85
86
|
operationBar,
|
|
86
87
|
buildImageProxyUrl,
|
|
87
88
|
onAddInlineComment,
|
|
89
|
+
buildAddInlineComment,
|
|
88
90
|
}: ConsoleItemDetailProps) => {
|
|
89
91
|
const resolvedState = state?.state ?? 'open';
|
|
90
92
|
const merged = state?.merged ?? false;
|
|
@@ -257,6 +259,11 @@ export const ConsoleItemDetail = ({
|
|
|
257
259
|
commitsError={related.commitsError}
|
|
258
260
|
now={now}
|
|
259
261
|
buildImageProxyUrl={buildImageProxyUrl}
|
|
262
|
+
onAddInlineComment={
|
|
263
|
+
buildAddInlineComment
|
|
264
|
+
? buildAddInlineComment(related.pullRequest.url)
|
|
265
|
+
: undefined
|
|
266
|
+
}
|
|
260
267
|
/>
|
|
261
268
|
))}
|
|
262
269
|
|
|
@@ -29,3 +29,20 @@ export const WithDetail: Story = {
|
|
|
29
29
|
commitsError: null,
|
|
30
30
|
},
|
|
31
31
|
};
|
|
32
|
+
|
|
33
|
+
export const WithInlineComments: Story = {
|
|
34
|
+
args: {
|
|
35
|
+
pullRequest: consoleRelatedPullRequestsFixture[0],
|
|
36
|
+
body: consoleRelatedPullRequestsFixture[0].summary?.body ?? '',
|
|
37
|
+
bodyIsLoading: false,
|
|
38
|
+
files: consoleChangedFilesFixture,
|
|
39
|
+
filesAreLoading: false,
|
|
40
|
+
filesError: null,
|
|
41
|
+
commits: consoleCommitsFixture,
|
|
42
|
+
commitsAreLoading: false,
|
|
43
|
+
commitsError: null,
|
|
44
|
+
onAddInlineComment: async (path, line, side, body) => {
|
|
45
|
+
window.alert(`comment on ${path}:${line} (${side})\n${body}`);
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
};
|
|
@@ -9,6 +9,7 @@ import { ConsolePanel } from '../layout/ConsolePanel';
|
|
|
9
9
|
import { ConsoleChangedFileList } from './ConsoleChangedFileList';
|
|
10
10
|
import { ConsoleCommitList } from './ConsoleCommitList';
|
|
11
11
|
import { ConsoleCopyUrlButton } from './ConsoleCopyUrlButton';
|
|
12
|
+
import type { ConsoleAddInlineComment } from './ConsoleFileDiff';
|
|
12
13
|
|
|
13
14
|
export type ConsolePullRequestSectionProps = {
|
|
14
15
|
pullRequest: ConsoleRelatedPullRequest;
|
|
@@ -22,6 +23,7 @@ export type ConsolePullRequestSectionProps = {
|
|
|
22
23
|
commitsError: string | null;
|
|
23
24
|
now: number;
|
|
24
25
|
buildImageProxyUrl?: ImageProxyUrlBuilder;
|
|
26
|
+
onAddInlineComment?: ConsoleAddInlineComment;
|
|
25
27
|
};
|
|
26
28
|
|
|
27
29
|
export const ConsolePullRequestDetail = ({
|
|
@@ -36,6 +38,7 @@ export const ConsolePullRequestDetail = ({
|
|
|
36
38
|
commitsError,
|
|
37
39
|
now,
|
|
38
40
|
buildImageProxyUrl,
|
|
41
|
+
onAddInlineComment,
|
|
39
42
|
}: ConsolePullRequestSectionProps) => {
|
|
40
43
|
const summary = pullRequest.summary;
|
|
41
44
|
const filesCount =
|
|
@@ -87,6 +90,7 @@ export const ConsolePullRequestDetail = ({
|
|
|
87
90
|
files={files}
|
|
88
91
|
isLoading={filesAreLoading}
|
|
89
92
|
error={filesError}
|
|
93
|
+
onAddInlineComment={onAddInlineComment}
|
|
90
94
|
/>
|
|
91
95
|
</ConsolePanel>
|
|
92
96
|
<ConsolePanel title="Commits" count={commitsCount} defaultCollapsed>
|
|
@@ -82,6 +82,55 @@ describe('splitMarkdownSegments', () => {
|
|
|
82
82
|
{ kind: 'markdown', source: '```mermaid\ngraph TD; A-->B;' },
|
|
83
83
|
]);
|
|
84
84
|
});
|
|
85
|
+
|
|
86
|
+
it('keeps a regular fenced code block as a single markdown segment', () => {
|
|
87
|
+
const segments = splitMarkdownSegments('```ts\nconst answer = 42;\n```');
|
|
88
|
+
expect(segments.map(stripKey)).toEqual([
|
|
89
|
+
{ kind: 'markdown', source: '```ts\nconst answer = 42;\n```' },
|
|
90
|
+
]);
|
|
91
|
+
const html = renderMarkdownToSafeHtml(
|
|
92
|
+
(segments[0] as { source: string }).source,
|
|
93
|
+
);
|
|
94
|
+
expect(html).toContain('<pre>');
|
|
95
|
+
expect(html).toContain('<code');
|
|
96
|
+
expect(html).toContain('const answer = 42;');
|
|
97
|
+
expect(html).not.toContain('```');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('does not treat a mermaid fence inside a regular code block as a diagram', () => {
|
|
101
|
+
const segments = splitMarkdownSegments(
|
|
102
|
+
'```text\n```mermaid\ngraph TD; A-->B;\n```\n```',
|
|
103
|
+
);
|
|
104
|
+
expect(segments.map(stripKey)).toEqual([
|
|
105
|
+
{
|
|
106
|
+
kind: 'markdown',
|
|
107
|
+
source: '```text\n```mermaid\ngraph TD; A-->B;\n```\n```',
|
|
108
|
+
},
|
|
109
|
+
]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('separates a mermaid fence from a following regular code fence', () => {
|
|
113
|
+
const segments = splitMarkdownSegments(
|
|
114
|
+
'intro\n\n```mermaid\ngraph TD; A-->B;\n```\n\n```ts\nconst answer = 42;\n```',
|
|
115
|
+
);
|
|
116
|
+
expect(segments.map(stripKey)).toEqual([
|
|
117
|
+
{ kind: 'markdown', source: 'intro\n' },
|
|
118
|
+
{ kind: 'mermaid', code: 'graph TD; A-->B;' },
|
|
119
|
+
{ kind: 'markdown', source: '\n```ts\nconst answer = 42;\n```' },
|
|
120
|
+
]);
|
|
121
|
+
const codeSegment = segments[2] as { source: string };
|
|
122
|
+
const html = renderMarkdownToSafeHtml(codeSegment.source);
|
|
123
|
+
expect(html).toContain('<pre>');
|
|
124
|
+
expect(html).toContain('<code');
|
|
125
|
+
expect(html).not.toContain('```');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('keeps an unterminated regular fence as markdown', () => {
|
|
129
|
+
const segments = splitMarkdownSegments('```ts\nconst answer = 42;');
|
|
130
|
+
expect(segments.map(stripKey)).toEqual([
|
|
131
|
+
{ kind: 'markdown', source: '```ts\nconst answer = 42;' },
|
|
132
|
+
]);
|
|
133
|
+
});
|
|
85
134
|
});
|
|
86
135
|
|
|
87
136
|
describe('hasMermaidFence', () => {
|
|
@@ -27,6 +27,14 @@ export type ConsoleMarkdownSegment =
|
|
|
27
27
|
|
|
28
28
|
const MERMAID_FENCE = /^```mermaid[^\n]*\n([\s\S]*?)\n```$/;
|
|
29
29
|
|
|
30
|
+
const isMermaidFenceOpen = (line: string): boolean =>
|
|
31
|
+
/^```mermaid\s*$/.test(line.trim());
|
|
32
|
+
|
|
33
|
+
const isCodeFenceOpen = (line: string): boolean =>
|
|
34
|
+
/^```/.test(line.trim()) && !isMermaidFenceOpen(line);
|
|
35
|
+
|
|
36
|
+
const isFenceClose = (line: string): boolean => line.trim() === '```';
|
|
37
|
+
|
|
30
38
|
export const splitMarkdownSegments = (
|
|
31
39
|
source: string,
|
|
32
40
|
): ConsoleMarkdownSegment[] => {
|
|
@@ -34,6 +42,7 @@ export const splitMarkdownSegments = (
|
|
|
34
42
|
const segments: ConsoleMarkdownSegment[] = [];
|
|
35
43
|
let markdownBuffer: string[] = [];
|
|
36
44
|
let mermaidBuffer: string[] | null = null;
|
|
45
|
+
let insideCodeFence = false;
|
|
37
46
|
let sequence = 0;
|
|
38
47
|
|
|
39
48
|
const flushMarkdown = (): void => {
|
|
@@ -49,13 +58,20 @@ export const splitMarkdownSegments = (
|
|
|
49
58
|
};
|
|
50
59
|
|
|
51
60
|
for (const line of lines) {
|
|
52
|
-
if (
|
|
61
|
+
if (insideCodeFence) {
|
|
62
|
+
markdownBuffer.push(line);
|
|
63
|
+
if (isFenceClose(line)) {
|
|
64
|
+
insideCodeFence = false;
|
|
65
|
+
}
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (mermaidBuffer === null && isMermaidFenceOpen(line)) {
|
|
53
69
|
flushMarkdown();
|
|
54
70
|
mermaidBuffer = [];
|
|
55
71
|
continue;
|
|
56
72
|
}
|
|
57
73
|
if (mermaidBuffer !== null) {
|
|
58
|
-
if (line
|
|
74
|
+
if (isFenceClose(line)) {
|
|
59
75
|
segments.push({
|
|
60
76
|
kind: 'mermaid',
|
|
61
77
|
key: `mermaid:${sequence}`,
|
|
@@ -68,6 +84,11 @@ export const splitMarkdownSegments = (
|
|
|
68
84
|
mermaidBuffer.push(line);
|
|
69
85
|
continue;
|
|
70
86
|
}
|
|
87
|
+
if (isCodeFenceOpen(line)) {
|
|
88
|
+
markdownBuffer.push(line);
|
|
89
|
+
insideCodeFence = true;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
71
92
|
markdownBuffer.push(line);
|
|
72
93
|
}
|
|
73
94
|
|
|
@@ -2,8 +2,14 @@ import { fireEvent, render, waitFor } from '@testing-library/react';
|
|
|
2
2
|
import type { ConsoleCaches } from '../hooks/useConsoleCaches';
|
|
3
3
|
import type { ConsoleOperationsApi } from '../hooks/useConsoleOperations';
|
|
4
4
|
import { ResourceCache } from '../lib/resourceCache';
|
|
5
|
+
import type {
|
|
6
|
+
ConsoleChangedFile,
|
|
7
|
+
ConsoleRelatedPullRequest,
|
|
8
|
+
} from '../logic/types';
|
|
5
9
|
import {
|
|
10
|
+
consoleChangedFilesFixture,
|
|
6
11
|
consoleListItemsFixture,
|
|
12
|
+
consoleRelatedPullRequestsFixture,
|
|
7
13
|
consoleStatusOptionsFixture,
|
|
8
14
|
consoleStoryColorsFixture,
|
|
9
15
|
consoleStoryOptionsFixture,
|
|
@@ -15,14 +21,20 @@ jest.mock('../lib/mermaidLoader', () => ({
|
|
|
15
21
|
}));
|
|
16
22
|
|
|
17
23
|
const prItem = consoleListItemsFixture[0];
|
|
24
|
+
const issueItem = consoleListItemsFixture[2];
|
|
18
25
|
|
|
19
|
-
|
|
26
|
+
type CachesOverrides = {
|
|
27
|
+
relatedPrs?: ConsoleRelatedPullRequest[];
|
|
28
|
+
prFiles?: ConsoleChangedFile[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const buildCaches = (overrides: CachesOverrides = {}): ConsoleCaches => {
|
|
20
32
|
const client = {
|
|
21
33
|
fetchItemBody: async () => '# body',
|
|
22
34
|
fetchComments: async () => [],
|
|
23
|
-
fetchPrFiles: async () => [],
|
|
35
|
+
fetchPrFiles: async () => overrides.prFiles ?? [],
|
|
24
36
|
fetchPrCommits: async () => [],
|
|
25
|
-
fetchRelatedPrs: async () => [],
|
|
37
|
+
fetchRelatedPrs: async () => overrides.relatedPrs ?? [],
|
|
26
38
|
fetchIssueState: async () => ({
|
|
27
39
|
state: 'open',
|
|
28
40
|
merged: false,
|
|
@@ -90,4 +102,57 @@ describe('ConsoleItemDetailContainer', () => {
|
|
|
90
102
|
'approve',
|
|
91
103
|
);
|
|
92
104
|
});
|
|
105
|
+
|
|
106
|
+
it('routes an inline review comment on an issue related pull request to that pull request url', async () => {
|
|
107
|
+
const operations = buildOperations();
|
|
108
|
+
const relatedPullRequest = consoleRelatedPullRequestsFixture[0];
|
|
109
|
+
const { container, findByRole, getAllByRole, getByPlaceholderText } =
|
|
110
|
+
render(
|
|
111
|
+
<ConsoleItemDetailContainer
|
|
112
|
+
tab="unread"
|
|
113
|
+
item={issueItem}
|
|
114
|
+
caches={buildCaches({
|
|
115
|
+
relatedPrs: [relatedPullRequest],
|
|
116
|
+
prFiles: consoleChangedFilesFixture,
|
|
117
|
+
})}
|
|
118
|
+
operations={operations}
|
|
119
|
+
statusOptions={consoleStatusOptionsFixture}
|
|
120
|
+
storyOptions={consoleStoryOptionsFixture}
|
|
121
|
+
storyColors={consoleStoryColorsFixture}
|
|
122
|
+
storyName="TDPM Console port"
|
|
123
|
+
overlayStatus={null}
|
|
124
|
+
now={Date.parse('2026-06-19T12:00:00.000Z')}
|
|
125
|
+
onQueueAction={jest.fn()}
|
|
126
|
+
/>,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const fileRow = await findByRole('button', {
|
|
130
|
+
name: new RegExp(consoleChangedFilesFixture[0].path),
|
|
131
|
+
});
|
|
132
|
+
fireEvent.click(fileRow);
|
|
133
|
+
|
|
134
|
+
const commentButton = getAllByRole('button', {
|
|
135
|
+
name: /^Comment on line/,
|
|
136
|
+
})[0];
|
|
137
|
+
fireEvent.click(commentButton);
|
|
138
|
+
|
|
139
|
+
fireEvent.change(
|
|
140
|
+
getByPlaceholderText('Leave a review comment on this line…'),
|
|
141
|
+
{ target: { value: 'Please rename this variable.' } },
|
|
142
|
+
);
|
|
143
|
+
const submitButton = container.querySelector(
|
|
144
|
+
'.console-diff-composer-submit',
|
|
145
|
+
);
|
|
146
|
+
expect(submitButton).not.toBeNull();
|
|
147
|
+
fireEvent.click(submitButton as Element);
|
|
148
|
+
|
|
149
|
+
const addInlineReviewComment =
|
|
150
|
+
operations.addInlineReviewComment as jest.Mock;
|
|
151
|
+
await waitFor(() => {
|
|
152
|
+
expect(addInlineReviewComment).toHaveBeenCalledTimes(1);
|
|
153
|
+
});
|
|
154
|
+
const call = addInlineReviewComment.mock.calls[0];
|
|
155
|
+
expect(call[0]).toBe(relatedPullRequest.url);
|
|
156
|
+
expect(call[4]).toBe('Please rename this variable.');
|
|
157
|
+
});
|
|
93
158
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback } from 'react';
|
|
1
|
+
import { useCallback, useMemo } from 'react';
|
|
2
2
|
import { ConsoleCommentComposer } from '../components/detail/ConsoleCommentComposer';
|
|
3
3
|
import { ConsoleItemDetail } from '../components/detail/ConsoleItemDetail';
|
|
4
4
|
import { ConsoleOperationMenu } from '../components/operations/ConsoleOperationMenu';
|
|
@@ -60,14 +60,20 @@ export const ConsoleItemDetailContainer = ({
|
|
|
60
60
|
[token],
|
|
61
61
|
);
|
|
62
62
|
const hasPullRequest = item.isPr || detail.relatedPullRequests.length > 0;
|
|
63
|
-
const
|
|
64
|
-
(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
const buildAddInlineComment = useCallback(
|
|
64
|
+
(prUrl: string) =>
|
|
65
|
+
(
|
|
66
|
+
path: string,
|
|
67
|
+
line: number,
|
|
68
|
+
side: ConsoleReviewCommentSide,
|
|
69
|
+
body: string,
|
|
70
|
+
) =>
|
|
71
|
+
operations.addInlineReviewComment(prUrl, path, line, side, body),
|
|
72
|
+
[operations],
|
|
73
|
+
);
|
|
74
|
+
const addInlineComment = useMemo(
|
|
75
|
+
() => buildAddInlineComment(item.url),
|
|
76
|
+
[buildAddInlineComment, item.url],
|
|
71
77
|
);
|
|
72
78
|
|
|
73
79
|
const handlers: ConsoleOperationHandlers = {
|
|
@@ -148,6 +154,7 @@ export const ConsoleItemDetailContainer = ({
|
|
|
148
154
|
now={now}
|
|
149
155
|
buildImageProxyUrl={resolveImageProxyUrl}
|
|
150
156
|
onAddInlineComment={item.isPr ? addInlineComment : undefined}
|
|
157
|
+
buildAddInlineComment={buildAddInlineComment}
|
|
151
158
|
commentComposer={
|
|
152
159
|
<ConsoleCommentComposer
|
|
153
160
|
isPr={item.isPr}
|
|
@@ -166,6 +166,39 @@ For reference, the avatar comes from an external host:
|
|
|
166
166
|

|
|
167
167
|
`;
|
|
168
168
|
|
|
169
|
+
export const consoleCodeFenceBodyFixture = `## Reproduction
|
|
170
|
+
|
|
171
|
+
A TypeScript fenced code block MUST render as a styled code box rather than
|
|
172
|
+
literal backticks:
|
|
173
|
+
|
|
174
|
+
\`\`\`ts
|
|
175
|
+
export const splitMarkdownSegments = (source: string): ConsoleMarkdownSegment[] => {
|
|
176
|
+
const lines = source.split('\\n');
|
|
177
|
+
return lines.length > 0 ? parse(lines) : [];
|
|
178
|
+
};
|
|
179
|
+
\`\`\`
|
|
180
|
+
|
|
181
|
+
A code block that documents a mermaid fence MUST stay literal text and MUST NOT
|
|
182
|
+
be turned into a diagram:
|
|
183
|
+
|
|
184
|
+
\`\`\`text
|
|
185
|
+
\`\`\`mermaid
|
|
186
|
+
graph TD; A-->B;
|
|
187
|
+
\`\`\`
|
|
188
|
+
|
|
189
|
+
The diagram below MUST still render as an actual diagram:
|
|
190
|
+
|
|
191
|
+
\`\`\`mermaid
|
|
192
|
+
sequenceDiagram
|
|
193
|
+
participant Browser
|
|
194
|
+
participant ConsoleServer
|
|
195
|
+
Browser->>ConsoleServer: GET /api/itembody?url=...
|
|
196
|
+
ConsoleServer-->>Browser: { body }
|
|
197
|
+
\`\`\`
|
|
198
|
+
|
|
199
|
+
Inline \`code\` spans are unaffected.
|
|
200
|
+
`;
|
|
201
|
+
|
|
169
202
|
export const consoleMermaidBodyFixture = `Here is the request flow:
|
|
170
203
|
|
|
171
204
|
\`\`\`mermaid
|