github-issue-tower-defence-management 1.90.0 → 1.91.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.
Files changed (155) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +14 -1
  3. package/bin/adapter/entry-points/cli/index.js +16 -12
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/cli/projectConfig.js +2 -0
  6. package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
  7. package/bin/adapter/entry-points/console/consoleOperationApi.js +54 -27
  8. package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
  9. package/bin/adapter/entry-points/console/consoleProjectResolver.js +38 -0
  10. package/bin/adapter/entry-points/console/consoleProjectResolver.js.map +1 -0
  11. package/bin/adapter/entry-points/console/consoleServer.js +3 -4
  12. package/bin/adapter/entry-points/console/consoleServer.js.map +1 -1
  13. package/bin/adapter/entry-points/console/ui-dist/assets/index-BU6p3cGU.css +1 -0
  14. package/bin/adapter/entry-points/console/ui-dist/assets/index-BvuSQN9s.js +100 -0
  15. package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
  16. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +16 -0
  17. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  18. package/jest.config.js +57 -9
  19. package/package.json +17 -13
  20. package/src/adapter/entry-points/cli/index.test.ts +12 -2
  21. package/src/adapter/entry-points/cli/index.ts +30 -12
  22. package/src/adapter/entry-points/cli/projectConfig.ts +3 -0
  23. package/src/adapter/entry-points/console/consoleOperationApi.test.ts +129 -15
  24. package/src/adapter/entry-points/console/consoleOperationApi.ts +83 -28
  25. package/src/adapter/entry-points/console/consoleProjectResolver.test.ts +96 -0
  26. package/src/adapter/entry-points/console/consoleProjectResolver.ts +50 -0
  27. package/src/adapter/entry-points/console/consoleServer.test.ts +5 -4
  28. package/src/adapter/entry-points/console/consoleServer.ts +5 -7
  29. package/src/adapter/entry-points/console/ui/jest.setup.ts +1 -0
  30. package/src/adapter/entry-points/console/ui/jest.styleMock.js +1 -0
  31. package/src/adapter/entry-points/console/ui/src/features/console/colors.test.ts +34 -0
  32. package/src/adapter/entry-points/console/ui/src/features/console/colors.ts +73 -0
  33. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleChangedFileList.stories.tsx +28 -0
  34. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleChangedFileList.test.tsx +42 -0
  35. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleChangedFileList.tsx +55 -0
  36. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCloseButtonGroup.stories.tsx +14 -0
  37. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCloseButtonGroup.test.tsx +23 -0
  38. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCloseButtonGroup.tsx +26 -0
  39. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommentList.stories.tsx +29 -0
  40. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommentList.test.tsx +55 -0
  41. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommentList.tsx +66 -0
  42. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommitList.stories.tsx +25 -0
  43. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommitList.test.tsx +53 -0
  44. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommitList.tsx +53 -0
  45. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemDetail.stories.tsx +79 -0
  46. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemDetail.test.tsx +81 -0
  47. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemDetail.tsx +226 -0
  48. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemIcon.stories.tsx +82 -0
  49. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemIcon.test.tsx +52 -0
  50. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemIcon.tsx +32 -0
  51. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListItemRow.stories.tsx +25 -0
  52. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListItemRow.test.tsx +43 -0
  53. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListItemRow.tsx +34 -0
  54. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.stories.tsx +22 -6
  55. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.test.tsx +87 -0
  56. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.tsx +36 -32
  57. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMarkdownView.stories.tsx +27 -0
  58. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMarkdownView.test.tsx +36 -0
  59. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMarkdownView.tsx +50 -0
  60. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMermaidDiagram.stories.tsx +22 -0
  61. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMermaidDiagram.test.tsx +38 -0
  62. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMermaidDiagram.tsx +65 -0
  63. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleNextActionDateGroup.stories.tsx +20 -0
  64. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleNextActionDateGroup.test.tsx +42 -0
  65. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleNextActionDateGroup.tsx +28 -0
  66. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleOperationBar.stories.tsx +55 -0
  67. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleOperationBar.test.tsx +85 -0
  68. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleOperationBar.tsx +55 -0
  69. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePanel.stories.tsx +26 -0
  70. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePanel.test.tsx +32 -0
  71. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePanel.tsx +36 -0
  72. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleProjectHeader.test.tsx +14 -0
  73. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestReviewGroup.stories.tsx +14 -0
  74. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestReviewGroup.test.tsx +33 -0
  75. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestReviewGroup.tsx +34 -0
  76. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestSection.stories.tsx +31 -0
  77. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestSection.test.tsx +40 -0
  78. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestSection.tsx +88 -0
  79. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStatusButtonGroup.stories.tsx +17 -0
  80. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStatusButtonGroup.test.tsx +49 -0
  81. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStatusButtonGroup.tsx +63 -0
  82. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryButtonGroup.stories.tsx +17 -0
  83. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryButtonGroup.test.tsx +45 -0
  84. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryButtonGroup.tsx +42 -0
  85. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryGroupHeader.stories.tsx +27 -0
  86. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryGroupHeader.test.tsx +24 -0
  87. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryGroupHeader.tsx +28 -0
  88. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.stories.tsx +41 -5
  89. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.test.tsx +59 -0
  90. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.tsx +28 -19
  91. package/src/adapter/entry-points/console/ui/src/features/console/fileStatus.test.ts +35 -0
  92. package/src/adapter/entry-points/console/ui/src/features/console/fileStatus.ts +21 -0
  93. package/src/adapter/entry-points/console/ui/src/features/console/fixtures.ts +206 -9
  94. package/src/adapter/entry-points/console/ui/src/features/console/grouping.test.ts +91 -0
  95. package/src/adapter/entry-points/console/ui/src/features/console/grouping.ts +79 -0
  96. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleCaches.test.ts +22 -0
  97. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleCaches.ts +42 -0
  98. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleItemDetailData.test.ts +126 -0
  99. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleItemDetailData.ts +167 -0
  100. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.test.ts +198 -0
  101. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.ts +243 -0
  102. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOverlay.test.ts +40 -0
  103. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOverlay.ts +71 -0
  104. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleResource.test.ts +41 -0
  105. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleResource.ts +57 -0
  106. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.test.ts +63 -0
  107. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.ts +129 -0
  108. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleToken.test.ts +41 -0
  109. package/src/adapter/entry-points/console/ui/src/features/console/itemIcons.test.ts +97 -0
  110. package/src/adapter/entry-points/console/ui/src/features/console/itemIcons.ts +95 -0
  111. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +155 -0
  112. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +187 -0
  113. package/src/adapter/entry-points/console/ui/src/features/console/lib/markdown.test.ts +76 -0
  114. package/src/adapter/entry-points/console/ui/src/features/console/lib/markdown.ts +73 -0
  115. package/src/adapter/entry-points/console/ui/src/features/console/lib/mermaidLoader.test.ts +27 -0
  116. package/src/adapter/entry-points/console/ui/src/features/console/lib/mermaidLoader.ts +71 -0
  117. package/src/adapter/entry-points/console/ui/src/features/console/lib/resourceCache.test.ts +56 -0
  118. package/src/adapter/entry-points/console/ui/src/features/console/lib/resourceCache.ts +51 -0
  119. package/src/adapter/entry-points/console/ui/src/features/console/operations.test.ts +37 -0
  120. package/src/adapter/entry-points/console/ui/src/features/console/operations.ts +35 -0
  121. package/src/adapter/entry-points/console/ui/src/features/console/overlay.test.ts +124 -0
  122. package/src/adapter/entry-points/console/ui/src/features/console/overlay.ts +101 -0
  123. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.test.tsx +79 -0
  124. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +109 -0
  125. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.test.tsx +74 -0
  126. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +133 -7
  127. package/src/adapter/entry-points/console/ui/src/features/console/relativeTime.test.ts +52 -0
  128. package/src/adapter/entry-points/console/ui/src/features/console/relativeTime.ts +51 -0
  129. package/src/adapter/entry-points/console/ui/src/features/console/types.ts +73 -1
  130. package/src/adapter/entry-points/console/ui/src/index.css +352 -2
  131. package/src/adapter/entry-points/console/ui/tsconfig.json +1 -0
  132. package/src/adapter/entry-points/console/ui/vite.config.ts +5 -0
  133. package/src/adapter/entry-points/console/ui-dist/assets/index-BU6p3cGU.css +1 -0
  134. package/src/adapter/entry-points/console/ui-dist/assets/index-BvuSQN9s.js +100 -0
  135. package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
  136. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +25 -0
  137. package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +4 -0
  138. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  139. package/types/adapter/entry-points/cli/projectConfig.d.ts +1 -0
  140. package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
  141. package/types/adapter/entry-points/console/consoleOperationApi.d.ts +6 -2
  142. package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
  143. package/types/adapter/entry-points/console/consoleProjectResolver.d.ts +6 -0
  144. package/types/adapter/entry-points/console/consoleProjectResolver.d.ts.map +1 -0
  145. package/types/adapter/entry-points/console/consoleServer.d.ts +2 -3
  146. package/types/adapter/entry-points/console/consoleServer.d.ts.map +1 -1
  147. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +1 -0
  148. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  149. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +1 -0
  150. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
  151. package/bin/adapter/entry-points/console/ui-dist/assets/index-DDjYPXRT.js +0 -49
  152. package/bin/adapter/entry-points/console/ui-dist/assets/index-DHlBLm7d.css +0 -1
  153. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleList.ts +0 -78
  154. package/src/adapter/entry-points/console/ui-dist/assets/index-DDjYPXRT.js +0 -49
  155. package/src/adapter/entry-points/console/ui-dist/assets/index-DHlBLm7d.css +0 -1
