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,49 @@
1
+ import { fireEvent, render } from '@testing-library/react';
2
+ import { consoleStatusOptionsFixture } from '../fixtures';
3
+ import { ConsoleStatusButtonGroup } from './ConsoleStatusButtonGroup';
4
+
5
+ describe('ConsoleStatusButtonGroup', () => {
6
+ it('renders only the four routed status buttons that exist in the data', () => {
7
+ const { getByText, queryByText } = render(
8
+ <ConsoleStatusButtonGroup
9
+ statusOptions={consoleStatusOptionsFixture}
10
+ onSetStatus={() => {}}
11
+ onSetInTmuxByHuman={() => {}}
12
+ />,
13
+ );
14
+ expect(getByText('In Tmux by agent')).toBeInTheDocument();
15
+ expect(getByText('In Tmux by human')).toBeInTheDocument();
16
+ expect(getByText('Todo by human')).toBeInTheDocument();
17
+ expect(getByText('Awaiting Workspace')).toBeInTheDocument();
18
+ expect(queryByText('Preparation')).toBeNull();
19
+ });
20
+
21
+ it('routes In Tmux by human to the intmux handler and others to the status handler', () => {
22
+ const onSetStatus = jest.fn();
23
+ const onSetInTmuxByHuman = jest.fn();
24
+ const { getByText } = render(
25
+ <ConsoleStatusButtonGroup
26
+ statusOptions={consoleStatusOptionsFixture}
27
+ onSetStatus={onSetStatus}
28
+ onSetInTmuxByHuman={onSetInTmuxByHuman}
29
+ />,
30
+ );
31
+ fireEvent.click(getByText('In Tmux by human'));
32
+ fireEvent.click(getByText('In Tmux by agent'));
33
+ expect(onSetInTmuxByHuman).toHaveBeenCalledTimes(1);
34
+ expect(onSetInTmuxByHuman.mock.calls[0][0].name).toBe('In Tmux by human');
35
+ expect(onSetStatus).toHaveBeenCalledTimes(1);
36
+ expect(onSetStatus.mock.calls[0][0].name).toBe('In Tmux by agent');
37
+ });
38
+
39
+ it('renders nothing when no routed option exists', () => {
40
+ const { container } = render(
41
+ <ConsoleStatusButtonGroup
42
+ statusOptions={[{ id: 'x', name: 'Preparation', color: 'YELLOW' }]}
43
+ onSetStatus={() => {}}
44
+ onSetInTmuxByHuman={() => {}}
45
+ />,
46
+ );
47
+ expect(container.firstChild).toBeNull();
48
+ });
49
+ });
@@ -0,0 +1,63 @@
1
+ import { colorFromEnum } from '../colors';
2
+ import { IN_TMUX_BY_HUMAN_NAME, STATUS_BUTTON_NAMES } from '../operations';
3
+ import type { ConsoleFieldOption } from '../types';
4
+
5
+ export type ConsoleStatusButtonGroupProps = {
6
+ statusOptions: ConsoleFieldOption[];
7
+ onSetStatus: (option: ConsoleFieldOption) => void;
8
+ onSetInTmuxByHuman: (option: ConsoleFieldOption) => void;
9
+ };
10
+
11
+ const findStatusOption = (
12
+ statusOptions: ConsoleFieldOption[],
13
+ name: string,
14
+ ): ConsoleFieldOption | null => {
15
+ const lower = name.toLowerCase();
16
+ return (
17
+ statusOptions.find((option) => option.name.toLowerCase() === lower) ?? null
18
+ );
19
+ };
20
+
21
+ export const ConsoleStatusButtonGroup = ({
22
+ statusOptions,
23
+ onSetStatus,
24
+ onSetInTmuxByHuman,
25
+ }: ConsoleStatusButtonGroupProps) => {
26
+ const buttons = STATUS_BUTTON_NAMES.map((name) => ({
27
+ name,
28
+ option: findStatusOption(statusOptions, name),
29
+ })).filter(
30
+ (entry): entry is { name: string; option: ConsoleFieldOption } =>
31
+ entry.option !== null,
32
+ );
33
+
34
+ if (buttons.length === 0) {
35
+ return null;
36
+ }
37
+
38
+ return (
39
+ <div className="console-op-group">
40
+ {buttons.map(({ name, option }) => {
41
+ const palette = colorFromEnum(option.color);
42
+ const isInTmuxByHuman = name === IN_TMUX_BY_HUMAN_NAME;
43
+ return (
44
+ <button
45
+ key={option.id}
46
+ type="button"
47
+ className="console-op-button"
48
+ style={{
49
+ color: palette.fg,
50
+ borderColor: palette.border,
51
+ backgroundColor: palette.bg,
52
+ }}
53
+ onClick={() =>
54
+ isInTmuxByHuman ? onSetInTmuxByHuman(option) : onSetStatus(option)
55
+ }
56
+ >
57
+ {option.name}
58
+ </button>
59
+ );
60
+ })}
61
+ </div>
62
+ );
63
+ };
@@ -0,0 +1,17 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { consoleStoryOptionsFixture } from '../fixtures';
3
+ import { ConsoleStoryButtonGroup } from './ConsoleStoryButtonGroup';
4
+
5
+ const meta: Meta<typeof ConsoleStoryButtonGroup> = {
6
+ title: 'Console/ConsoleStoryButtonGroup',
7
+ component: ConsoleStoryButtonGroup,
8
+ args: { onSetStory: () => {} },
9
+ };
10
+
11
+ export default meta;
12
+
13
+ type Story = StoryObj<typeof ConsoleStoryButtonGroup>;
14
+
15
+ export const AssignableStories: Story = {
16
+ args: { storyOptions: consoleStoryOptionsFixture },
17
+ };
@@ -0,0 +1,45 @@
1
+ import { fireEvent, render } from '@testing-library/react';
2
+ import { ConsoleStoryButtonGroup } from './ConsoleStoryButtonGroup';
3
+
4
+ const options = [
5
+ { id: '1', name: 'TDPM Console port', color: 'BLUE' as const },
6
+ {
7
+ id: '2',
8
+ name: "regular / NO STORY; DON'T WORK ON THIS",
9
+ color: 'RED' as const,
10
+ },
11
+ { id: '3', name: 'Move to Okinawa', color: 'PURPLE' as const },
12
+ ];
13
+
14
+ describe('ConsoleStoryButtonGroup', () => {
15
+ it('excludes no-story options', () => {
16
+ const { getByText, queryByText } = render(
17
+ <ConsoleStoryButtonGroup storyOptions={options} onSetStory={() => {}} />,
18
+ );
19
+ expect(getByText('TDPM Console port')).toBeInTheDocument();
20
+ expect(getByText('Move to Okinawa')).toBeInTheDocument();
21
+ expect(queryByText(/NO STORY/)).toBeNull();
22
+ });
23
+
24
+ it('reports the selected story option', () => {
25
+ const onSetStory = jest.fn();
26
+ const { getByText } = render(
27
+ <ConsoleStoryButtonGroup
28
+ storyOptions={options}
29
+ onSetStory={onSetStory}
30
+ />,
31
+ );
32
+ fireEvent.click(getByText('Move to Okinawa'));
33
+ expect(onSetStory).toHaveBeenCalledWith(options[2]);
34
+ });
35
+
36
+ it('renders nothing when only no-story options exist', () => {
37
+ const { container } = render(
38
+ <ConsoleStoryButtonGroup
39
+ storyOptions={[options[1]]}
40
+ onSetStory={() => {}}
41
+ />,
42
+ );
43
+ expect(container.firstChild).toBeNull();
44
+ });
45
+ });
@@ -0,0 +1,42 @@
1
+ import { colorFromEnum } from '../colors';
2
+ import type { ConsoleFieldOption } from '../types';
3
+
4
+ export type ConsoleStoryButtonGroupProps = {
5
+ storyOptions: ConsoleFieldOption[];
6
+ onSetStory: (option: ConsoleFieldOption) => void;
7
+ };
8
+
9
+ const isNoStoryOption = (option: ConsoleFieldOption): boolean =>
10
+ option.name.toLowerCase().includes('no story');
11
+
12
+ export const ConsoleStoryButtonGroup = ({
13
+ storyOptions,
14
+ onSetStory,
15
+ }: ConsoleStoryButtonGroupProps) => {
16
+ const assignable = storyOptions.filter((option) => !isNoStoryOption(option));
17
+ if (assignable.length === 0) {
18
+ return null;
19
+ }
20
+ return (
21
+ <div className="console-op-group console-op-group-stories">
22
+ {assignable.map((option) => {
23
+ const palette = colorFromEnum(option.color);
24
+ return (
25
+ <button
26
+ key={option.id}
27
+ type="button"
28
+ className="console-op-button"
29
+ style={{
30
+ color: palette.fg,
31
+ borderColor: palette.border,
32
+ backgroundColor: palette.bg,
33
+ }}
34
+ onClick={() => onSetStory(option)}
35
+ >
36
+ {option.name}
37
+ </button>
38
+ );
39
+ })}
40
+ </div>
41
+ );
42
+ };
@@ -0,0 +1,27 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { ConsoleStoryGroupHeader } from './ConsoleStoryGroupHeader';
3
+
4
+ const meta: Meta<typeof ConsoleStoryGroupHeader> = {
5
+ title: 'Console/ConsoleStoryGroupHeader',
6
+ component: ConsoleStoryGroupHeader,
7
+ };
8
+
9
+ export default meta;
10
+
11
+ type Story = StoryObj<typeof ConsoleStoryGroupHeader>;
12
+
13
+ export const ConsolePortStory: Story = {
14
+ args: { story: 'TDPM Console port', count: 4, colorEnum: 'BLUE' },
15
+ };
16
+
17
+ export const RegularWorkflowStory: Story = {
18
+ args: {
19
+ story: 'regular / workflow improvement',
20
+ count: 12,
21
+ colorEnum: 'GRAY',
22
+ },
23
+ };
24
+
25
+ export const NoStory: Story = {
26
+ args: { story: '(No story)', count: 3, colorEnum: null },
27
+ };
@@ -0,0 +1,24 @@
1
+ import { render } from '@testing-library/react';
2
+ import { ConsoleStoryGroupHeader } from './ConsoleStoryGroupHeader';
3
+
4
+ describe('ConsoleStoryGroupHeader', () => {
5
+ it('renders the story name and count', () => {
6
+ const { getByText } = render(
7
+ <ConsoleStoryGroupHeader
8
+ story="TDPM Console port"
9
+ count={4}
10
+ colorEnum="BLUE"
11
+ />,
12
+ );
13
+ expect(getByText('TDPM Console port')).toBeInTheDocument();
14
+ expect(getByText('4')).toBeInTheDocument();
15
+ });
16
+
17
+ it('applies the dot color from the enum', () => {
18
+ const { container } = render(
19
+ <ConsoleStoryGroupHeader story="s" count={1} colorEnum="GREEN" />,
20
+ );
21
+ const dot = container.querySelector('.console-story-dot');
22
+ expect(dot).toHaveStyle({ backgroundColor: '#3fb950' });
23
+ });
24
+ });
@@ -0,0 +1,28 @@
1
+ import { colorFromEnum } from '../colors';
2
+ import type { ConsoleColor } from '../types';
3
+
4
+ export type ConsoleStoryGroupHeaderProps = {
5
+ story: string;
6
+ count: number;
7
+ colorEnum: ConsoleColor | null;
8
+ };
9
+
10
+ export const ConsoleStoryGroupHeader = ({
11
+ story,
12
+ count,
13
+ colorEnum,
14
+ }: ConsoleStoryGroupHeaderProps) => {
15
+ const palette = colorFromEnum(colorEnum);
16
+ return (
17
+ <div className="console-group-header">
18
+ <span className="console-storytag">
19
+ <span
20
+ className="console-story-dot"
21
+ style={{ backgroundColor: palette.dot }}
22
+ />
23
+ {story}
24
+ </span>
25
+ <span className="console-group-count">{count}</span>
26
+ </div>
27
+ );
28
+ };
@@ -12,23 +12,59 @@ export default meta;
12
12
 
