github-issue-tower-defence-management 1.89.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 (160) hide show
  1. package/.github/CODEOWNERS +1 -2
  2. package/CHANGELOG.md +14 -0
  3. package/README.md +15 -2
  4. package/bin/adapter/entry-points/cli/index.js +16 -12
  5. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  6. package/bin/adapter/entry-points/cli/projectConfig.js +2 -0
  7. package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
  8. package/bin/adapter/entry-points/console/consoleOperationApi.js +54 -27
  9. package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
  10. package/bin/adapter/entry-points/console/consoleProjectResolver.js +38 -0
  11. package/bin/adapter/entry-points/console/consoleProjectResolver.js.map +1 -0
  12. package/bin/adapter/entry-points/console/consoleServer.js +43 -17
  13. package/bin/adapter/entry-points/console/consoleServer.js.map +1 -1
  14. package/bin/adapter/entry-points/console/ui-dist/assets/index-BU6p3cGU.css +1 -0
  15. package/bin/adapter/entry-points/console/ui-dist/assets/index-BvuSQN9s.js +100 -0
  16. package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
  17. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +16 -0
  18. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  19. package/jest.config.js +57 -9
  20. package/package.json +17 -13
  21. package/src/adapter/entry-points/cli/index.test.ts +12 -2
  22. package/src/adapter/entry-points/cli/index.ts +30 -12
  23. package/src/adapter/entry-points/cli/projectConfig.ts +3 -0
  24. package/src/adapter/entry-points/console/consoleOperationApi.test.ts +129 -15
  25. package/src/adapter/entry-points/console/consoleOperationApi.ts +83 -28
  26. package/src/adapter/entry-points/console/consoleProjectResolver.test.ts +96 -0
  27. package/src/adapter/entry-points/console/consoleProjectResolver.ts +50 -0
  28. package/src/adapter/entry-points/console/consoleServer.test.ts +86 -4
  29. package/src/adapter/entry-points/console/consoleServer.ts +53 -23
  30. package/src/adapter/entry-points/console/ui/jest.setup.ts +1 -0
  31. package/src/adapter/entry-points/console/ui/jest.styleMock.js +1 -0
  32. package/src/adapter/entry-points/console/ui/src/features/console/colors.test.ts +34 -0
  33. package/src/adapter/entry-points/console/ui/src/features/console/colors.ts +73 -0
  34. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleChangedFileList.stories.tsx +28 -0
  35. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleChangedFileList.test.tsx +42 -0
  36. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleChangedFileList.tsx +55 -0
  37. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCloseButtonGroup.stories.tsx +14 -0
  38. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCloseButtonGroup.test.tsx +23 -0
  39. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCloseButtonGroup.tsx +26 -0
  40. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommentList.stories.tsx +29 -0
  41. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommentList.test.tsx +55 -0
  42. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommentList.tsx +66 -0
  43. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommitList.stories.tsx +25 -0
  44. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommitList.test.tsx +53 -0
  45. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommitList.tsx +53 -0
  46. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemDetail.stories.tsx +79 -0
  47. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemDetail.test.tsx +81 -0
  48. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemDetail.tsx +226 -0
  49. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemIcon.stories.tsx +82 -0
  50. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemIcon.test.tsx +52 -0
  51. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemIcon.tsx +32 -0
  52. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListItemRow.stories.tsx +25 -0
  53. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListItemRow.test.tsx +43 -0
  54. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListItemRow.tsx +34 -0
  55. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.stories.tsx +22 -6
  56. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.test.tsx +87 -0
  57. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.tsx +36 -32
  58. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMarkdownView.stories.tsx +27 -0
  59. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMarkdownView.test.tsx +36 -0
  60. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMarkdownView.tsx +50 -0
  61. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMermaidDiagram.stories.tsx +22 -0
  62. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMermaidDiagram.test.tsx +38 -0
  63. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMermaidDiagram.tsx +65 -0
  64. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleNextActionDateGroup.stories.tsx +20 -0
  65. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleNextActionDateGroup.test.tsx +42 -0
  66. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleNextActionDateGroup.tsx +28 -0
  67. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleOperationBar.stories.tsx +55 -0
  68. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleOperationBar.test.tsx +85 -0
  69. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleOperationBar.tsx +55 -0
  70. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePanel.stories.tsx +26 -0
  71. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePanel.test.tsx +32 -0
  72. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePanel.tsx +36 -0
  73. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleProjectHeader.stories.tsx +29 -0
  74. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleProjectHeader.test.tsx +14 -0
  75. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleProjectHeader.tsx +14 -0
  76. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestReviewGroup.stories.tsx +14 -0
  77. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestReviewGroup.test.tsx +33 -0
  78. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestReviewGroup.tsx +34 -0
  79. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestSection.stories.tsx +31 -0
  80. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestSection.test.tsx +40 -0
  81. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestSection.tsx +88 -0
  82. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStatusButtonGroup.stories.tsx +17 -0
  83. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStatusButtonGroup.test.tsx +49 -0
  84. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStatusButtonGroup.tsx +63 -0
  85. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryButtonGroup.stories.tsx +17 -0
  86. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryButtonGroup.test.tsx +45 -0
  87. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryButtonGroup.tsx +42 -0
  88. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryGroupHeader.stories.tsx +27 -0
  89. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryGroupHeader.test.tsx +24 -0
  90. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryGroupHeader.tsx +28 -0
  91. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.stories.tsx +41 -5
  92. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.test.tsx +59 -0
  93. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.tsx +28 -19
  94. package/src/adapter/entry-points/console/ui/src/features/console/fileStatus.test.ts +35 -0
  95. package/src/adapter/entry-points/console/ui/src/features/console/fileStatus.ts +21 -0
  96. package/src/adapter/entry-points/console/ui/src/features/console/fixtures.ts +206 -9
  97. package/src/adapter/entry-points/console/ui/src/features/console/grouping.test.ts +91 -0
  98. package/src/adapter/entry-points/console/ui/src/features/console/grouping.ts +79 -0
  99. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleCaches.test.ts +22 -0
  100. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleCaches.ts +42 -0
  101. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleItemDetailData.test.ts +126 -0
  102. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleItemDetailData.ts +167 -0
  103. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.test.ts +198 -0
  104. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.ts +243 -0
  105. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOverlay.test.ts +40 -0
  106. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOverlay.ts +71 -0
  107. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsolePjcode.test.ts +24 -0
  108. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsolePjcode.ts +17 -0
  109. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleResource.test.ts +41 -0
  110. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleResource.ts +57 -0
  111. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.test.ts +63 -0
  112. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.ts +129 -0
  113. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleToken.test.ts +41 -0
  114. package/src/adapter/entry-points/console/ui/src/features/console/itemIcons.test.ts +97 -0
  115. package/src/adapter/entry-points/console/ui/src/features/console/itemIcons.ts +95 -0
  116. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +155 -0
  117. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +187 -0
  118. package/src/adapter/entry-points/console/ui/src/features/console/lib/markdown.test.ts +76 -0
  119. package/src/adapter/entry-points/console/ui/src/features/console/lib/markdown.ts +73 -0
  120. package/src/adapter/entry-points/console/ui/src/features/console/lib/mermaidLoader.test.ts +27 -0
  121. package/src/adapter/entry-points/console/ui/src/features/console/lib/mermaidLoader.ts +71 -0
  122. package/src/adapter/entry-points/console/ui/src/features/console/lib/resourceCache.test.ts +56 -0
  123. package/src/adapter/entry-points/console/ui/src/features/console/lib/resourceCache.ts +51 -0
  124. package/src/adapter/entry-points/console/ui/src/features/console/operations.test.ts +37 -0
  125. package/src/adapter/entry-points/console/ui/src/features/console/operations.ts +35 -0
  126. package/src/adapter/entry-points/console/ui/src/features/console/overlay.test.ts +124 -0
  127. package/src/adapter/entry-points/console/ui/src/features/console/overlay.ts +101 -0
  128. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.test.tsx +79 -0
  129. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +109 -0
  130. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.test.tsx +74 -0
  131. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +137 -7
  132. package/src/adapter/entry-points/console/ui/src/features/console/relativeTime.test.ts +52 -0
  133. package/src/adapter/entry-points/console/ui/src/features/console/relativeTime.ts +51 -0
  134. package/src/adapter/entry-points/console/ui/src/features/console/types.ts +73 -1
  135. package/src/adapter/entry-points/console/ui/src/index.css +352 -2
  136. package/src/adapter/entry-points/console/ui/tsconfig.json +3 -1
  137. package/src/adapter/entry-points/console/ui/vite.config.ts +6 -1
  138. package/src/adapter/entry-points/console/ui-dist/assets/index-BU6p3cGU.css +1 -0
  139. package/src/adapter/entry-points/console/ui-dist/assets/index-BvuSQN9s.js +100 -0
  140. package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
  141. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +25 -0
  142. package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +4 -0
  143. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  144. package/types/adapter/entry-points/cli/projectConfig.d.ts +1 -0
  145. package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
  146. package/types/adapter/entry-points/console/consoleOperationApi.d.ts +6 -2
  147. package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
  148. package/types/adapter/entry-points/console/consoleProjectResolver.d.ts +6 -0
  149. package/types/adapter/entry-points/console/consoleProjectResolver.d.ts.map +1 -0
  150. package/types/adapter/entry-points/console/consoleServer.d.ts +3 -3
  151. package/types/adapter/entry-points/console/consoleServer.d.ts.map +1 -1
  152. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +1 -0
  153. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  154. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +1 -0
  155. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
  156. package/bin/adapter/entry-points/console/ui-dist/assets/index-DFxrSRH4.css +0 -1
  157. package/bin/adapter/entry-points/console/ui-dist/assets/index-DcOZ02ON.js +0 -49
  158. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleList.ts +0 -65
  159. package/src/adapter/entry-points/console/ui-dist/assets/index-DFxrSRH4.css +0 -1
  160. package/src/adapter/entry-points/console/ui-dist/assets/index-DcOZ02ON.js +0 -49