@@ -0,0 +1,52 @@
1
+ import { render } from '@testing-library/react';
2
+ import { CONSOLE_ITEM_ICONS } from '../itemIcons';
3
+ import { ConsoleItemIcon } from './ConsoleItemIcon';
4
+
5
+ describe('ConsoleItemIcon', () => {
6
+ it('renders the green open pull-request icon for an open PR', () => {
7
+ const { getByRole } = render(
8
+ <ConsoleItemIcon
9
+ isPr
10
+ state="open"
11
+ merged={false}
12
+ isDraft={false}
13
+ stateReason=""
14
+ />,
15
+ );
16
+ const svg = getByRole('img');
17
+ expect(svg).toHaveAttribute('fill', CONSOLE_ITEM_ICONS.prOpen.color);
18
+ expect(svg).toHaveAttribute('aria-label', 'prOpen');
19
+ });
20
+
21
+ it('renders the red closed pull-request icon for a closed PR', () => {
22
+ const { getByRole } = render(
23
+ <ConsoleItemIcon
24
+ isPr
25
+ state="closed"
26
+ merged={false}
27
+ isDraft={false}
28
+ stateReason=""
29
+ />,
30
+ );
31
+ expect(getByRole('img')).toHaveAttribute(
32
+ 'fill',
33
+ CONSOLE_ITEM_ICONS.prClosed.color,
34
+ );
35
+ });
36
+
37
+ it('renders the gray not-planned icon for a closed unplanned issue', () => {
38
+ const { getByRole } = render(
39
+ <ConsoleItemIcon
40
+ isPr={false}
41
+ state="closed"
42
+ merged={false}
43
+ isDraft={false}
44
+ stateReason="not_planned"
45
+ />,
46
+ );
47
+ expect(getByRole('img')).toHaveAttribute(
48
+ 'fill',
49
+ CONSOLE_ITEM_ICONS.issueClosedNotPlanned.color,
50
+ );
51
+ });
52
+ });
@@ -0,0 +1,32 @@
1
+ import {
2
+ CONSOLE_ITEM_ICONS,
3
+ type ConsoleItemIconInput,
4
+ resolveConsoleItemIconKind,
5
+ } from '../itemIcons';
6
+
7
+ export type ConsoleItemIconProps = ConsoleItemIconInput & {
8
+ size?: number;
9
+ };
10
+
11
+ export const ConsoleItemIcon = ({
12
+ size = 12,
13
+ ...input
14
+ }: ConsoleItemIconProps) => {
15
+ const kind = resolveConsoleItemIconKind(input);
16
+ const definition = CONSOLE_ITEM_ICONS[kind];
17
+ return (
18
+ <svg
19
+ className="console-item-icon"
20
+ viewBox="0 0 16 16"
21
+ width={size}
22
+ height={size}
23
+ fill={definition.color}
24
+ role="img"
25
+ aria-label={kind}
26
+ >
27
+ {definition.paths.map((d) => (
28
+ <path key={d} d={d} />
29
+ ))}
30
+ </svg>
31
+ );
32
+ };
@@ -0,0 +1,25 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { consoleListItemsFixture } from '../fixtures';
3
+ import { ConsoleListItemRow } from './ConsoleListItemRow';
4
+
5
+ const meta: Meta<typeof ConsoleListItemRow> = {
6
+ title: 'Console/ConsoleListItemRow',
7
+ component: ConsoleListItemRow,
8
+ args: { onSelect: () => {} },
9
+ };
10
+
11
+ export default meta;
12
+
13
+ type Story = StoryObj<typeof ConsoleListItemRow>;
14
+
15
+ export const PullRequestRow: Story = {
16
+ args: { item: consoleListItemsFixture[0], isActive: false },
17
+ };
18
+
19
+ export const IssueRow: Story = {
20
+ args: { item: consoleListItemsFixture[2], isActive: false },
21
+ };
22
+
23
+ export const ActiveRow: Story = {
24
+ args: { item: consoleListItemsFixture[0], isActive: true },
25
+ };
@@ -0,0 +1,43 @@
1
+ import { fireEvent, render } from '@testing-library/react';
2
+ import { consoleListItemsFixture } from '../fixtures';
3
+ import { ConsoleListItemRow } from './ConsoleListItemRow';
4
+
5
+ const prItem = consoleListItemsFixture[0];
6
+ const issueItem = consoleListItemsFixture[2];
7
+
8
+ describe('ConsoleListItemRow', () => {
9
+ it('renders a PR number with the PR prefix', () => {
10
+ const { getByText } = render(
11
+ <ConsoleListItemRow item={prItem} isActive={false} onSelect={() => {}} />,
12
+ );
13
+ expect(getByText(`PR #${prItem.number}`)).toBeInTheDocument();
14
+ expect(getByText(prItem.title)).toBeInTheDocument();
15
+ });
16
+
17
+ it('renders an issue number with the hash prefix', () => {
18
+ const { getByText } = render(
19
+ <ConsoleListItemRow
20
+ item={issueItem}
21
+ isActive={false}
22
+ onSelect={() => {}}
23
+ />,
24
+ );
25
+ expect(getByText(`#${issueItem.number}`)).toBeInTheDocument();
26
+ });
27
+
28
+ it('reports the item on click', () => {
29
+ const onSelect = jest.fn();
30
+ const { getByRole } = render(
31
+ <ConsoleListItemRow item={prItem} isActive={false} onSelect={onSelect} />,
32
+ );
33
+ fireEvent.click(getByRole('button'));
34
+ expect(onSelect).toHaveBeenCalledWith(prItem);
35
+ });
36
+
37
+ it('marks the active row', () => {
38
+ const { getByRole } = render(
39
+ <ConsoleListItemRow item={prItem} isActive onSelect={() => {}} />,
40
+ );
41
+ expect(getByRole('button')).toHaveAttribute('data-active', 'true');
42
+ });
43
+ });
@@ -0,0 +1,34 @@
1
+ import type { ConsoleListItem } from '../types';
2
+ import { ConsoleItemIcon } from './ConsoleItemIcon';
3
+
4
+ export type ConsoleListItemRowProps = {
5
+ item: ConsoleListItem;
6
+ isActive: boolean;
7
+ onSelect: (item: ConsoleListItem) => void;
8
+ };
9
+
10
+ export const ConsoleListItemRow = ({
11
+ item,
12
+ isActive,
13
+ onSelect,
14
+ }: ConsoleListItemRowProps) => (
15
+ <button
16
+ type="button"
17
+ className="console-item-row"
18
+ aria-current={isActive ? 'true' : undefined}
19
+ data-active={isActive ? 'true' : undefined}
20
+ onClick={() => onSelect(item)}
21
+ >
22
+ <ConsoleItemIcon
23
+ isPr={item.isPr}
24
+ state="open"
25
+ merged={false}
26
+ isDraft={false}
27
+ stateReason=""
28
+ />
29
+ <span className="console-item-title">{item.title}</span>
30
+ <span className="console-item-number">
31
+ {item.isPr ? `PR #${item.number}` : `#${item.number}`}
32
+ </span>
33
+ </button>
34
+ );
@@ -1,5 +1,9 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite';
2
- import { consoleListItemsFixture } from '../fixtures';
2
+ import {
3
+ consoleListItemsFixture,
4
+ consoleStoryColorsFixture,
5
+ } from '../fixtures';
6
+ import { buildConsoleListRows } from '../grouping';
3
7
  import { ConsoleListView } from './ConsoleListView';