13
13
  type Story = StoryObj<typeof ConsoleTabBar>;
14
14
 
15
- export const PrsActive: Story = {
15
+ const counts: Record<ConsoleTabName, number> = {
16
+ prs: 35,
17
+ triage: 12,
18
+ unread: 7,
19
+ 'failed-preparation': 2,
20
+ 'todo-by-human': 4,
21
+ };
22
+
23
+ export const AllTabsWithCounts: Story = {
24
+ args: {
25
+ activeTab: 'prs',
26
+ counts,
27
+ onSelectTab: () => {},
28
+ },
29
+ };
30
+
31
+ export const ZeroCountTabsHidden: Story = {
16
32
  args: {
17
33
  activeTab: 'prs',
18
- onSelectTab: () => undefined,
34
+ counts: {
35
+ prs: 35,
36
+ triage: 0,
37
+ unread: 7,
38
+ 'failed-preparation': 0,
39
+ 'todo-by-human': 0,
40
+ },
41
+ onSelectTab: () => {},
19
42
  },
20
43
  };
21
44
 
22
- export const TriageActive: Story = {
45
+ export const ActiveZeroCountTabStaysVisible: Story = {
23
46
  args: {
24
47
  activeTab: 'triage',
25
- onSelectTab: () => undefined,
48
+ counts: {
49
+ prs: 35,
50
+ triage: 0,
51
+ unread: 7,
52
+ 'failed-preparation': 0,
53
+ 'todo-by-human': 0,
54
+ },
55
+ onSelectTab: () => {},
26
56
  },
27
57
  };
28
58
 
29
59
  export const Interactive: Story = {
30
60
  render: () => {
31
61
  const [activeTab, setActiveTab] = useState<ConsoleTabName>('prs');
32
- return <ConsoleTabBar activeTab={activeTab} onSelectTab={setActiveTab} />;
62
+ return (
63
+ <ConsoleTabBar
64
+ activeTab={activeTab}
65
+ counts={counts}
66
+ onSelectTab={setActiveTab}
67
+ />
68
+ );
33
69
  },
34
70
  };
@@ -0,0 +1,59 @@
1
+ import { fireEvent, render } from '@testing-library/react';
2
+ import type { ConsoleTabName } from '../types';
3
+ import { ConsoleTabBar } from './ConsoleTabBar';
4
+
5
+ const counts: Record<ConsoleTabName, number> = {
6
+ prs: 3,
7
+ triage: 0,
8
+ unread: 5,
9
+ 'failed-preparation': 0,
10
+ 'todo-by-human': 2,
11
+ };
12
+
13
+ describe('ConsoleTabBar', () => {
14
+ it('hides zero-count tabs except the active tab', () => {
15
+ const { queryByText } = render(
16
+ <ConsoleTabBar activeTab="prs" counts={counts} onSelectTab={() => {}} />,
17
+ );
18
+ expect(queryByText('Awaiting Quality Check')).not.toBeNull();
19
+ expect(queryByText('Unread')).not.toBeNull();
20
+ expect(queryByText('Todo by human')).not.toBeNull();
21
+ expect(queryByText('Triage')).toBeNull();
22
+ expect(queryByText('Failed Preparation')).toBeNull();
23
+ });
24
+
25
+ it('keeps a zero-count active tab visible', () => {
26
+ const { queryByText } = render(
27
+ <ConsoleTabBar
28
+ activeTab="triage"
29
+ counts={counts}
30
+ onSelectTab={() => {}}
31
+ />,
32
+ );
33
+ expect(queryByText('Triage')).not.toBeNull();
34
+ });
35
+
36
+ it('uses the exact lowercase Todo by human label', () => {
37
+ const { getByText } = render(
38
+ <ConsoleTabBar
39
+ activeTab="todo-by-human"
40
+ counts={counts}
41
+ onSelectTab={() => {}}
42
+ />,
43
+ );
44
+ expect(getByText('Todo by human')).toBeInTheDocument();
45
+ });
46
+
47
+ it('reports the selected tab', () => {
48
+ const onSelectTab = jest.fn();
49
+ const { getByText } = render(
50
+ <ConsoleTabBar
51
+ activeTab="prs"
52
+ counts={counts}
53
+ onSelectTab={onSelectTab}
54
+ />,
55
+ );
56
+ fireEvent.click(getByText('Unread'));
57
+ expect(onSelectTab).toHaveBeenCalledWith('unread');
58
+ });
59
+ });
@@ -1,32 +1,41 @@
1
- import { Button } from '@/components/ui/button';
2
- import { cn } from '@/lib/utils';
3
1
  import { CONSOLE_TABS, type ConsoleTabName } from '../types';
4
2
 
5
3
  export type ConsoleTabBarProps = {
6
4
  activeTab: ConsoleTabName;
5
+ counts: Record<ConsoleTabName, number>;
7
6
  onSelectTab: (tab: ConsoleTabName) => void;
8
7
  };
9
8
 
10
9
  export const ConsoleTabBar = ({
11
10
  activeTab,
11
+ counts,
12
12
  onSelectTab,
13
13
  }: ConsoleTabBarProps) => (
14
- <nav
15
- aria-label="Console tabs"
16
- className="flex flex-wrap gap-1 border-b border-border p-2"
17
- >
18
- {CONSOLE_TABS.map((tab) => (
19
- <Button
20
- key={tab.name}
21
- type="button"
22
- size="sm"
23
- variant={tab.name === activeTab ? 'default' : 'ghost'}
24
- aria-current={tab.name === activeTab ? 'page' : undefined}
25
- className={cn(tab.name === activeTab && 'font-semibold')}
26
- onClick={() => onSelectTab(tab.name)}
27
- >
28
- {tab.label}
29
- </Button>
30
- ))}
14
+ <nav aria-label="Console tabs" className="console-tabbar">
15
+ {CONSOLE_TABS.map((tab) => {
16
+ const count = counts[tab.name] ?? 0;
17
+ const isActive = tab.name === activeTab;
18
+ if (count === 0 && !isActive) {
19
+ return null;
20
+ }
21
+ return (
22
+ <button
23
+ key={tab.name}
24
+ type="button"
25
+ className="console-tab"
26
+ data-active={isActive ? 'true' : undefined}
27
+ aria-current={isActive ? 'page' : undefined}
28
+ onClick={() => onSelectTab(tab.name)}
29
+ >
30
+ <span className="console-tab-label">{tab.label}</span>
31
+ <span
32
+ className="console-tab-badge"
33
+ data-zero={count === 0 ? 'true' : undefined}
34
+ >
35
+ {count}
36
+ </span>
37
+ </button>
38
+ );
39
+ })}
31
40
  </nav>
32
41
  );
@@ -0,0 +1,35 @@
1
+ import { fileStatusBadge } from './fileStatus';
2
+
3
+ describe('fileStatusBadge', () => {
4
+ it('maps added to a green A badge', () => {
5
+ expect(fileStatusBadge('added')).toEqual({ label: 'A', color: '#3fb950' });
6
+ });
7
+
8
+ it('maps modified to a yellow M badge', () => {
9
+ expect(fileStatusBadge('modified')).toEqual({
10
+ label: 'M',
11
+ color: '#d29922',
12
+ });
13
+ });
14
+
15
+ it('maps removed to a red D badge', () => {
16
+ expect(fileStatusBadge('removed')).toEqual({
17
+ label: 'D',
18
+ color: '#f85149',
19
+ });
20
+ });
21
+
22
+ it('maps renamed to a purple R badge', () => {
23
+ expect(fileStatusBadge('renamed')).toEqual({
24
+ label: 'R',
25
+ color: '#a371f7',
26
+ });
27
+ });
28
+
29
+ it('maps an unknown status to a gray question badge', () => {
30
+ expect(fileStatusBadge('whatever')).toEqual({
31
+ label: '?',
32
+ color: '#8b949e',
33
+ });
34
+ });
35
+ });
@@ -0,0 +1,21 @@
1
+ export type ConsoleFileStatusBadge = {
2
+ label: string;
3
+ color: string;
4
+ };
5
+
6
+ export const fileStatusBadge = (status: string): ConsoleFileStatusBadge => {
7
+ switch (status) {
8
+ case 'added':
9
+ return { label: 'A', color: '#3fb950' };
10
+ case 'modified':
11
+ return { label: 'M', color: '#d29922' };
12
+ case 'removed':
13
+ return { label: 'D', color: '#f85149' };
14
+ case 'renamed':
15
+ return { label: 'R', color: '#a371f7' };
16
+ case 'changed':
17
+ return { label: 'M', color: '#d29922' };
18
+ default:
19
+ return { label: '?', color: '#8b949e' };
20
+ }
21
+ };