github-issue-tower-defence-management 1.165.1 → 1.166.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/bin/adapter/entry-points/console/consoleOperationApi.js +44 -1
- package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleTabNames.js +1 -0
- package/bin/adapter/entry-points/console/consoleTabNames.js.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-CrSqMAVR.js +86 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/bin/adapter/entry-points/console/webServer.js +2 -0
- package/bin/adapter/entry-points/console/webServer.js.map +1 -1
- package/bin/domain/usecases/console/GenerateConsoleListsUseCase.js +23 -0
- package/bin/domain/usecases/console/GenerateConsoleListsUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/console/consoleOperationApi.test.ts +211 -0
- package/src/adapter/entry-points/console/consoleOperationApi.ts +63 -0
- package/src/adapter/entry-points/console/consoleTabNames.ts +1 -0
- package/src/adapter/entry-points/console/ui/e2e/consoleScenario.spec.ts +61 -0
- package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +63 -2
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.stories.tsx +4 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.test.tsx +1 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.stories.tsx +77 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.test.tsx +196 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.tsx +163 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleNavigation.test.ts +1 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.ts +15 -0
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +29 -0
- package/src/adapter/entry-points/console/ui/src/features/console/logic/tabAdvance.test.ts +1 -0
- package/src/adapter/entry-points/console/ui/src/features/console/logic/types.ts +10 -1
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +43 -8
- package/src/adapter/entry-points/console/ui-dist/assets/index-CrSqMAVR.js +86 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/src/adapter/entry-points/console/webServer.ts +3 -0
- package/src/domain/usecases/console/GenerateConsoleListsUseCase.test.ts +86 -1
- package/src/domain/usecases/console/GenerateConsoleListsUseCase.ts +49 -1
- 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/consoleTabNames.d.ts.map +1 -1
- package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
- package/types/domain/usecases/console/GenerateConsoleListsUseCase.d.ts +18 -1
- package/types/domain/usecases/console/GenerateConsoleListsUseCase.d.ts.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-CHUtr_4X.js +0 -86
- package/src/adapter/entry-points/console/ui-dist/assets/index-CHUtr_4X.js +0 -86
|
@@ -38,6 +38,12 @@ export type ConsoleE2eRequestChangesCall = {
|
|
|
38
38
|
inlineCommentLocation: { line: number; side: string } | null;
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
+
export type ConsoleE2eCreateIssueCall = {
|
|
42
|
+
org: string;
|
|
43
|
+
repo: string;
|
|
44
|
+
title: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
41
47
|
type ConsoleFixtureListItem = {
|
|
42
48
|
number: number;
|
|
43
49
|
title: string;
|
|
@@ -275,6 +281,39 @@ export const CONSOLE_E2E_TAB_ITEMS: Record<string, ConsoleFixtureListItem[]> = {
|
|
|
275
281
|
],
|
|
276
282
|
};
|
|
277
283
|
|
|
284
|
+
const CONSOLE_E2E_STORIES_SNAPSHOT = {
|
|
285
|
+
pjcode: CONSOLE_E2E_PJCODE,
|
|
286
|
+
generatedAt: '2026-06-18T01:22:09.000Z',
|
|
287
|
+
stories: [
|
|
288
|
+
{
|
|
289
|
+
storyName: 'TDPM Console port',
|
|
290
|
+
storyOptionId: '1491051e',
|
|
291
|
+
color: 'BLUE',
|
|
292
|
+
openItemCount: 6,
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
storyName: 'Publish product documentation site',
|
|
296
|
+
storyOptionId: 'f7cd5cbc',
|
|
297
|
+
color: 'GREEN',
|
|
298
|
+
openItemCount: 1,
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
storyName: 'regular / WORKFLOW BLOCKER',
|
|
302
|
+
storyOptionId: 'a3b9c4d2',
|
|
303
|
+
color: 'RED',
|
|
304
|
+
openItemCount: 1,
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
storyOrder: [
|
|
308
|
+
'TDPM Console port',
|
|
309
|
+
'regular / workflow improvement',
|
|
310
|
+
'Publish product documentation site',
|
|
311
|
+
'regular / WORKFLOW BLOCKER',
|
|
312
|
+
],
|
|
313
|
+
storyColors: STORY_COLORS,
|
|
314
|
+
defaultNameWithOwner: REPO_NAME_WITH_OWNER,
|
|
315
|
+
};
|
|
316
|
+
|
|
278
317
|
const writeFixtureData = (consoleDataOutputDir: string): void => {
|
|
279
318
|
for (const [tab, items] of Object.entries(CONSOLE_E2E_TAB_ITEMS)) {
|
|
280
319
|
const tabDir = path.join(consoleDataOutputDir, CONSOLE_E2E_PJCODE, tab);
|
|
@@ -284,6 +323,16 @@ const writeFixtureData = (consoleDataOutputDir: string): void => {
|
|
|
284
323
|
JSON.stringify(buildSnapshot(items)),
|
|
285
324
|
);
|
|
286
325
|
}
|
|
326
|
+
const storiesTabDir = path.join(
|
|
327
|
+
consoleDataOutputDir,
|
|
328
|
+
CONSOLE_E2E_PJCODE,
|
|
329
|
+
'stories',
|
|
330
|
+
);
|
|
331
|
+
fs.mkdirSync(storiesTabDir, { recursive: true });
|
|
332
|
+
fs.writeFileSync(
|
|
333
|
+
path.join(storiesTabDir, 'list.json'),
|
|
334
|
+
JSON.stringify(CONSOLE_E2E_STORIES_SNAPSHOT),
|
|
335
|
+
);
|
|
287
336
|
};
|
|
288
337
|
|
|
289
338
|
const buildE2eProject = (): Project => ({
|
|
@@ -408,13 +457,21 @@ const inlineCommentPullRequestDetail: PullRequestDetail = {
|
|
|
408
457
|
const createStubIssueRepository = (
|
|
409
458
|
reviewCommentCalls: ConsoleE2eReviewCommentCall[],
|
|
410
459
|
requestChangesCalls: ConsoleE2eRequestChangesCall[],
|
|
460
|
+
createIssueCalls: ConsoleE2eCreateIssueCall[],
|
|
411
461
|
): IssueRepository => ({
|
|
412
462
|
getAllIssues: () => notImplemented('getAllIssues'),
|
|
413
463
|
getIssueByUrl: async (url: string): Promise<Issue | null> =>
|
|
414
464
|
buildIssueForUrl(url),
|
|
415
465
|
getIssueBodyByUrl: async (url: string): Promise<string | null> =>
|
|
416
466
|
buildIssueForUrl(url)?.body ?? null,
|
|
417
|
-
createNewIssue:
|
|
467
|
+
createNewIssue: async (
|
|
468
|
+
org: string,
|
|
469
|
+
repo: string,
|
|
470
|
+
title: string,
|
|
471
|
+
): Promise<number> => {
|
|
472
|
+
createIssueCalls.push({ org, repo, title });
|
|
473
|
+
return 9001;
|
|
474
|
+
},
|
|
418
475
|
searchIssue: () => notImplemented('searchIssue'),
|
|
419
476
|
updateIssue: () => notImplemented('updateIssue'),
|
|
420
477
|
updateIssueBody: () => notImplemented('updateIssueBody'),
|
|
@@ -507,7 +564,7 @@ const createStubIssueRepository = (
|
|
|
507
564
|
createCommentByUrl: async (): Promise<void> => undefined,
|
|
508
565
|
getAllOpened: () => notImplemented('getAllOpened'),
|
|
509
566
|
getStoryObjectMap: () => notImplemented('getStoryObjectMap'),
|
|
510
|
-
addIssueToProject: () =>
|
|
567
|
+
addIssueToProject: async (): Promise<void> => undefined,
|
|
511
568
|
setDependedIssueUrl: () => notImplemented('setDependedIssueUrl'),
|
|
512
569
|
getIssueOrPullRequestBody: async (): Promise<string> =>
|
|
513
570
|
[
|
|
@@ -572,6 +629,7 @@ export type ConsoleE2eHarness = {
|
|
|
572
629
|
consoleDataOutputDir: string;
|
|
573
630
|
reviewCommentCalls: ConsoleE2eReviewCommentCall[];
|
|
574
631
|
requestChangesCalls: ConsoleE2eRequestChangesCall[];
|
|
632
|
+
createIssueCalls: ConsoleE2eCreateIssueCall[];
|
|
575
633
|
stop: () => Promise<void>;
|
|
576
634
|
};
|
|
577
635
|
|
|
@@ -592,6 +650,7 @@ export const startConsoleE2eHarness = async (): Promise<ConsoleE2eHarness> => {
|
|
|
592
650
|
|
|
593
651
|
const reviewCommentCalls: ConsoleE2eReviewCommentCall[] = [];
|
|
594
652
|
const requestChangesCalls: ConsoleE2eRequestChangesCall[] = [];
|
|
653
|
+
const createIssueCalls: ConsoleE2eCreateIssueCall[] = [];
|
|
595
654
|
|
|
596
655
|
const server = await startWebServer({
|
|
597
656
|
accessToken: CONSOLE_E2E_TOKEN,
|
|
@@ -600,6 +659,7 @@ export const startConsoleE2eHarness = async (): Promise<ConsoleE2eHarness> => {
|
|
|
600
659
|
issueRepository: createStubIssueRepository(
|
|
601
660
|
reviewCommentCalls,
|
|
602
661
|
requestChangesCalls,
|
|
662
|
+
createIssueCalls,
|
|
603
663
|
),
|
|
604
664
|
resolveProject,
|
|
605
665
|
isPjcodeConfigured,
|
|
@@ -629,6 +689,7 @@ export const startConsoleE2eHarness = async (): Promise<ConsoleE2eHarness> => {
|
|
|
629
689
|
consoleDataOutputDir,
|
|
630
690
|
reviewCommentCalls,
|
|
631
691
|
requestChangesCalls,
|
|
692
|
+
createIssueCalls,
|
|
632
693
|
stop: async (): Promise<void> => {
|
|
633
694
|
await closeServer(server);
|
|
634
695
|
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
@@ -26,6 +26,7 @@ const counts: Record<ConsoleTabName, number> = {
|
|
|
26
26
|
'failed-preparation': 2,
|
|
27
27
|
'todo-by-human': 66,
|
|
28
28
|
'todo-by-agent': 24,
|
|
29
|
+
stories: 3,
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
export const AllTabsWithCounts: Story = {
|
|
@@ -46,6 +47,7 @@ export const ZeroCountTabsHidden: Story = {
|
|
|
46
47
|
'failed-preparation': 0,
|
|
47
48
|
'todo-by-human': 0,
|
|
48
49
|
'todo-by-agent': 0,
|
|
50
|
+
stories: 0,
|
|
49
51
|
},
|
|
50
52
|
},
|
|
51
53
|
};
|
|
@@ -61,6 +63,7 @@ export const ZeroCountActiveTabStaysVisible: Story = {
|
|
|
61
63
|
'failed-preparation': 0,
|
|
62
64
|
'todo-by-human': 0,
|
|
63
65
|
'todo-by-agent': 0,
|
|
66
|
+
stories: 0,
|
|
64
67
|
},
|
|
65
68
|
},
|
|
66
69
|
};
|
|
@@ -76,6 +79,7 @@ export const AfterAutoAdvanceToNextTab: Story = {
|
|
|
76
79
|
'failed-preparation': 2,
|
|
77
80
|
'todo-by-human': 4,
|
|
78
81
|
'todo-by-agent': 0,
|
|
82
|
+
stories: 0,
|
|
79
83
|
},
|
|
80
84
|
onSelectTab: () => {},
|
|
81
85
|
},
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { ConsoleStoryList } from './ConsoleStoryList';
|
|
3
|
+
|
|
4
|
+
const stories = [
|
|
5
|
+
{
|
|
6
|
+
storyName: 'TDPM Console port',
|
|
7
|
+
storyOptionId: '1491051e',
|
|
8
|
+
color: 'BLUE' as const,
|
|
9
|
+
openItemCount: 12,
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
storyName: 'Publish product documentation site',
|
|
13
|
+
storyOptionId: 'f7cd5cbc',
|
|
14
|
+
color: 'GREEN' as const,
|
|
15
|
+
openItemCount: 3,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
storyName: 'Move to Okinawa',
|
|
19
|
+
storyOptionId: '564803ee',
|
|
20
|
+
color: 'PURPLE' as const,
|
|
21
|
+
openItemCount: 0,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
storyName: 'regular / regular payment invoice tax',
|
|
25
|
+
storyOptionId: 'd7cdcb61',
|
|
26
|
+
color: 'YELLOW' as const,
|
|
27
|
+
openItemCount: 7,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
storyName: 'regular / WORKFLOW BLOCKER',
|
|
31
|
+
storyOptionId: 'e35b3da2',
|
|
32
|
+
color: 'RED' as const,
|
|
33
|
+
openItemCount: 2,
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const meta: Meta<typeof ConsoleStoryList> = {
|
|
38
|
+
title: 'Console/ConsoleStoryList',
|
|
39
|
+
component: ConsoleStoryList,
|
|
40
|
+
args: { onCreateIssue: () => Promise.resolve() },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export default meta;
|
|
44
|
+
|
|
45
|
+
type Story = StoryObj<typeof ConsoleStoryList>;
|
|
46
|
+
|
|
47
|
+
export const WithStories: Story = {
|
|
48
|
+
args: {
|
|
49
|
+
stories,
|
|
50
|
+
isLoading: false,
|
|
51
|
+
error: null,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const Loading: Story = {
|
|
56
|
+
args: {
|
|
57
|
+
stories: [],
|
|
58
|
+
isLoading: true,
|
|
59
|
+
error: null,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const Empty: Story = {
|
|
64
|
+
args: {
|
|
65
|
+
stories: [],
|
|
66
|
+
isLoading: false,
|
|
67
|
+
error: null,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const ErrorState: Story = {
|
|
72
|
+
args: {
|
|
73
|
+
stories: [],
|
|
74
|
+
isLoading: false,
|
|
75
|
+
error: 'HTTP 404',
|
|
76
|
+
},
|
|
77
|
+
};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { fireEvent, render, waitFor } from '@testing-library/react';
|
|
2
|
+
import type { ConsoleStoryEntry } from '../../logic/types';
|
|
3
|
+
import { ConsoleStoryList } from './ConsoleStoryList';
|
|
4
|
+
|
|
5
|
+
const storyEntries: ConsoleStoryEntry[] = [
|
|
6
|
+
{
|
|
7
|
+
storyName: 'TDPM Console port',
|
|
8
|
+
storyOptionId: '1491051e',
|
|
9
|
+
color: 'BLUE',
|
|
10
|
+
openItemCount: 12,
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
storyName: 'Move to Okinawa',
|
|
14
|
+
storyOptionId: '564803ee',
|
|
15
|
+
color: 'PURPLE',
|
|
16
|
+
openItemCount: 0,
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
describe('ConsoleStoryList', () => {
|
|
21
|
+
it('renders each story name and its open item count', () => {
|
|
22
|
+
const { getByText } = render(
|
|
23
|
+
<ConsoleStoryList
|
|
24
|
+
stories={storyEntries}
|
|
25
|
+
isLoading={false}
|
|
26
|
+
error={null}
|
|
27
|
+
onCreateIssue={() => Promise.resolve()}
|
|
28
|
+
/>,
|
|
29
|
+
);
|
|
30
|
+
expect(getByText('TDPM Console port')).toBeInTheDocument();
|
|
31
|
+
expect(getByText('12')).toBeInTheDocument();
|
|
32
|
+
expect(getByText('Move to Okinawa')).toBeInTheDocument();
|
|
33
|
+
expect(getByText('0')).toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('renders an Add task button for each story row', () => {
|
|
37
|
+
const { getAllByRole } = render(
|
|
38
|
+
<ConsoleStoryList
|
|
39
|
+
stories={storyEntries}
|
|
40
|
+
isLoading={false}
|
|
41
|
+
error={null}
|
|
42
|
+
onCreateIssue={() => Promise.resolve()}
|
|
43
|
+
/>,
|
|
44
|
+
);
|
|
45
|
+
const buttons = getAllByRole('button', { name: 'Add task' });
|
|
46
|
+
expect(buttons).toHaveLength(storyEntries.length);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('shows the create form when Add task is clicked', () => {
|
|
50
|
+
const { getAllByRole, getByPlaceholderText } = render(
|
|
51
|
+
<ConsoleStoryList
|
|
52
|
+
stories={storyEntries}
|
|
53
|
+
isLoading={false}
|
|
54
|
+
error={null}
|
|
55
|
+
onCreateIssue={() => Promise.resolve()}
|
|
56
|
+
/>,
|
|
57
|
+
);
|
|
58
|
+
const [firstButton] = getAllByRole('button', { name: 'Add task' });
|
|
59
|
+
fireEvent.click(firstButton);
|
|
60
|
+
expect(getByPlaceholderText('Issue title')).toBeInTheDocument();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('hides the form when Add task is clicked again on the same row', () => {
|
|
64
|
+
const { getAllByRole, queryByPlaceholderText } = render(
|
|
65
|
+
<ConsoleStoryList
|
|
66
|
+
stories={storyEntries}
|
|
67
|
+
isLoading={false}
|
|
68
|
+
error={null}
|
|
69
|
+
onCreateIssue={() => Promise.resolve()}
|
|
70
|
+
/>,
|
|
71
|
+
);
|
|
72
|
+
const [firstButton] = getAllByRole('button', { name: 'Add task' });
|
|
73
|
+
fireEvent.click(firstButton);
|
|
74
|
+
fireEvent.click(firstButton);
|
|
75
|
+
expect(queryByPlaceholderText('Issue title')).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('calls onCreateIssue with the correct storyOptionId and title', async () => {
|
|
79
|
+
const onCreateIssue = jest.fn().mockResolvedValue(undefined);
|
|
80
|
+
const { getAllByRole, getByPlaceholderText, getByRole } = render(
|
|
81
|
+
<ConsoleStoryList
|
|
82
|
+
stories={storyEntries}
|
|
83
|
+
isLoading={false}
|
|
84
|
+
error={null}
|
|
85
|
+
onCreateIssue={onCreateIssue}
|
|
86
|
+
/>,
|
|
87
|
+
);
|
|
88
|
+
fireEvent.click(getAllByRole('button', { name: 'Add task' })[0]);
|
|
89
|
+
fireEvent.change(getByPlaceholderText('Issue title'), {
|
|
90
|
+
target: { value: 'New feature task' },
|
|
91
|
+
});
|
|
92
|
+
fireEvent.click(getByRole('button', { name: 'Create' }));
|
|
93
|
+
await waitFor(() =>
|
|
94
|
+
expect(onCreateIssue).toHaveBeenCalledWith(
|
|
95
|
+
'1491051e',
|
|
96
|
+
'New feature task',
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('shows a validation error when Create is clicked with an empty title', () => {
|
|
102
|
+
const { getAllByRole, getByRole, getByText } = render(
|
|
103
|
+
<ConsoleStoryList
|
|
104
|
+
stories={storyEntries}
|
|
105
|
+
isLoading={false}
|
|
106
|
+
error={null}
|
|
107
|
+
onCreateIssue={() => Promise.resolve()}
|
|
108
|
+
/>,
|
|
109
|
+
);
|
|
110
|
+
fireEvent.click(getAllByRole('button', { name: 'Add task' })[0]);
|
|
111
|
+
fireEvent.click(getByRole('button', { name: 'Create' }));
|
|
112
|
+
expect(getByText('Title is required')).toBeInTheDocument();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('closes the form and clears the input after a successful create', async () => {
|
|
116
|
+
const onCreateIssue = jest.fn().mockResolvedValue(undefined);
|
|
117
|
+
const {
|
|
118
|
+
getAllByRole,
|
|
119
|
+
getByPlaceholderText,
|
|
120
|
+
getByRole,
|
|
121
|
+
queryByPlaceholderText,
|
|
122
|
+
} = render(
|
|
123
|
+
<ConsoleStoryList
|
|
124
|
+
stories={storyEntries}
|
|
125
|
+
isLoading={false}
|
|
126
|
+
error={null}
|
|
127
|
+
onCreateIssue={onCreateIssue}
|
|
128
|
+
/>,
|
|
129
|
+
);
|
|
130
|
+
fireEvent.click(getAllByRole('button', { name: 'Add task' })[0]);
|
|
131
|
+
fireEvent.change(getByPlaceholderText('Issue title'), {
|
|
132
|
+
target: { value: 'My task' },
|
|
133
|
+
});
|
|
134
|
+
fireEvent.click(getByRole('button', { name: 'Create' }));
|
|
135
|
+
await waitFor(() =>
|
|
136
|
+
expect(queryByPlaceholderText('Issue title')).toBeNull(),
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('shows an API error when onCreateIssue rejects', async () => {
|
|
141
|
+
const onCreateIssue = jest
|
|
142
|
+
.fn()
|
|
143
|
+
.mockRejectedValue(new Error('Network error'));
|
|
144
|
+
const { getAllByRole, getByPlaceholderText, getByRole, getByText } = render(
|
|
145
|
+
<ConsoleStoryList
|
|
146
|
+
stories={storyEntries}
|
|
147
|
+
isLoading={false}
|
|
148
|
+
error={null}
|
|
149
|
+
onCreateIssue={onCreateIssue}
|
|
150
|
+
/>,
|
|
151
|
+
);
|
|
152
|
+
fireEvent.click(getAllByRole('button', { name: 'Add task' })[0]);
|
|
153
|
+
fireEvent.change(getByPlaceholderText('Issue title'), {
|
|
154
|
+
target: { value: 'My task' },
|
|
155
|
+
});
|
|
156
|
+
fireEvent.click(getByRole('button', { name: 'Create' }));
|
|
157
|
+
await waitFor(() => expect(getByText('Network error')).toBeInTheDocument());
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('shows a loading message when isLoading is true', () => {
|
|
161
|
+
const { getByText } = render(
|
|
162
|
+
<ConsoleStoryList
|
|
163
|
+
stories={[]}
|
|
164
|
+
isLoading={true}
|
|
165
|
+
error={null}
|
|
166
|
+
onCreateIssue={() => Promise.resolve()}
|
|
167
|
+
/>,
|
|
168
|
+
);
|
|
169
|
+
expect(getByText('Loading stories...')).toBeInTheDocument();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('shows an empty message when there are no stories', () => {
|
|
173
|
+
const { getByText } = render(
|
|
174
|
+
<ConsoleStoryList
|
|
175
|
+
stories={[]}
|
|
176
|
+
isLoading={false}
|
|
177
|
+
error={null}
|
|
178
|
+
onCreateIssue={() => Promise.resolve()}
|
|
179
|
+
/>,
|
|
180
|
+
);
|
|
181
|
+
expect(getByText('No active stories')).toBeInTheDocument();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('shows an error message when error is set', () => {
|
|
185
|
+
const { getByRole, getByText } = render(
|
|
186
|
+
<ConsoleStoryList
|
|
187
|
+
stories={[]}
|
|
188
|
+
isLoading={false}
|
|
189
|
+
error="HTTP 503"
|
|
190
|
+
onCreateIssue={() => Promise.resolve()}
|
|
191
|
+
/>,
|
|
192
|
+
);
|
|
193
|
+
expect(getByRole('alert')).toBeInTheDocument();
|
|
194
|
+
expect(getByText(/HTTP 503/)).toBeInTheDocument();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { colorFromEnum } from '../../logic/colors';
|
|
3
|
+
import type { ConsoleStoryEntry } from '../../logic/types';
|
|
4
|
+
|
|
5
|
+
type StoryCreateFormProps = {
|
|
6
|
+
storyOptionId: string;
|
|
7
|
+
onSubmit: (storyOptionId: string, title: string) => Promise<void>;
|
|
8
|
+
onCancel: () => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const StoryCreateForm = ({
|
|
12
|
+
storyOptionId,
|
|
13
|
+
onSubmit,
|
|
14
|
+
onCancel,
|
|
15
|
+
}: StoryCreateFormProps) => {
|
|
16
|
+
const [titleInput, setTitleInput] = useState('');
|
|
17
|
+
const [submitting, setSubmitting] = useState(false);
|
|
18
|
+
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
19
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
inputRef.current?.focus();
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
25
|
+
const handleSubmit = async (): Promise<void> => {
|
|
26
|
+
const trimmed = titleInput.trim();
|
|
27
|
+
if (trimmed.length === 0) {
|
|
28
|
+
setSubmitError('Title is required');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
setSubmitting(true);
|
|
32
|
+
setSubmitError(null);
|
|
33
|
+
try {
|
|
34
|
+
await onSubmit(storyOptionId, trimmed);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
setSubmitError(err instanceof Error ? err.message : String(err));
|
|
37
|
+
} finally {
|
|
38
|
+
setSubmitting(false);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<form
|
|
44
|
+
className="console-story-create-form"
|
|
45
|
+
onSubmit={(event) => {
|
|
46
|
+
event.preventDefault();
|
|
47
|
+
void handleSubmit();
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
<input
|
|
51
|
+
ref={inputRef}
|
|
52
|
+
type="text"
|
|
53
|
+
className="console-story-create-input"
|
|
54
|
+
placeholder="Issue title"
|
|
55
|
+
value={titleInput}
|
|
56
|
+
onChange={(e) => setTitleInput(e.target.value)}
|
|
57
|
+
disabled={submitting}
|
|
58
|
+
/>
|
|
59
|
+
<button type="submit" className="console-op-button" disabled={submitting}>
|
|
60
|
+
{submitting ? 'Creating…' : 'Create'}
|
|
61
|
+
</button>
|
|
62
|
+
<button
|
|
63
|
+
type="button"
|
|
64
|
+
className="console-op-button"
|
|
65
|
+
onClick={onCancel}
|
|
66
|
+
disabled={submitting}
|
|
67
|
+
>
|
|
68
|
+
Cancel
|
|
69
|
+
</button>
|
|
70
|
+
{submitError !== null && (
|
|
71
|
+
<p role="alert" className="console-list-error">
|
|
72
|
+
{submitError}
|
|
73
|
+
</p>
|
|
74
|
+
)}
|
|
75
|
+
</form>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export type ConsoleStoryListProps = {
|
|
80
|
+
stories: ConsoleStoryEntry[];
|
|
81
|
+
isLoading: boolean;
|
|
82
|
+
error: string | null;
|
|
83
|
+
onCreateIssue: (storyOptionId: string, title: string) => Promise<void>;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const ConsoleStoryList = ({
|
|
87
|
+
stories,
|
|
88
|
+
isLoading,
|
|
89
|
+
error,
|
|
90
|
+
onCreateIssue,
|
|
91
|
+
}: ConsoleStoryListProps) => {
|
|
92
|
+
const [expandedOptionId, setExpandedOptionId] = useState<string | null>(null);
|
|
93
|
+
|
|
94
|
+
if (error !== null) {
|
|
95
|
+
return (
|
|
96
|
+
<p role="alert" className="console-list-message console-list-error">
|
|
97
|
+
Failed to load stories: {error}
|
|
98
|
+
</p>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (isLoading) {
|
|
103
|
+
return <p className="console-list-message">Loading stories...</p>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (stories.length === 0) {
|
|
107
|
+
return <p className="console-list-empty">No active stories</p>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const handleAddClick = (storyOptionId: string): void => {
|
|
111
|
+
setExpandedOptionId(
|
|
112
|
+
expandedOptionId === storyOptionId ? null : storyOptionId,
|
|
113
|
+
);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const handleSubmit = async (
|
|
117
|
+
storyOptionId: string,
|
|
118
|
+
title: string,
|
|
119
|
+
): Promise<void> => {
|
|
120
|
+
await onCreateIssue(storyOptionId, title);
|
|
121
|
+
setExpandedOptionId(null);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<ul className="console-story-list">
|
|
126
|
+
{stories.map((entry) => {
|
|
127
|
+
const palette = colorFromEnum(entry.color);
|
|
128
|
+
const isExpanded = expandedOptionId === entry.storyOptionId;
|
|
129
|
+
return (
|
|
130
|
+
<li key={entry.storyOptionId} className="console-story-list-row">
|
|
131
|
+
<div className="console-story-list-row-main">
|
|
132
|
+
<span
|
|
133
|
+
className="console-storytag"
|
|
134
|
+
style={{ color: palette.fg, borderColor: palette.border }}
|
|
135
|
+
>
|
|
136
|
+
<span
|
|
137
|
+
className="console-story-dot"
|
|
138
|
+
style={{ backgroundColor: palette.dot }}
|
|
139
|
+
/>
|
|
140
|
+
{entry.storyName}
|
|
141
|
+
</span>
|
|
142
|
+
<span className="console-story-count">{entry.openItemCount}</span>
|
|
143
|
+
<button
|
|
144
|
+
type="button"
|
|
145
|
+
className="console-op-button"
|
|
146
|
+
onClick={() => handleAddClick(entry.storyOptionId)}
|
|
147
|
+
>
|
|
148
|
+
Add task
|
|
149
|
+
</button>
|
|
150
|
+
</div>
|
|
151
|
+
{isExpanded && (
|
|
152
|
+
<StoryCreateForm
|
|
153
|
+
storyOptionId={entry.storyOptionId}
|
|
154
|
+
onSubmit={handleSubmit}
|
|
155
|
+
onCancel={() => setExpandedOptionId(null)}
|
|
156
|
+
/>
|
|
157
|
+
)}
|
|
158
|
+
</li>
|
|
159
|
+
);
|
|
160
|
+
})}
|
|
161
|
+
</ul>
|
|
162
|
+
);
|
|
163
|
+
};
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
ConsoleFieldOption,
|
|
4
4
|
ConsoleListItem,
|
|
5
5
|
ConsoleStoryColorSource,
|
|
6
|
+
ConsoleStoryEntry,
|
|
6
7
|
ConsoleTabName,
|
|
7
8
|
} from '../logic/types';
|
|
8
9
|
import { CONSOLE_TABS } from '../logic/types';
|
|
@@ -13,6 +14,8 @@ export type ConsoleTabSnapshot = {
|
|
|
13
14
|
statusOptions: ConsoleFieldOption[];
|
|
14
15
|
storyOptions: ConsoleFieldOption[];
|
|
15
16
|
storyColors: ConsoleStoryColorSource;
|
|
17
|
+
stories: ConsoleStoryEntry[];
|
|
18
|
+
defaultNameWithOwner: string | null;
|
|
16
19
|
};
|
|
17
20
|
|
|
18
21
|
export type ConsoleTabDataState = {
|
|
@@ -51,6 +54,13 @@ const parseOptions = (value: unknown): ConsoleFieldOption[] => {
|
|
|
51
54
|
return value.filter(isRecord) as unknown as ConsoleFieldOption[];
|
|
52
55
|
};
|
|
53
56
|
|
|
57
|
+
const parseStoryEntries = (value: unknown): ConsoleStoryEntry[] => {
|
|
58
|
+
if (!Array.isArray(value)) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
return value.filter(isRecord) as unknown as ConsoleStoryEntry[];
|
|
62
|
+
};
|
|
63
|
+
|
|
54
64
|
const parseSnapshot = (payload: unknown): ConsoleTabSnapshot => ({
|
|
55
65
|
items: parseItems(payload),
|
|
56
66
|
generatedAt:
|
|
@@ -63,6 +73,11 @@ const parseSnapshot = (payload: unknown): ConsoleTabSnapshot => ({
|
|
|
63
73
|
isRecord(payload) && isRecord(payload.storyColors)
|
|
64
74
|
? (payload.storyColors as ConsoleStoryColorSource)
|
|
65
75
|
: {},
|
|
76
|
+
stories: isRecord(payload) ? parseStoryEntries(payload.stories) : [],
|
|
77
|
+
defaultNameWithOwner:
|
|
78
|
+
isRecord(payload) && typeof payload.defaultNameWithOwner === 'string'
|
|
79
|
+
? payload.defaultNameWithOwner
|
|
80
|
+
: null,
|
|
66
81
|
});
|
|
67
82
|
|
|
68
83
|
const emptySnapshots = (): Record<
|
|
@@ -339,3 +339,32 @@ export const postConsoleReviewComment = async (
|
|
|
339
339
|
throw new Error(await readOperationErrorReason(response));
|
|
340
340
|
}
|
|
341
341
|
};
|
|
342
|
+
|
|
343
|
+
export const CREATE_ISSUE_OPERATION_PATH = '/api/createissue';
|
|
344
|
+
|
|
345
|
+
export type ConsoleCreateIssueRequest = {
|
|
346
|
+
pjcode: string;
|
|
347
|
+
title: string;
|
|
348
|
+
storyOptionId: string;
|
|
349
|
+
nameWithOwner: string;
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
export const postConsoleCreateIssue = async (
|
|
353
|
+
request: ConsoleCreateIssueRequest,
|
|
354
|
+
): Promise<string> => {
|
|
355
|
+
const response = await fetch(CREATE_ISSUE_OPERATION_PATH, {
|
|
356
|
+
method: 'POST',
|
|
357
|
+
headers: { 'Content-Type': 'application/json' },
|
|
358
|
+
body: JSON.stringify(request),
|
|
359
|
+
});
|
|
360
|
+
if (!response.ok) {
|
|
361
|
+
throw new Error(await readOperationErrorReason(response));
|
|
362
|
+
}
|
|
363
|
+
const payload: unknown = await response.json();
|
|
364
|
+
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
365
|
+
v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
366
|
+
if (!isRecord(payload) || typeof payload.issueUrl !== 'string') {
|
|
367
|
+
throw new Error('issueUrl was not returned');
|
|
368
|
+
}
|
|
369
|
+
return payload.issueUrl;
|
|
370
|
+
};
|