4
8
 
5
9
  const meta: Meta<typeof ConsoleListView> = {
@@ -11,34 +15,46 @@ export default meta;
11
15
 
12
16
  type Story = StoryObj<typeof ConsoleListView>;
13
17
 
14
- export const WithItems: Story = {
18
+ export const WithStoryGroups: Story = {
15
19
  args: {
16
- items: consoleListItemsFixture,
20
+ rows: buildConsoleListRows(consoleListItemsFixture, {}),
21
+ storyColors: consoleStoryColorsFixture,
22
+ activeItemId: null,
17
23
  isLoading: false,
18
24
  error: null,
25
+ onSelectItem: () => {},
19
26
  },
20
27
  };
21
28
 
22
29
  export const Loading: Story = {
23
30
  args: {
24
- items: [],
31
+ rows: [],
32
+ storyColors: {},
33
+ activeItemId: null,
25
34
  isLoading: true,
26
35
  error: null,
36
+ onSelectItem: () => {},
27
37
  },
28
38
  };
29
39
 
30
40
  export const Empty: Story = {
31
41
  args: {
32
- items: [],
42
+ rows: [],
43
+ storyColors: {},
44
+ activeItemId: null,
33
45
  isLoading: false,
34
46
  error: null,
47
+ onSelectItem: () => {},
35
48
  },
36
49
  };
37
50
 
38
51
  export const ErrorState: Story = {
39
52
  args: {
40
- items: [],
53
+ rows: [],
54
+ storyColors: {},
55
+ activeItemId: null,
41
56
  isLoading: false,
42
57
  error: 'HTTP 404',
58
+ onSelectItem: () => {},
43
59
  },
44
60
  };
@@ -0,0 +1,87 @@
1
+ import { fireEvent, render } from '@testing-library/react';
2
+ import {
3
+ consoleListItemsFixture,
4
+ consoleStoryColorsFixture,
5
+ } from '../fixtures';
6
+ import { buildConsoleListRows } from '../grouping';
7
+ import { ConsoleListView } from './ConsoleListView';
8
+
9
+ const rows = buildConsoleListRows(consoleListItemsFixture, {});
10
+
11
+ describe('ConsoleListView', () => {
12
+ it('renders group headers and items in array order', () => {
13
+ const { getAllByRole, getByText } = render(
14
+ <ConsoleListView
15
+ rows={rows}
16
+ storyColors={consoleStoryColorsFixture}
17
+ activeItemId={null}
18
+ isLoading={false}
19
+ error={null}
20
+ onSelectItem={() => {}}
21
+ />,
22
+ );
23
+ expect(getByText('TDPM Console port')).toBeInTheDocument();
24
+ expect(getByText('regular / workflow improvement')).toBeInTheDocument();
25
+ expect(getAllByRole('button').length).toBe(consoleListItemsFixture.length);
26
+ });
27
+
28
+ it('reports the selected item', () => {
29
+ const onSelectItem = jest.fn();
30
+ const { getByText } = render(
31
+ <ConsoleListView
32
+ rows={rows}
33
+ storyColors={consoleStoryColorsFixture}
34
+ activeItemId={null}
35
+ isLoading={false}
36
+ error={null}
37
+ onSelectItem={onSelectItem}
38
+ />,
39
+ );
40
+ fireEvent.click(
41
+ getByText('Add serveConsole subcommand under entry-points'),
42
+ );
43
+ expect(onSelectItem).toHaveBeenCalledWith(consoleListItemsFixture[0]);
44
+ });
45
+
46
+ it('shows the loading state', () => {
47
+ const { getByText } = render(
48
+ <ConsoleListView
49
+ rows={[]}
50
+ storyColors={{}}
51
+ activeItemId={null}
52
+ isLoading
53
+ error={null}
54
+ onSelectItem={() => {}}
55
+ />,
56
+ );
57
+ expect(getByText('Loading list...')).toBeInTheDocument();
58
+ });
59
+
60
+ it('shows the empty state', () => {
61
+ const { getByText } = render(
62
+ <ConsoleListView
63
+ rows={[]}
64
+ storyColors={{}}
65
+ activeItemId={null}
66
+ isLoading={false}
67
+ error={null}
68
+ onSelectItem={() => {}}
69
+ />,
70
+ );
71
+ expect(getByText('No items.')).toBeInTheDocument();
72
+ });
73
+
74
+ it('shows the error state', () => {
75
+ const { getByRole } = render(
76
+ <ConsoleListView
77
+ rows={[]}
78
+ storyColors={{}}
79
+ activeItemId={null}
80
+ isLoading={false}
81
+ error="HTTP 404"
82
+ onSelectItem={() => {}}
83
+ />,
84
+ );
85
+ expect(getByRole('alert')).toHaveTextContent('HTTP 404');
86
+ });
87
+ });
@@ -1,58 +1,62 @@
1
- import { Badge } from '@/components/ui/badge';
2
- import type { ConsoleListItem } from '../types';
1
+ import { type ConsoleListRow, resolveStoryColorEnum } from '../grouping';
2
+ import type { ConsoleListItem, ConsoleStoryColorSource } from '../types';
3
+ import { ConsoleListItemRow } from './ConsoleListItemRow';
4
+ import { ConsoleStoryGroupHeader } from './ConsoleStoryGroupHeader';
3
5
 
4
6
  export type ConsoleListViewProps = {
5
- items: ConsoleListItem[];
7
+ rows: ConsoleListRow[];
8
+ storyColors: ConsoleStoryColorSource;
9
+ activeItemId: string | null;
6
10
  isLoading: boolean;
7
11
  error: string | null;
12
+ onSelectItem: (item: ConsoleListItem) => void;
8
13
  };
9
14
 
10
15
  export const ConsoleListView = ({
11
- items,
16
+ rows,
17
+ storyColors,
18
+ activeItemId,
12
19
  isLoading,
13
20
  error,
21
+ onSelectItem,
14
22
  }: ConsoleListViewProps) => {
15
23
  if (error !== null) {
16
24
  return (
17
- <p role="alert" className="p-4 text-sm text-destructive">
25
+ <p role="alert" className="console-list-message console-list-error">
18
26
  Failed to load list: {error}
19
27
  </p>
20
28
  );
21
29
  }
22
30
 
23
31
  if (isLoading) {
24
- return <p className="p-4 text-sm text-muted-foreground">Loading list...</p>;
32
+ return <p className="console-list-message">Loading list...</p>;
25
33
  }
26
34
 
27
- if (items.length === 0) {
28
- return <p className="p-4 text-sm text-muted-foreground">No items.</p>;
35
+ if (rows.length === 0) {
36
+ return <p className="console-list-message">No items.</p>;
29
37
  }
30
38
 
31
39
  return (
32
- <ul className="divide-y divide-border">
33
- {items.map((item) => (
34
- <li key={item.itemId} className="flex flex-col gap-1 p-3">
35
- <div className="flex items-center gap-2">
36
- <Badge variant={item.isPr ? 'default' : 'secondary'}>
37
- {item.isPr ? 'PR' : 'Issue'}
38
- </Badge>
39
- <a
40
- href={item.url}
41
- className="font-medium underline-offset-2 hover:underline"
42
- >
43
- {item.title}
44
- </a>
45
- <span className="text-sm text-muted-foreground">
46
- #{item.number}
47
- </span>
48
- </div>
49
- <div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
50
- <span>{item.repo}</span>
51
- {item.story !== '' && <span>story: {item.story}</span>}
52
- <span>{new Date(item.createdAt).toISOString()}</span>
53
- </div>
54
- </li>
55
- ))}
40
+ <ul className="console-list">
41
+ {rows.map((row) =>
42
+ row.kind === 'group-header' ? (
43
+ <li key={`group:${row.story}`} className="console-list-group">
44
+ <ConsoleStoryGroupHeader
45
+ story={row.story}
46
+ count={row.count}
47
+ colorEnum={resolveStoryColorEnum(storyColors, row.story)}
48
+ />
49
+ </li>
50
+ ) : (
51
+ <li key={row.item.itemId} className="console-list-row">
52
+ <ConsoleListItemRow
53
+ item={row.item}
54
+ isActive={row.item.itemId === activeItemId}
55
+ onSelect={onSelectItem}
56
+ />
57
+ </li>
58
+ ),
59
+ )}
56
60
  </ul>
57
61
  );
58
62
  };
@@ -0,0 +1,27 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import {
3
+ consoleMarkdownBodyFixture,
4
+ consoleMermaidBodyFixture,
5
+ } from '../fixtures';
6
+ import { ConsoleMarkdownView } from './ConsoleMarkdownView';
7
+
8
+ const meta: Meta<typeof ConsoleMarkdownView> = {
9
+ title: 'Console/ConsoleMarkdownView',
10
+ component: ConsoleMarkdownView,
11
+ };
12
+
13
+ export default meta;
14
+
15
+ type Story = StoryObj<typeof ConsoleMarkdownView>;
16
+
17
+ export const RichMarkdown: Story = {
18
+ args: { body: consoleMarkdownBodyFixture },
19
+ };
20
+
21
+ export const WithMermaidFence: Story = {
22
+ args: { body: consoleMermaidBodyFixture },
23
+ };
24
+
25
+ export const Empty: Story = {
26
+ args: { body: '' },
27
+ };
@@ -0,0 +1,36 @@
1
+ import { render, waitFor } from '@testing-library/react';
2
+ import { ConsoleMarkdownView } from './ConsoleMarkdownView';
3
+
4
+ jest.mock('../lib/mermaidLoader', () => ({
5
+ renderMermaidToSvg: jest.fn(
6
+ async () => '<svg data-testid="mermaid-svg"></svg>',
7
+ ),
8
+ }));
9
+
10
+ describe('ConsoleMarkdownView', () => {
11
+ it('renders markdown body content', () => {
12
+ const { container } = render(
13
+ <ConsoleMarkdownView body={'## Heading\n\n- bullet'} />,
14
+ );
15
+ expect(container.querySelector('h2')).not.toBeNull();
16
+ expect(container.querySelector('li')).not.toBeNull();
17
+ });
18
+
19
+ it('shows the empty message for a blank body', () => {
20
+ const { getByText } = render(<ConsoleMarkdownView body=" " />);
21
+ expect(getByText('No description provided.')).toBeInTheDocument();
22
+ });
23
+
24
+ it('renders a mermaid fence via the diagram component', async () => {
25
+ const { container } = render(
26
+ <ConsoleMarkdownView
27
+ body={'intro\n\n```mermaid\ngraph TD; A-->B;\n```'}
28
+ />,
29
+ );
30
+ await waitFor(() => {
31
+ expect(
32
+ container.querySelector('.console-mermaid-rendered'),
33
+ ).not.toBeNull();
34
+ });
35
+ });
36
+ });
@@ -0,0 +1,50 @@
1
+ import { useEffect, useMemo, useRef } from 'react';
2
+ import {
3
+ renderMarkdownToSafeHtml,
4
+ splitMarkdownSegments,
5
+ } from '../lib/markdown';
6
+ import { ConsoleMermaidDiagram } from './ConsoleMermaidDiagram';
7
+
8
+ export type ConsoleMarkdownViewProps = {
9
+ body: string;
10
+ };
11
+
12
+ type ConsoleMarkdownHtmlBlockProps = {
13
+ source: string;
14
+ };
15
+
16
+ const ConsoleMarkdownHtmlBlock = ({
17
+ source,
18
+ }: ConsoleMarkdownHtmlBlockProps) => {
19
+ const html = useMemo(() => renderMarkdownToSafeHtml(source), [source]);
20
+ const containerRef = useRef<HTMLDivElement>(null);
21
+
22
+ useEffect(() => {
23
+ const container = containerRef.current;
24
+ if (container !== null) {
25
+ container.innerHTML = html;
26
+ }
27
+ }, [html]);
28
+
29
+ return <div ref={containerRef} className="console-markdown" />;
30
+ };
31
+
32
+ export const ConsoleMarkdownView = ({ body }: ConsoleMarkdownViewProps) => {
33
+ const segments = useMemo(() => splitMarkdownSegments(body), [body]);
34
+
35
+ if (body.trim() === '') {
36
+ return <p className="console-markdown-empty">No description provided.</p>;
37
+ }
38
+
39
+ return (
40
+ <div className="console-markdown-view">
41
+ {segments.map((segment) =>
42
+ segment.kind === 'mermaid' ? (
43
+ <ConsoleMermaidDiagram key={segment.key} code={segment.code} />
44
+ ) : (
45
+ <ConsoleMarkdownHtmlBlock key={segment.key} source={segment.source} />
46
+ ),
47
+ )}
48
+ </div>
49
+ );
50
+ };
@@ -0,0 +1,22 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { consoleMermaidCodeFixture } from '../fixtures';
3
+ import { ConsoleMermaidDiagram } from './ConsoleMermaidDiagram';
4
+
5
+ const meta: Meta<typeof ConsoleMermaidDiagram> = {
6
+ title: 'Console/ConsoleMermaidDiagram',
7
+ component: ConsoleMermaidDiagram,
8
+ };
9
+
10
+ export default meta;
11
+
12
+ type Story = StoryObj<typeof ConsoleMermaidDiagram>;
13
+
14
+ export const SequenceDiagram: Story = {
15
+ args: { code: consoleMermaidCodeFixture },
16
+ };
17
+
18
+ export const FlowDiagram: Story = {
19
+ args: {
20
+ code: 'graph TD;\n Start-->Fetch;\n Fetch-->Render;\n Render-->Done;',
21
+ },
22
+ };
@@ -0,0 +1,38 @@
1
+ import { render, waitFor } from '@testing-library/react';
2
+ import { renderMermaidToSvg } from '../lib/mermaidLoader';
3
+ import { ConsoleMermaidDiagram } from './ConsoleMermaidDiagram';
4
+
5
+ jest.mock('../lib/mermaidLoader', () => ({
6
+ renderMermaidToSvg: jest.fn(),
7
+ }));
8
+
9
+ const mockedRender = renderMermaidToSvg as jest.MockedFunction<
10
+ typeof renderMermaidToSvg
11
+ >;
12
+
13
+ describe('ConsoleMermaidDiagram', () => {
14
+ beforeEach(() => {
15
+ mockedRender.mockReset();
16
+ });
17
+
18
+ it('renders the sanitized svg once ready', async () => {
19
+ mockedRender.mockResolvedValue('<svg id="ok"></svg>');
20
+ const { container } = render(
21
+ <ConsoleMermaidDiagram code="graph TD; A-->B;" />,
22
+ );
23
+ await waitFor(() => {
24
+ expect(container.querySelector('svg#ok')).not.toBeNull();
25
+ });
26
+ });
27
+
28
+ it('shows the source and an error note when rendering fails', async () => {
29
+ mockedRender.mockRejectedValue(new Error('parse error'));
30
+ const { getByText } = render(<ConsoleMermaidDiagram code="bad diagram" />);
31
+ await waitFor(() => {
32
+ expect(
33
+ getByText(/Mermaid render error: parse error/),
34
+ ).toBeInTheDocument();
35
+ });
36
+ expect(getByText('bad diagram')).toBeInTheDocument();
37
+ });
38
+ });
@@ -0,0 +1,65 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { renderMermaidToSvg } from '../lib/mermaidLoader';
3
+
4
+ export type ConsoleMermaidDiagramProps = {
5
+ code: string;
6
+ };
7
+
8
+ type MermaidRenderState =
9
+ | { status: 'loading' }
10
+ | { status: 'ready'; svg: string }
11
+ | { status: 'error'; message: string };
12
+
13
+ export const ConsoleMermaidDiagram = ({ code }: ConsoleMermaidDiagramProps) => {
14
+ const [state, setState] = useState<MermaidRenderState>({ status: 'loading' });
15
+ const containerRef = useRef<HTMLDivElement>(null);
16
+
17
+ useEffect(() => {
18
+ let cancelled = false;
19
+ setState({ status: 'loading' });
20
+ renderMermaidToSvg(code)
21
+ .then((svg) => {
22
+ if (!cancelled) {
23
+ setState({ status: 'ready', svg });
24
+ }
25
+ })
26
+ .catch((cause: unknown) => {
27
+ if (!cancelled) {
28
+ setState({
29
+ status: 'error',
30
+ message: cause instanceof Error ? cause.message : String(cause),
31
+ });
32
+ }
33
+ });
34
+ return () => {
35
+ cancelled = true;
36
+ };
37
+ }, [code]);
38
+
39
+ useEffect(() => {
40
+ const container = containerRef.current;
41
+ if (container === null) {
42
+ return;
43
+ }
44
+ container.innerHTML = state.status === 'ready' ? state.svg : '';
45
+ }, [state]);
46
+
47
+ if (state.status === 'loading') {
48
+ return <div className="console-mermaid-loading">Rendering diagram...</div>;
49
+ }
50
+
51
+ if (state.status === 'error') {
52
+ return (
53
+ <div className="console-mermaid">
54
+ <div className="console-mermaid-error">
55
+ Mermaid render error: {state.message}
56
+ </div>
57
+ <pre className="console-mermaid-source">
58
+ <code>{code}</code>
59
+ </pre>
60
+ </div>
61
+ );
62
+ }
63
+
64
+ return <div ref={containerRef} className="console-mermaid-rendered" />;
65
+ };