@@ -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
+ };
@@ -0,0 +1,20 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { ConsoleNextActionDateGroup } from './ConsoleNextActionDateGroup';
3
+
4
+ const meta: Meta<typeof ConsoleNextActionDateGroup> = {
5
+ title: 'Console/ConsoleNextActionDateGroup',
6
+ component: ConsoleNextActionDateGroup,
7
+ args: { onSetNextActionDate: () => {} },
8
+ };
9
+
10
+ export default meta;
11
+
12
+ type Story = StoryObj<typeof ConsoleNextActionDateGroup>;
13
+
14
+ export const StandardTab: Story = {
15
+ args: { isTodoByHuman: false },
16
+ };
17
+
18
+ export const TodoByHumanTab: Story = {
19
+ args: { isTodoByHuman: true },
20
+ };
@@ -0,0 +1,42 @@
1
+ import { fireEvent, render } from '@testing-library/react';
2
+ import { ConsoleNextActionDateGroup } from './ConsoleNextActionDateGroup';
3
+
4
+ describe('ConsoleNextActionDateGroup', () => {
5
+ it('shows +1 day and +1 week outside the todo-by-human tab', () => {
6
+ const { getByText, queryByText } = render(
7
+ <ConsoleNextActionDateGroup
8
+ isTodoByHuman={false}
9
+ onSetNextActionDate={() => {}}
10
+ />,
11
+ );
12
+ expect(getByText('+1 day')).toBeInTheDocument();
13
+ expect(getByText('+1 week')).toBeInTheDocument();
14
+ expect(queryByText('+1 week and skip')).toBeNull();
15
+ });
16
+
17
+ it('shows +1 week and skip on the todo-by-human tab', () => {
18
+ const { getByText } = render(
19
+ <ConsoleNextActionDateGroup
20
+ isTodoByHuman
21
+ onSetNextActionDate={() => {}}
22
+ />,
23
+ );
24
+ expect(getByText('+1 week and skip')).toBeInTheDocument();
25
+ });
26
+
27
+ it('reports the snooze actions', () => {
28
+ const onSetNextActionDate = jest.fn();
29
+ const { getByText } = render(
30
+ <ConsoleNextActionDateGroup
31
+ isTodoByHuman={false}
32
+ onSetNextActionDate={onSetNextActionDate}
33
+ />,
34
+ );
35
+ fireEvent.click(getByText('+1 day'));
36
+ fireEvent.click(getByText('+1 week'));
37
+ expect(onSetNextActionDate.mock.calls.map((call) => call[0])).toEqual([
38
+ 'snooze_1day',
39
+ 'snooze_1week',
40
+ ]);
41
+ });
42
+ });
@@ -0,0 +1,28 @@
1
+ import type { ConsoleNextActionDateAction } from '../operations';
2
+
3
+ export type ConsoleNextActionDateGroupProps = {
4
+ isTodoByHuman: boolean;
5
+ onSetNextActionDate: (action: ConsoleNextActionDateAction) => void;
6
+ };
7
+
8
+ export const ConsoleNextActionDateGroup = ({
9
+ isTodoByHuman,
10
+ onSetNextActionDate,
11
+ }: ConsoleNextActionDateGroupProps) => (
12
+ <div className="console-op-group">
13
+ <button
14
+ type="button"
15
+ className="console-op-button"
16
+ onClick={() => onSetNextActionDate('snooze_1day')}
17
+ >
18
+ +1 day
19
+ </button>
20
+ <button
21
+ type="button"
22
+ className="console-op-button"
23
+ onClick={() => onSetNextActionDate('snooze_1week')}
24
+ >
25
+ {isTodoByHuman ? '+1 week and skip' : '+1 week'}
26
+ </button>
27
+ </div>
28
+ );
@@ -0,0 +1,55 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import {
3
+ consoleListItemsFixture,
4
+ consoleStatusOptionsFixture,
5
+ consoleStoryOptionsFixture,
6
+ } from '../fixtures';
7
+ import type { ConsoleOperationHandlers } from '../operations';
8
+ import { ConsoleOperationBar } from './ConsoleOperationBar';
9
+
10
+ const handlers: ConsoleOperationHandlers = {
11
+ onReview: () => {},
12
+ onSetNextActionDate: () => {},
13
+ onSetStory: () => {},
14
+ onSetStatus: () => {},
15
+ onSetInTmuxByHuman: () => {},
16
+ onClose: () => {},
17
+ };
18
+
19
+ const meta: Meta<typeof ConsoleOperationBar> = {
20
+ title: 'Console/ConsoleOperationBar',
21
+ component: ConsoleOperationBar,
22
+ args: {
23
+ statusOptions: consoleStatusOptionsFixture,
24
+ storyOptions: consoleStoryOptionsFixture,
25
+ handlers,
26
+ },
27
+ };
28
+
29
+ export default meta;
30
+
31
+ type Story = StoryObj<typeof ConsoleOperationBar>;
32
+
33
+ export const PrsTabPullRequest: Story = {
34
+ args: {
35
+ tab: 'prs',
36
+ item: consoleListItemsFixture[0],
37
+ hasPullRequest: true,
38
+ },
39
+ };
40
+
41
+ export const TriageTabIssueWithStoryGroup: Story = {
42
+ args: {
43
+ tab: 'triage',
44
+ item: consoleListItemsFixture[2],
45
+ hasPullRequest: false,
46
+ },
47
+ };
48
+
49
+ export const TodoByHumanTabIssue: Story = {
50
+ args: {
51
+ tab: 'todo-by-human',
52
+ item: consoleListItemsFixture[2],
53
+ hasPullRequest: false,
54
+ },
55
+ };
@@ -0,0 +1,85 @@
1
+ import { render } from '@testing-library/react';
2
+ import {
3
+ consoleListItemsFixture,
4
+ consoleStatusOptionsFixture,
5
+ consoleStoryOptionsFixture,
6
+ } from '../fixtures';
7
+ import type { ConsoleOperationHandlers } from '../operations';
8
+ import { ConsoleOperationBar } from './ConsoleOperationBar';
9
+
10
+ const handlers: ConsoleOperationHandlers = {
11
+ onReview: jest.fn(),
12
+ onSetNextActionDate: jest.fn(),
13
+ onSetStory: jest.fn(),
14
+ onSetStatus: jest.fn(),
15
+ onSetInTmuxByHuman: jest.fn(),
16
+ onClose: jest.fn(),
17
+ };
18
+
19
+ const prItem = consoleListItemsFixture[0];
20
+ const issueItem = consoleListItemsFixture[2];
21
+
22
+ describe('ConsoleOperationBar', () => {
23
+ it('shows the review group for a PR and hides story and close groups outside triage', () => {
24
+ const { getByText, queryByText } = render(
25
+ <ConsoleOperationBar
26
+ tab="prs"
27
+ item={prItem}
28
+ hasPullRequest
29
+ statusOptions={consoleStatusOptionsFixture}
30
+ storyOptions={consoleStoryOptionsFixture}
31
+ handlers={handlers}
32
+ />,
33
+ );
34
+ expect(getByText('Approve')).toBeInTheDocument();
35
+ expect(getByText('+1 day')).toBeInTheDocument();
36
+ expect(getByText('Awaiting Workspace')).toBeInTheDocument();
37
+ expect(queryByText('Close')).toBeNull();
38
+ expect(queryByText('TDPM Console port')).toBeNull();
39
+ });
40
+
41
+ it('shows the story group on the triage tab and the close group for an issue', () => {
42
+ const { getByText } = render(
43
+ <ConsoleOperationBar
44
+ tab="triage"
45
+ item={issueItem}
46
+ hasPullRequest={false}
47
+ statusOptions={consoleStatusOptionsFixture}
48
+ storyOptions={consoleStoryOptionsFixture}
49
+ handlers={handlers}
50
+ />,
51
+ );
52
+ expect(getByText('Move to Okinawa')).toBeInTheDocument();
53
+ expect(getByText('Close')).toBeInTheDocument();
54
+ expect(getByText('Close as not planned')).toBeInTheDocument();
55
+ });
56
+
57
+ it('shows +1 week and skip on the todo-by-human tab', () => {
58
+ const { getByText } = render(
59
+ <ConsoleOperationBar
60
+ tab="todo-by-human"
61
+ item={issueItem}
62
+ hasPullRequest={false}
63
+ statusOptions={consoleStatusOptionsFixture}
64
+ storyOptions={consoleStoryOptionsFixture}
65
+ handlers={handlers}
66
+ />,
67
+ );
68
+ expect(getByText('+1 week and skip')).toBeInTheDocument();
69
+ });
70
+
71
+ it('hides the review group when there is no pull request', () => {
72
+ const { queryByText } = render(
73
+ <ConsoleOperationBar
74
+ tab="unread"
75
+ item={issueItem}
76
+ hasPullRequest={false}
77
+ statusOptions={consoleStatusOptionsFixture}
78
+ storyOptions={consoleStoryOptionsFixture}
79
+ handlers={handlers}
80
+ />,
81
+ );
82
+ expect(queryByText('Approve')).toBeNull();
83
+ expect(queryByText('Close')).not.toBeNull();
84
+ });
85
+ });
@@ -0,0 +1,55 @@
1
+ import { type ConsoleOperationHandlers, isTodoByHumanTab } from '../operations';
2
+ import type {
3
+ ConsoleFieldOption,
4
+ ConsoleListItem,
5
+ ConsoleTabName,
6
+ } from '../types';
7
+ import { ConsoleCloseButtonGroup } from './ConsoleCloseButtonGroup';
8
+ import { ConsoleNextActionDateGroup } from './ConsoleNextActionDateGroup';
9
+ import { ConsolePullRequestReviewGroup } from './ConsolePullRequestReviewGroup';
10
+ import { ConsoleStatusButtonGroup } from './ConsoleStatusButtonGroup';
11
+ import { ConsoleStoryButtonGroup } from './ConsoleStoryButtonGroup';
12
+
13
+ export type ConsoleOperationBarProps = {
14
+ tab: ConsoleTabName;
15
+ item: ConsoleListItem;
16
+ hasPullRequest: boolean;
17
+ statusOptions: ConsoleFieldOption[];
18
+ storyOptions: ConsoleFieldOption[];
19
+ handlers: ConsoleOperationHandlers;
20
+ };
21
+
22
+ export const ConsoleOperationBar = ({
23
+ tab,
24
+ item,
25
+ hasPullRequest,
26
+ statusOptions,
27
+ storyOptions,
28
+ handlers,
29
+ }: ConsoleOperationBarProps) => {
30
+ const showStory = tab === 'triage';
31
+ const showClose = !item.isPr;
32
+ return (
33
+ <div className="console-operation-bar">
34
+ {hasPullRequest && (
35
+ <ConsolePullRequestReviewGroup onReview={handlers.onReview} />
36
+ )}
37
+ <ConsoleNextActionDateGroup
38
+ isTodoByHuman={isTodoByHumanTab(tab)}
39
+ onSetNextActionDate={handlers.onSetNextActionDate}
40
+ />
41
+ {showStory && (
42
+ <ConsoleStoryButtonGroup
43
+ storyOptions={storyOptions}
44
+ onSetStory={handlers.onSetStory}
45
+ />
46
+ )}
47
+ <ConsoleStatusButtonGroup
48
+ statusOptions={statusOptions}
49
+ onSetStatus={handlers.onSetStatus}
50
+ onSetInTmuxByHuman={handlers.onSetInTmuxByHuman}
51
+ />
52
+ {showClose && <ConsoleCloseButtonGroup onClose={handlers.onClose} />}
53
+ </div>
54
+ );
55
+ };
@@ -0,0 +1,26 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { ConsolePanel } from './ConsolePanel';
3
+
4
+ const meta: Meta<typeof ConsolePanel> = {
5
+ title: 'Console/ConsolePanel',
6
+ component: ConsolePanel,
7
+ };
8
+
9
+ export default meta;
10
+
11
+ type Story = StoryObj<typeof ConsolePanel>;
12
+
13
+ export const Expanded: Story = {
14
+ args: {
15
+ title: 'Description',
16
+ children: <p style={{ padding: 12 }}>Panel body content</p>,
17
+ },
18
+ };
19
+
20
+ export const Collapsed: Story = {
21
+ args: {
22
+ title: 'Commits',
23
+ defaultCollapsed: true,
24
+ children: <p style={{ padding: 12 }}>Panel body content</p>,
25
+ },
26
+ };