github-issue-tower-defence-management 1.91.3 → 1.91.4
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/bin/adapter/entry-points/console/consoleOperationApi.js +28 -1
- package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleServer.js +2 -0
- package/bin/adapter/entry-points/console/consoleServer.js.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/{index-BU6p3cGU.css → index-0IuY3q4G.css} +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-D04N09aG.js +100 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapter/entry-points/console/consoleOperationApi.test.ts +72 -0
- package/src/adapter/entry-points/console/consoleOperationApi.ts +32 -0
- package/src/adapter/entry-points/console/consoleServer.test.ts +49 -0
- package/src/adapter/entry-points/console/consoleServer.ts +3 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentComposer.stories.tsx +30 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentComposer.test.tsx +81 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentComposer.tsx +118 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.stories.tsx +1 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.test.tsx +20 -3
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.tsx +31 -5
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsolePanel.stories.tsx +25 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsolePanel.test.tsx +27 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsolePanel.tsx +4 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.stories.tsx +18 -14
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.test.tsx +40 -13
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.tsx +67 -29
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemList.stories.tsx +1 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemList.test.tsx +7 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemList.tsx +4 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemSummary.stories.tsx +1 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemSummary.test.tsx +43 -6
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemSummary.tsx +26 -3
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleNextActionDateActions.tsx +2 -2
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsolePullRequestReviewActions.tsx +7 -8
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleNavigation.test.ts +76 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleNavigation.ts +111 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.test.ts +37 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.ts +18 -0
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +34 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.test.tsx +5 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +8 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.test.tsx +26 -9
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +21 -20
- package/src/adapter/entry-points/console/ui/src/index.css +241 -39
- package/src/adapter/entry-points/console/ui-dist/assets/{index-BU6p3cGU.css → index-0IuY3q4G.css} +1 -1
- package/src/adapter/entry-points/console/ui-dist/assets/index-D04N09aG.js +100 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts +1 -0
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleServer.d.ts.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-BvuSQN9s.js +0 -100
- package/src/adapter/entry-points/console/ui-dist/assets/index-Cn4xr5-h.js +0 -100
|
@@ -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-D04N09aG.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-0IuY3q4G.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -7,6 +7,7 @@ import { Project } from '../../../domain/entities/Project';
|
|
|
7
7
|
import { Issue } from '../../../domain/entities/Issue';
|
|
8
8
|
import {
|
|
9
9
|
ConsoleOperationContext,
|
|
10
|
+
handleComment,
|
|
10
11
|
handleIntmux,
|
|
11
12
|
handleReview,
|
|
12
13
|
handleTriage,
|
|
@@ -555,4 +556,75 @@ describe('consoleOperationApi', () => {
|
|
|
555
556
|
expect(response.statusCode).toBe(200);
|
|
556
557
|
});
|
|
557
558
|
});
|
|
559
|
+
|
|
560
|
+
describe('handleComment', () => {
|
|
561
|
+
it('posts a comment and returns the created comment', async () => {
|
|
562
|
+
issueRepository.getIssueOrPullRequestComments.mockResolvedValue([
|
|
563
|
+
{
|
|
564
|
+
author: 'github-actions',
|
|
565
|
+
body: 'All required checks have passed.',
|
|
566
|
+
createdAt: new Date('2026-06-17T07:48:11.000Z'),
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
author: 'HiromiShikata',
|
|
570
|
+
body: 'Please rebase onto the latest main branch.',
|
|
571
|
+
createdAt: new Date('2026-06-17T09:03:27.000Z'),
|
|
572
|
+
},
|
|
573
|
+
]);
|
|
574
|
+
const response = await handleComment(context, {
|
|
575
|
+
pjcode: 'umino',
|
|
576
|
+
url: 'https://github.com/o/r/issues/1',
|
|
577
|
+
body: 'Please rebase onto the latest main branch.',
|
|
578
|
+
});
|
|
579
|
+
expect(response.statusCode).toBe(200);
|
|
580
|
+
expect(issueRepository.createCommentByUrl).toHaveBeenCalledWith(
|
|
581
|
+
'https://github.com/o/r/issues/1',
|
|
582
|
+
'Please rebase onto the latest main branch.',
|
|
583
|
+
);
|
|
584
|
+
expect(response.body).toEqual({
|
|
585
|
+
ok: true,
|
|
586
|
+
comment: {
|
|
587
|
+
author: 'HiromiShikata',
|
|
588
|
+
body: 'Please rebase onto the latest main branch.',
|
|
589
|
+
createdAt: '2026-06-17T09:03:27.000Z',
|
|
590
|
+
},
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it('falls back to the posted body when no comment is returned', async () => {
|
|
595
|
+
issueRepository.getIssueOrPullRequestComments.mockResolvedValue([]);
|
|
596
|
+
const response = await handleComment(context, {
|
|
597
|
+
pjcode: 'umino',
|
|
598
|
+
url: 'https://github.com/o/r/issues/1',
|
|
599
|
+
body: 'A first comment on this issue.',
|
|
600
|
+
});
|
|
601
|
+
expect(response.statusCode).toBe(200);
|
|
602
|
+
expect(response.body).toEqual({
|
|
603
|
+
ok: true,
|
|
604
|
+
comment: {
|
|
605
|
+
author: '',
|
|
606
|
+
body: 'A first comment on this issue.',
|
|
607
|
+
createdAt: '',
|
|
608
|
+
},
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it('rejects when url is missing', async () => {
|
|
613
|
+
const response = await handleComment(context, {
|
|
614
|
+
pjcode: 'umino',
|
|
615
|
+
body: 'A comment without a target url.',
|
|
616
|
+
});
|
|
617
|
+
expect(response.statusCode).toBe(400);
|
|
618
|
+
expect(issueRepository.createCommentByUrl).not.toHaveBeenCalled();
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it('rejects when body is missing', async () => {
|
|
622
|
+
const response = await handleComment(context, {
|
|
623
|
+
pjcode: 'umino',
|
|
624
|
+
url: 'https://github.com/o/r/issues/1',
|
|
625
|
+
});
|
|
626
|
+
expect(response.statusCode).toBe(400);
|
|
627
|
+
expect(issueRepository.createCommentByUrl).not.toHaveBeenCalled();
|
|
628
|
+
});
|
|
629
|
+
});
|
|
558
630
|
});
|
|
@@ -296,6 +296,38 @@ export const handleTriage = async (
|
|
|
296
296
|
return badRequest(`unknown triage action "${action}"`);
|
|
297
297
|
};
|
|
298
298
|
|
|
299
|
+
export const handleComment = async (
|
|
300
|
+
context: ConsoleOperationContext,
|
|
301
|
+
body: Record<string, unknown>,
|
|
302
|
+
): Promise<ConsoleOperationResponse> => {
|
|
303
|
+
const url = body.url;
|
|
304
|
+
const commentBody = body.body;
|
|
305
|
+
if (!isNonEmptyString(url)) {
|
|
306
|
+
return badRequest('url is required');
|
|
307
|
+
}
|
|
308
|
+
if (!isNonEmptyString(commentBody)) {
|
|
309
|
+
return badRequest('body is required');
|
|
310
|
+
}
|
|
311
|
+
await context.issueRepository.createCommentByUrl(url, commentBody);
|
|
312
|
+
const comments =
|
|
313
|
+
await context.issueRepository.getIssueOrPullRequestComments(url);
|
|
314
|
+
const posted = comments[comments.length - 1] ?? null;
|
|
315
|
+
return {
|
|
316
|
+
statusCode: 200,
|
|
317
|
+
body: {
|
|
318
|
+
ok: true,
|
|
319
|
+
comment:
|
|
320
|
+
posted === null
|
|
321
|
+
? { author: '', body: commentBody, createdAt: '' }
|
|
322
|
+
: {
|
|
323
|
+
author: posted.author,
|
|
324
|
+
body: posted.body,
|
|
325
|
+
createdAt: posted.createdAt.toISOString(),
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
};
|
|
330
|
+
|
|
299
331
|
export const handleIntmux = async (
|
|
300
332
|
context: ConsoleOperationContext,
|
|
301
333
|
body: Record<string, unknown>,
|
|
@@ -533,6 +533,55 @@ describe('consoleServer new routes integration', () => {
|
|
|
533
533
|
}
|
|
534
534
|
});
|
|
535
535
|
|
|
536
|
+
it('posts a comment through the comment operation api', async () => {
|
|
537
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
538
|
+
const issueRepository = mock<IssueRepository>();
|
|
539
|
+
issueRepository.getIssueOrPullRequestComments.mockResolvedValue([
|
|
540
|
+
{
|
|
541
|
+
author: 'HiromiShikata',
|
|
542
|
+
body: 'Thanks, this resolves the parity gap.',
|
|
543
|
+
createdAt: new Date('2026-06-18T03:21:00.000Z'),
|
|
544
|
+
},
|
|
545
|
+
]);
|
|
546
|
+
const server = await startConsoleServer({
|
|
547
|
+
accessToken: testToken,
|
|
548
|
+
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
549
|
+
consoleDataOutputDir: null,
|
|
550
|
+
issueRepository,
|
|
551
|
+
resolveProject: async (pjcode) =>
|
|
552
|
+
pjcode === 'umino' ? { pjcode, project: buildProject() } : null,
|
|
553
|
+
port: 0,
|
|
554
|
+
});
|
|
555
|
+
try {
|
|
556
|
+
const response = await request(
|
|
557
|
+
server,
|
|
558
|
+
'POST',
|
|
559
|
+
`/api/comment?k=${testToken}`,
|
|
560
|
+
{
|
|
561
|
+
pjcode: 'umino',
|
|
562
|
+
url: 'https://github.com/o/r/issues/1',
|
|
563
|
+
body: 'Thanks, this resolves the parity gap.',
|
|
564
|
+
},
|
|
565
|
+
);
|
|
566
|
+
expect(response.statusCode).toBe(200);
|
|
567
|
+
expect(issueRepository.createCommentByUrl).toHaveBeenCalledWith(
|
|
568
|
+
'https://github.com/o/r/issues/1',
|
|
569
|
+
'Thanks, this resolves the parity gap.',
|
|
570
|
+
);
|
|
571
|
+
expect(JSON.parse(response.body)).toEqual({
|
|
572
|
+
ok: true,
|
|
573
|
+
comment: {
|
|
574
|
+
author: 'HiromiShikata',
|
|
575
|
+
body: 'Thanks, this resolves the parity gap.',
|
|
576
|
+
createdAt: '2026-06-18T03:21:00.000Z',
|
|
577
|
+
},
|
|
578
|
+
});
|
|
579
|
+
} finally {
|
|
580
|
+
await closeServer(server);
|
|
581
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
|
|
536
585
|
it('runs an operation api and records the done exclusion', async () => {
|
|
537
586
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
538
587
|
const dataDir = path.join(tmpDir, 'data');
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
import {
|
|
20
20
|
ConsoleOperationContext,
|
|
21
21
|
ConsoleProjectResolver,
|
|
22
|
+
handleComment,
|
|
22
23
|
handleIntmux,
|
|
23
24
|
handleReview,
|
|
24
25
|
handleTriage,
|
|
@@ -306,6 +307,8 @@ const handleOperationApi = async (
|
|
|
306
307
|
return handleTriage(context, body);
|
|
307
308
|
case '/api/intmux':
|
|
308
309
|
return handleIntmux(context, body);
|
|
310
|
+
case '/api/comment':
|
|
311
|
+
return handleComment(context, body);
|
|
309
312
|
default:
|
|
310
313
|
return null;
|
|
311
314
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import type { ConsoleComment } from '../../logic/types';
|
|
3
|
+
import { ConsoleCommentComposer } from './ConsoleCommentComposer';
|
|
4
|
+
|
|
5
|
+
const acceptComment = async (body: string): Promise<ConsoleComment> => ({
|
|
6
|
+
author: 'HiromiShikata',
|
|
7
|
+
body,
|
|
8
|
+
createdAt: '2026-06-19T11:58:00.000Z',
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const meta: Meta<typeof ConsoleCommentComposer> = {
|
|
12
|
+
title: 'Console/ConsoleCommentComposer',
|
|
13
|
+
component: ConsoleCommentComposer,
|
|
14
|
+
args: {
|
|
15
|
+
now: Date.parse('2026-06-19T12:00:00.000Z'),
|
|
16
|
+
onSubmit: acceptComment,
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default meta;
|
|
21
|
+
|
|
22
|
+
type Story = StoryObj<typeof ConsoleCommentComposer>;
|
|
23
|
+
|
|
24
|
+
export const IssueComposerOpen: Story = {
|
|
25
|
+
args: { isPr: false },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const PullRequestComposerCollapsed: Story = {
|
|
29
|
+
args: { isPr: true },
|
|
30
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { fireEvent, render, waitFor } from '@testing-library/react';
|
|
2
|
+
import type { ConsoleComment } from '../../logic/types';
|
|
3
|
+
import { ConsoleCommentComposer } from './ConsoleCommentComposer';
|
|
4
|
+
|
|
5
|
+
jest.mock('../../lib/mermaidLoader', () => ({
|
|
6
|
+
renderMermaidToSvg: jest.fn(async () => '<svg></svg>'),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
const now = Date.parse('2026-06-19T12:00:00.000Z');
|
|
10
|
+
|
|
11
|
+
describe('ConsoleCommentComposer', () => {
|
|
12
|
+
it('opens the form by default for an issue item', () => {
|
|
13
|
+
const { getByPlaceholderText } = render(
|
|
14
|
+
<ConsoleCommentComposer
|
|
15
|
+
isPr={false}
|
|
16
|
+
now={now}
|
|
17
|
+
onSubmit={async (body) => ({
|
|
18
|
+
author: 'HiromiShikata',
|
|
19
|
+
body,
|
|
20
|
+
createdAt: '2026-06-19T11:58:00.000Z',
|
|
21
|
+
})}
|
|
22
|
+
/>,
|
|
23
|
+
);
|
|
24
|
+
expect(getByPlaceholderText('Leave a comment…')).toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('keeps the form closed by default for a pull request item', () => {
|
|
28
|
+
const { queryByPlaceholderText, getByText } = render(
|
|
29
|
+
<ConsoleCommentComposer
|
|
30
|
+
isPr
|
|
31
|
+
now={now}
|
|
32
|
+
onSubmit={async (body) => ({
|
|
33
|
+
author: 'HiromiShikata',
|
|
34
|
+
body,
|
|
35
|
+
createdAt: '2026-06-19T11:58:00.000Z',
|
|
36
|
+
})}
|
|
37
|
+
/>,
|
|
38
|
+
);
|
|
39
|
+
expect(queryByPlaceholderText('Leave a comment…')).toBeNull();
|
|
40
|
+
expect(getByText('💬 Add a comment')).toBeInTheDocument();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('submits the comment and shows the posted comment', async () => {
|
|
44
|
+
const onSubmit = jest.fn(
|
|
45
|
+
async (body: string): Promise<ConsoleComment> => ({
|
|
46
|
+
author: 'HiromiShikata',
|
|
47
|
+
body,
|
|
48
|
+
createdAt: '2026-06-19T11:58:00.000Z',
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
const { getByPlaceholderText, getByText } = render(
|
|
52
|
+
<ConsoleCommentComposer isPr={false} now={now} onSubmit={onSubmit} />,
|
|
53
|
+
);
|
|
54
|
+
fireEvent.change(getByPlaceholderText('Leave a comment…'), {
|
|
55
|
+
target: { value: 'Looks good after the rebase.' },
|
|
56
|
+
});
|
|
57
|
+
fireEvent.click(getByText('Comment'));
|
|
58
|
+
await waitFor(() => {
|
|
59
|
+
expect(getByText('Looks good after the rebase.')).toBeInTheDocument();
|
|
60
|
+
});
|
|
61
|
+
expect(onSubmit).toHaveBeenCalledWith('Looks good after the rebase.');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('shows a failure message when the submission rejects', async () => {
|
|
65
|
+
const { getByPlaceholderText, getByText, findByRole } = render(
|
|
66
|
+
<ConsoleCommentComposer
|
|
67
|
+
isPr={false}
|
|
68
|
+
now={now}
|
|
69
|
+
onSubmit={async () => {
|
|
70
|
+
throw new Error('HTTP 500');
|
|
71
|
+
}}
|
|
72
|
+
/>,
|
|
73
|
+
);
|
|
74
|
+
fireEvent.change(getByPlaceholderText('Leave a comment…'), {
|
|
75
|
+
target: { value: 'This should fail to post.' },
|
|
76
|
+
});
|
|
77
|
+
fireEvent.click(getByText('Comment'));
|
|
78
|
+
const alert = await findByRole('alert');
|
|
79
|
+
expect(alert.textContent).toContain('HTTP 500');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
formatFullTimestamp,
|
|
4
|
+
formatRelativeTime,
|
|
5
|
+
} from '../../logic/relativeTime';
|
|
6
|
+
import type { ConsoleComment } from '../../logic/types';
|
|
7
|
+
import { ConsoleMarkdownContent } from '../content/ConsoleMarkdownContent';
|
|
8
|
+
|
|
9
|
+
export type ConsoleCommentComposerProps = {
|
|
10
|
+
isPr: boolean;
|
|
11
|
+
now: number;
|
|
12
|
+
onSubmit: (body: string) => Promise<ConsoleComment>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type ComposerStatus =
|
|
16
|
+
| { kind: 'idle' }
|
|
17
|
+
| { kind: 'posting' }
|
|
18
|
+
| { kind: 'error'; message: string };
|
|
19
|
+
|
|
20
|
+
export const ConsoleCommentComposer = ({
|
|
21
|
+
isPr,
|
|
22
|
+
now,
|
|
23
|
+
onSubmit,
|
|
24
|
+
}: ConsoleCommentComposerProps) => {
|
|
25
|
+
const [open, setOpen] = useState<boolean>(!isPr);
|
|
26
|
+
const [draft, setDraft] = useState<string>('');
|
|
27
|
+
const [status, setStatus] = useState<ComposerStatus>({ kind: 'idle' });
|
|
28
|
+
const [posted, setPosted] = useState<ConsoleComment[]>([]);
|
|
29
|
+
|
|
30
|
+
const submit = async (): Promise<void> => {
|
|
31
|
+
const body = draft.trim();
|
|
32
|
+
if (body.length === 0 || status.kind === 'posting') {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
setStatus({ kind: 'posting' });
|
|
36
|
+
try {
|
|
37
|
+
const comment = await onSubmit(body);
|
|
38
|
+
setPosted((previous) => [...previous, comment]);
|
|
39
|
+
setDraft('');
|
|
40
|
+
setStatus({ kind: 'idle' });
|
|
41
|
+
} catch (error) {
|
|
42
|
+
setStatus({
|
|
43
|
+
kind: 'error',
|
|
44
|
+
message: error instanceof Error ? error.message : 'failed to post',
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className="console-composer">
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
className="console-composer-toggle"
|
|
54
|
+
aria-expanded={open}
|
|
55
|
+
onClick={() => setOpen((value) => !value)}
|
|
56
|
+
>
|
|
57
|
+
{open ? '✕ Cancel' : '💬 Add a comment'}
|
|
58
|
+
</button>
|
|
59
|
+
{posted.length > 0 && (
|
|
60
|
+
<div className="console-composer-posted">
|
|
61
|
+
{posted.map((comment) => (
|
|
62
|
+
<article
|
|
63
|
+
key={`${comment.author}:${comment.createdAt}:${comment.body}`}
|
|
64
|
+
className="console-comment"
|
|
65
|
+
>
|
|
66
|
+
<header
|
|
67
|
+
className="console-comment-header"
|
|
68
|
+
title={formatFullTimestamp(comment.createdAt)}
|
|
69
|
+
>
|
|
70
|
+
<span className="console-comment-author">
|
|
71
|
+
{comment.author === '' ? 'you' : comment.author}
|
|
72
|
+
</span>
|
|
73
|
+
<span className="console-comment-time">
|
|
74
|
+
{formatRelativeTime(comment.createdAt, now)}
|
|
75
|
+
</span>
|
|
76
|
+
</header>
|
|
77
|
+
<ConsoleMarkdownContent body={comment.body} />
|
|
78
|
+
</article>
|
|
79
|
+
))}
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
{open && (
|
|
83
|
+
<div className="console-composer-form">
|
|
84
|
+
<textarea
|
|
85
|
+
className="console-composer-input"
|
|
86
|
+
rows={3}
|
|
87
|
+
placeholder="Leave a comment…"
|
|
88
|
+
value={draft}
|
|
89
|
+
onChange={(event) => setDraft(event.target.value)}
|
|
90
|
+
/>
|
|
91
|
+
<div className="console-composer-row">
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
className="console-composer-submit"
|
|
95
|
+
disabled={status.kind === 'posting'}
|
|
96
|
+
onClick={() => {
|
|
97
|
+
void submit();
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
Comment
|
|
101
|
+
</button>
|
|
102
|
+
{status.kind === 'posting' && (
|
|
103
|
+
<span className="console-composer-status">Posting…</span>
|
|
104
|
+
)}
|
|
105
|
+
{status.kind === 'error' && (
|
|
106
|
+
<span
|
|
107
|
+
role="alert"
|
|
108
|
+
className="console-composer-status console-composer-error"
|
|
109
|
+
>
|
|
110
|
+
Failed: {status.message}
|
|
111
|
+
</span>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
};
|
|
@@ -34,18 +34,35 @@ const baseProps = {
|
|
|
34
34
|
commitsError: null,
|
|
35
35
|
relatedPullRequests: [],
|
|
36
36
|
now,
|
|
37
|
+
commentComposer: <div>comment-composer</div>,
|
|
37
38
|
operationBar: <div>operation-bar</div>,
|
|
38
39
|
};
|
|
39
40
|
|
|
40
41
|
describe('ConsoleItemDetail', () => {
|
|
41
|
-
it('renders the PR title with the PR number, sub bar and
|
|
42
|
+
it('renders the PR title with the PR number, sub bar and counted panels', () => {
|
|
42
43
|
const { getByText, getAllByText } = render(
|
|
43
44
|
<ConsoleItemDetail item={prItem} {...baseProps} />,
|
|
44
45
|
);
|
|
45
46
|
expect(getAllByText(`PR #${prItem.number}`).length).toBeGreaterThan(0);
|
|
46
|
-
expect(
|
|
47
|
-
|
|
47
|
+
expect(
|
|
48
|
+
getByText(`Changed files (${consoleChangedFilesFixture.length})`),
|
|
49
|
+
).toBeInTheDocument();
|
|
50
|
+
expect(
|
|
51
|
+
getByText(`Comments (${consoleCommentsFixture.length})`),
|
|
52
|
+
).toBeInTheDocument();
|
|
53
|
+
expect(
|
|
54
|
+
getByText(`Commits (${consoleCommitsFixture.length})`),
|
|
55
|
+
).toBeInTheDocument();
|
|
48
56
|
expect(getByText('operation-bar')).toBeInTheDocument();
|
|
57
|
+
expect(getByText('comment-composer')).toBeInTheDocument();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('renders the Description open link to the item url', () => {
|
|
61
|
+
const { getByText } = render(
|
|
62
|
+
<ConsoleItemDetail item={prItem} {...baseProps} />,
|
|
63
|
+
);
|
|
64
|
+
const openLink = getByText('open');
|
|
65
|
+
expect(openLink).toHaveAttribute('href', prItem.url);
|
|
49
66
|
});
|
|
50
67
|
|
|
51
68
|
it('renders the story tag and opened relative time', () => {
|
|
@@ -52,6 +52,7 @@ export type ConsoleItemDetailProps = {
|
|
|
52
52
|
commitsError: string | null;
|
|
53
53
|
relatedPullRequests: ConsoleRelatedPullRequestView[];
|
|
54
54
|
now: number;
|
|
55
|
+
commentComposer: ReactNode;
|
|
55
56
|
operationBar: ReactNode;
|
|
56
57
|
};
|
|
57
58
|
|
|
@@ -75,6 +76,7 @@ export const ConsoleItemDetail = ({
|
|
|
75
76
|
commitsError,
|
|
76
77
|
relatedPullRequests,
|
|
77
78
|
now,
|
|
79
|
+
commentComposer,
|
|
78
80
|
operationBar,
|
|
79
81
|
}: ConsoleItemDetailProps) => {
|
|
80
82
|
const resolvedState = state?.state ?? 'open';
|
|
@@ -85,6 +87,12 @@ export const ConsoleItemDetail = ({
|
|
|
85
87
|
const statusPalette = overlayStatus
|
|
86
88
|
? colorFromEnum(overlayStatus.color)
|
|
87
89
|
: null;
|
|
90
|
+
const filesCount =
|
|
91
|
+
filesAreLoading || filesError !== null ? null : files.length;
|
|
92
|
+
const commentsCount =
|
|
93
|
+
commentsAreLoading || commentsError !== null ? null : comments.length;
|
|
94
|
+
const commitsCount =
|
|
95
|
+
commitsAreLoading || commitsError !== null ? null : commits.length;
|
|
88
96
|
|
|
89
97
|
return (
|
|
90
98
|
<article className="console-detail">
|
|
@@ -164,7 +172,19 @@ export const ConsoleItemDetail = ({
|
|
|
164
172
|
opened {formatRelativeTime(item.createdAt, now)}
|
|
165
173
|
</div>
|
|
166
174
|
|
|
167
|
-
<ConsolePanel
|
|
175
|
+
<ConsolePanel
|
|
176
|
+
title="Description"
|
|
177
|
+
headerAction={
|
|
178
|
+
<a
|
|
179
|
+
href={item.url}
|
|
180
|
+
className="console-panel-open-link"
|
|
181
|
+
target="_blank"
|
|
182
|
+
rel="noopener noreferrer"
|
|
183
|
+
>
|
|
184
|
+
open
|
|
185
|
+
</a>
|
|
186
|
+
}
|
|
187
|
+
>
|
|
168
188
|
{bodyError !== null ? (
|
|
169
189
|
<p role="alert" className="console-detail-body-error">
|
|
170
190
|
Failed to load description: {bodyError}
|
|
@@ -177,7 +197,7 @@ export const ConsoleItemDetail = ({
|
|
|
177
197
|
</ConsolePanel>
|
|
178
198
|
|
|
179
199
|
{item.isPr && (
|
|
180
|
-
<ConsolePanel title="Changed files">
|
|
200
|
+
<ConsolePanel title="Changed files" count={filesCount}>
|
|
181
201
|
<ConsoleChangedFileList
|
|
182
202
|
files={files}
|
|
183
203
|
isLoading={filesAreLoading}
|
|
@@ -186,7 +206,11 @@ export const ConsoleItemDetail = ({
|
|
|
186
206
|
</ConsolePanel>
|
|
187
207
|
)}
|
|
188
208
|
|
|
189
|
-
<ConsolePanel
|
|
209
|
+
<ConsolePanel
|
|
210
|
+
title="Comments"
|
|
211
|
+
count={commentsCount}
|
|
212
|
+
defaultCollapsed={item.isPr}
|
|
213
|
+
>
|
|
190
214
|
<ConsoleCommentList
|
|
191
215
|
comments={comments}
|
|
192
216
|
isLoading={commentsAreLoading}
|
|
@@ -196,7 +220,7 @@ export const ConsoleItemDetail = ({
|
|
|
196
220
|
</ConsolePanel>
|
|
197
221
|
|
|
198
222
|
{item.isPr && (
|
|
199
|
-
<ConsolePanel title="Commits" defaultCollapsed>
|
|
223
|
+
<ConsolePanel title="Commits" count={commitsCount} defaultCollapsed>
|
|
200
224
|
<ConsoleCommitList
|
|
201
225
|
commits={commits}
|
|
202
226
|
isLoading={commitsAreLoading}
|
|
@@ -223,7 +247,9 @@ export const ConsoleItemDetail = ({
|
|
|
223
247
|
/>
|
|
224
248
|
))}
|
|
225
249
|
|
|
226
|
-
|
|
250
|
+
{commentComposer}
|
|
251
|
+
|
|
252
|
+
<div className="console-actionbar">{operationBar}</div>
|
|
227
253
|
</article>
|
|
228
254
|
);
|
|
229
255
|
};
|
|
@@ -24,3 +24,28 @@ export const Collapsed: Story = {
|
|
|
24
24
|
children: <p style={{ padding: 12 }}>Panel body content</p>,
|
|
25
25
|
},
|
|
26
26
|
};
|
|
27
|
+
|
|
28
|
+
export const WithCountAndOpenLink: Story = {
|
|
29
|
+
args: {
|
|
30
|
+
title: 'Description',
|
|
31
|
+
headerAction: (
|
|
32
|
+
<a
|
|
33
|
+
href="https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/pull/851"
|
|
34
|
+
className="console-panel-open-link"
|
|
35
|
+
target="_blank"
|
|
36
|
+
rel="noopener noreferrer"
|
|
37
|
+
>
|
|
38
|
+
open
|
|
39
|
+
</a>
|
|
40
|
+
),
|
|
41
|
+
children: <p style={{ padding: 12 }}>Panel body content</p>,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const ChangedFilesWithCount: Story = {
|
|
46
|
+
args: {
|
|
47
|
+
title: 'Changed files',
|
|
48
|
+
count: 3,
|
|
49
|
+
children: <p style={{ padding: 12 }}>Panel body content</p>,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
@@ -20,6 +20,33 @@ describe('ConsolePanel', () => {
|
|
|
20
20
|
expect(queryByText('content')).toBeNull();
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
+
it('appends the count to the title when provided', () => {
|
|
24
|
+
const { getByText } = render(
|
|
25
|
+
<ConsolePanel title="Changed files" count={3}>
|
|
26
|
+
<p>content</p>
|
|
27
|
+
</ConsolePanel>,
|
|
28
|
+
);
|
|
29
|
+
expect(getByText('Changed files (3)')).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('omits the count when it is null', () => {
|
|
33
|
+
const { getByText } = render(
|
|
34
|
+
<ConsolePanel title="Description" count={null}>
|
|
35
|
+
<p>content</p>
|
|
36
|
+
</ConsolePanel>,
|
|
37
|
+
);
|
|
38
|
+
expect(getByText('Description')).toBeInTheDocument();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('renders the header action node', () => {
|
|
42
|
+
const { getByText } = render(
|
|
43
|
+
<ConsolePanel title="Description" headerAction={<a href="/x">open</a>}>
|
|
44
|
+
<p>content</p>
|
|
45
|
+
</ConsolePanel>,
|
|
46
|
+
);
|
|
47
|
+
expect(getByText('open')).toBeInTheDocument();
|
|
48
|
+
});
|
|
49
|
+
|
|
23
50
|
it('toggles collapsed state on the header button', () => {
|
|
24
51
|
const { getByRole, queryByText } = render(
|
|
25
52
|
<ConsolePanel title="Comments" defaultCollapsed>
|
package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsolePanel.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { type ReactNode, useState } from 'react';
|
|
|
2
2
|
|
|
3
3
|
export type ConsolePanelProps = {
|
|
4
4
|
title: string;
|
|
5
|
+
count?: number | null;
|
|
5
6
|
defaultCollapsed?: boolean;
|
|
6
7
|
headerAction?: ReactNode;
|
|
7
8
|
children: ReactNode;
|
|
@@ -9,11 +10,13 @@ export type ConsolePanelProps = {
|
|
|
9
10
|
|
|
10
11
|
export const ConsolePanel = ({
|
|
11
12
|
title,
|
|
13
|
+
count = null,
|
|
12
14
|
defaultCollapsed = false,
|
|
13
15
|
headerAction,
|
|
14
16
|
children,
|
|
15
17
|
}: ConsolePanelProps) => {
|
|
16
18
|
const [collapsed, setCollapsed] = useState<boolean>(defaultCollapsed);
|
|
19
|
+
const heading = count === null ? title : `${title} (${count})`;
|
|
17
20
|
return (
|
|
18
21
|
<section className="console-panel">
|
|
19
22
|
<header className="console-panel-header">
|
|
@@ -24,7 +27,7 @@ export const ConsolePanel = ({
|
|
|
24
27
|
onClick={() => setCollapsed((value) => !value)}
|
|
25
28
|
>
|
|
26
29
|
<span className="console-panel-caret">{collapsed ? '▸' : '▾'}</span>
|
|
27
|
-
<span className="console-panel-title">{
|
|
30
|
+
<span className="console-panel-title">{heading}</span>
|
|
28
31
|
</button>
|
|
29
32
|
{headerAction !== undefined && (
|
|
30
33
|
<div className="console-panel-action">{headerAction}</div>
|