github-issue-tower-defence-management 1.91.2 → 1.91.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/bin/adapter/entry-points/console/consoleOperationApi.js +28 -1
  3. package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
  4. package/bin/adapter/entry-points/console/consoleServer.js +2 -0
  5. package/bin/adapter/entry-points/console/consoleServer.js.map +1 -1
  6. package/bin/adapter/entry-points/console/ui-dist/assets/{index-BU6p3cGU.css → index-0IuY3q4G.css} +1 -1
  7. package/bin/adapter/entry-points/console/ui-dist/assets/index-D04N09aG.js +100 -0
  8. package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
  9. package/package.json +1 -1
  10. package/src/adapter/entry-points/console/consoleOperationApi.test.ts +72 -0
  11. package/src/adapter/entry-points/console/consoleOperationApi.ts +32 -0
  12. package/src/adapter/entry-points/console/consoleServer.test.ts +49 -0
  13. package/src/adapter/entry-points/console/consoleServer.ts +3 -0
  14. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentComposer.stories.tsx +30 -0
  15. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentComposer.test.tsx +81 -0
  16. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentComposer.tsx +118 -0
  17. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.stories.tsx +1 -0
  18. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.test.tsx +20 -3
  19. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.tsx +31 -5
  20. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsolePanel.stories.tsx +25 -0
  21. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsolePanel.test.tsx +27 -0
  22. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsolePanel.tsx +4 -1
  23. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.stories.tsx +18 -14
  24. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.test.tsx +40 -13
  25. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.tsx +67 -29
  26. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemList.stories.tsx +1 -0
  27. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemList.test.tsx +7 -1
  28. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemList.tsx +4 -1
  29. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemSummary.stories.tsx +1 -1
  30. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemSummary.test.tsx +43 -6
  31. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemSummary.tsx +26 -3
  32. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleNextActionDateActions.tsx +2 -2
  33. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsolePullRequestReviewActions.tsx +7 -8
  34. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleNavigation.test.ts +76 -0
  35. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleNavigation.ts +111 -0
  36. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.test.ts +37 -0
  37. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.ts +18 -0
  38. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +29 -1
  39. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +40 -6
  40. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.test.tsx +5 -0
  41. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +8 -0
  42. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.test.tsx +26 -9
  43. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +21 -20
  44. package/src/adapter/entry-points/console/ui/src/index.css +241 -39
  45. package/src/adapter/entry-points/console/ui-dist/assets/{index-BU6p3cGU.css → index-0IuY3q4G.css} +1 -1
  46. package/src/adapter/entry-points/console/ui-dist/assets/index-D04N09aG.js +100 -0
  47. package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
  48. package/types/adapter/entry-points/console/consoleOperationApi.d.ts +1 -0
  49. package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
  50. package/types/adapter/entry-points/console/consoleServer.d.ts.map +1 -1
  51. package/bin/adapter/entry-points/console/ui-dist/assets/index-BvuSQN9s.js +0 -100
  52. package/src/adapter/entry-points/console/ui-dist/assets/index-Druih-WG.js +0 -100
@@ -6,6 +6,12 @@ import { ConsoleTabList } from './ConsoleTabList';
6
6
  const meta: Meta<typeof ConsoleTabList> = {
7
7
  title: 'Console/ConsoleTabList',
8
8
  component: ConsoleTabList,
9
+ args: {
10
+ pjcode: 'umino',
11
+ generatedAt: '2026-06-19T08:42:11.000Z',
12
+ tabHref: (tab: ConsoleTabName) => `/projects/umino/${tab}`,
13
+ onSelectTab: () => {},
14
+ },
9
15
  };
10
16
 
11
17
  export default meta;
@@ -14,53 +20,51 @@ type Story = StoryObj<typeof ConsoleTabList>;
14
20
 
15
21
  const counts: Record<ConsoleTabName, number> = {
16
22
  prs: 35,
17
- triage: 12,
18
- unread: 7,
23
+ triage: 132,
24
+ unread: 18,
19
25
  'failed-preparation': 2,
20
- 'todo-by-human': 4,
26
+ 'todo-by-human': 66,
21
27
  };
22
28
 
23
29
  export const AllTabsWithCounts: Story = {
24
30
  args: {
25
31
  activeTab: 'prs',
26
32
  counts,
27
- onSelectTab: () => {},
28
33
  },
29
34
  };
30
35
 
31
- export const ZeroCountTabsHidden: Story = {
36
+ export const ZeroCountTabsStillShown: Story = {
32
37
  args: {
33
38
  activeTab: 'prs',
34
39
  counts: {
35
40
  prs: 35,
36
41
  triage: 0,
37
- unread: 7,
42
+ unread: 18,
38
43
  'failed-preparation': 0,
39
44
  'todo-by-human': 0,
40
45
  },
41
- onSelectTab: () => {},
42
46
  },
43
47
  };
44
48
 
45
- export const ActiveZeroCountTabStaysVisible: Story = {
49
+ export const ZeroCountActiveTab: Story = {
46
50
  args: {
47
- activeTab: 'triage',
51
+ activeTab: 'failed-preparation',
48
52
  counts: {
49
53
  prs: 35,
50
- triage: 0,
51
- unread: 7,
54
+ triage: 132,
55
+ unread: 18,
52
56
  'failed-preparation': 0,
53
- 'todo-by-human': 0,
57
+ 'todo-by-human': 66,
54
58
  },
55
- onSelectTab: () => {},
56
59
  },
57
60
  };
58
61
 
59
62
  export const Interactive: Story = {
60
- render: () => {
63
+ render: (args) => {
61
64
  const [activeTab, setActiveTab] = useState<ConsoleTabName>('prs');
62
65
  return (
63
66
  <ConsoleTabList
67
+ {...args}
64
68
  activeTab={activeTab}
65
69
  counts={counts}
66
70
  onSelectTab={setActiveTab}
@@ -10,35 +10,61 @@ const counts: Record<ConsoleTabName, number> = {
10
10
  'todo-by-human': 2,
11
11
  };
12
12
 
13
+ const baseProps = {
14
+ pjcode: 'umino',
15
+ generatedAt: '2026-06-19T08:42:11.000Z',
16
+ tabHref: (tab: ConsoleTabName) => `/projects/umino/${tab}`,
17
+ onSelectTab: () => {},
18
+ };
19
+
13
20
  describe('ConsoleTabList', () => {
14
- it('hides zero-count tabs except the active tab', () => {
21
+ it('shows every tab including zero-count tabs', () => {
15
22
  const { queryByText } = render(
16
- <ConsoleTabList activeTab="prs" counts={counts} onSelectTab={() => {}} />,
23
+ <ConsoleTabList {...baseProps} activeTab="prs" counts={counts} />,
17
24
  );
18
25
  expect(queryByText('Awaiting Quality Check')).not.toBeNull();
26
+ expect(queryByText('Triage')).not.toBeNull();
19
27
  expect(queryByText('Unread')).not.toBeNull();
28
+ expect(queryByText('Failed Preparation')).not.toBeNull();
20
29
  expect(queryByText('Todo by human')).not.toBeNull();
21
- expect(queryByText('Triage')).toBeNull();
22
- expect(queryByText('Failed Preparation')).toBeNull();
23
30
  });
24
31
 
25
- it('keeps a zero-count active tab visible', () => {
26
- const { queryByText } = render(
27
- <ConsoleTabList
28
- activeTab="triage"
29
- counts={counts}
30
- onSelectTab={() => {}}
31
- />,
32
+ it('marks the active tab and renders zero badges with the zero attribute', () => {
33
+ const { getByText } = render(
34
+ <ConsoleTabList {...baseProps} activeTab="prs" counts={counts} />,
32
35
  );
33
- expect(queryByText('Triage')).not.toBeNull();
36
+ const triageBadge = getByText('Triage')
37
+ .closest('a')
38
+ ?.querySelector('.console-tab-badge');
39
+ expect(triageBadge).toHaveAttribute('data-zero', 'true');
40
+ expect(triageBadge?.textContent).toBe('0');
41
+ expect(getByText('Awaiting Quality Check').closest('a')).toHaveAttribute(
42
+ 'aria-current',
43
+ 'page',
44
+ );
45
+ });
46
+
47
+ it('renders the active tab count sub-heading', () => {
48
+ const { getByText } = render(
49
+ <ConsoleTabList {...baseProps} activeTab="triage" counts={counts} />,
50
+ );
51
+ expect(getByText('0 items to triage')).toBeInTheDocument();
52
+ });
53
+
54
+ it('renders the project code and snapshot time', () => {
55
+ const { getByText } = render(
56
+ <ConsoleTabList {...baseProps} activeTab="prs" counts={counts} />,
57
+ );
58
+ expect(getByText('umino')).toBeInTheDocument();
59
+ expect(getByText('snapshot: 2026-06-19T08:42:11.000Z')).toBeInTheDocument();
34
60
  });
35
61
 
36
62
  it('uses the exact lowercase Todo by human label', () => {
37
63
  const { getByText } = render(
38
64
  <ConsoleTabList
65
+ {...baseProps}
39
66
  activeTab="todo-by-human"
40
67
  counts={counts}
41
- onSelectTab={() => {}}
42
68
  />,
43
69
  );
44
70
  expect(getByText('Todo by human')).toBeInTheDocument();
@@ -48,6 +74,7 @@ describe('ConsoleTabList', () => {
48
74
  const onSelectTab = jest.fn();
49
75
  const { getByText } = render(
50
76
  <ConsoleTabList
77
+ {...baseProps}
51
78
  activeTab="prs"
52
79
  counts={counts}
53
80
  onSelectTab={onSelectTab}
@@ -3,39 +3,77 @@ import { CONSOLE_TABS, type ConsoleTabName } from '../../logic/types';
3
3
  export type ConsoleTabBarProps = {
4
4
  activeTab: ConsoleTabName;
5
5
  counts: Record<ConsoleTabName, number>;
6
+ pjcode: string | null;
7
+ generatedAt: string | null;
8
+ tabHref: (tab: ConsoleTabName) => string;
6
9
  onSelectTab: (tab: ConsoleTabName) => void;
7
10
  };
8
11
 
12
+ const COUNT_HEADING_LABELS: Record<ConsoleTabName, string> = {
13
+ prs: 'items awaiting quality check',
14
+ triage: 'items to triage',
15
+ unread: 'unread items',
16
+ 'failed-preparation': 'failed preparation items',
17
+ 'todo-by-human': 'todo by human items',
18
+ };
19
+
9
20
  export const ConsoleTabList = ({
10
21
  activeTab,
11
22
  counts,
23
+ pjcode,
24
+ generatedAt,
25
+ tabHref,
12
26
  onSelectTab,
13
- }: ConsoleTabBarProps) => (
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
- })}
40
- </nav>
41
- );
27
+ }: ConsoleTabBarProps) => {
28
+ const activeCount = counts[activeTab] ?? 0;
29
+ return (
30
+ <>
31
+ <nav aria-label="Console tabs" className="console-tabbar">
32
+ {CONSOLE_TABS.map((tab) => {
33
+ const count = counts[tab.name] ?? 0;
34
+ const isActive = tab.name === activeTab;
35
+ return (
36
+ <a
37
+ key={tab.name}
38
+ href={tabHref(tab.name)}
39
+ className="console-tab"
40
+ data-active={isActive ? 'true' : undefined}
41
+ aria-current={isActive ? 'page' : undefined}
42
+ onClick={(event) => {
43
+ if (
44
+ event.defaultPrevented ||
45
+ event.button !== 0 ||
46
+ event.metaKey ||
47
+ event.ctrlKey ||
48
+ event.shiftKey ||
49
+ event.altKey
50
+ ) {
51
+ return;
52
+ }
53
+ event.preventDefault();
54
+ onSelectTab(tab.name);
55
+ }}
56
+ >
57
+ <span className="console-tab-label">{tab.label}</span>
58
+ <span
59
+ className="console-tab-badge"
60
+ data-zero={count === 0 ? 'true' : undefined}
61
+ >
62
+ {count}
63
+ </span>
64
+ </a>
65
+ );
66
+ })}
67
+ {pjcode !== null && (
68
+ <span className="console-tab-pjname">{pjcode}</span>
69
+ )}
70
+ {generatedAt !== null && (
71
+ <span className="console-tab-geninfo">snapshot: {generatedAt}</span>
72
+ )}
73
+ </nav>
74
+ <p className="console-tab-count-heading">
75
+ {activeCount} {COUNT_HEADING_LABELS[activeTab]}
76
+ </p>
77
+ </>
78
+ );
79
+ };
@@ -9,6 +9,7 @@ import { ConsoleItemList } from './ConsoleItemList';
9
9
  const meta: Meta<typeof ConsoleItemList> = {
10
10
  title: 'Console/ConsoleItemList',
11
11
  component: ConsoleItemList,
12
+ args: { now: Date.parse('2026-06-19T12:00:00.000Z') },
12
13
  };
13
14
 
14
15
  export default meta;
@@ -7,6 +7,7 @@ import {
7
7
  import { ConsoleItemList } from './ConsoleItemList';
8
8
 
9
9
  const rows = buildConsoleListRows(consoleListItemsFixture, {});
10
+ const now = Date.parse('2026-06-19T12:00:00.000Z');
10
11
 
11
12
  describe('ConsoleItemList', () => {
12
13
  it('renders group headers and items in array order', () => {
@@ -15,6 +16,7 @@ describe('ConsoleItemList', () => {
15
16
  rows={rows}
16
17
  storyColors={consoleStoryColorsFixture}
17
18
  activeItemId={null}
19
+ now={now}
18
20
  isLoading={false}
19
21
  error={null}
20
22
  onSelectItem={() => {}}
@@ -32,6 +34,7 @@ describe('ConsoleItemList', () => {
32
34
  rows={rows}
33
35
  storyColors={consoleStoryColorsFixture}
34
36
  activeItemId={null}
37
+ now={now}
35
38
  isLoading={false}
36
39
  error={null}
37
40
  onSelectItem={onSelectItem}
@@ -49,6 +52,7 @@ describe('ConsoleItemList', () => {
49
52
  rows={[]}
50
53
  storyColors={{}}
51
54
  activeItemId={null}
55
+ now={now}
52
56
  isLoading
53
57
  error={null}
54
58
  onSelectItem={() => {}}
@@ -63,12 +67,13 @@ describe('ConsoleItemList', () => {
63
67
  rows={[]}
64
68
  storyColors={{}}
65
69
  activeItemId={null}
70
+ now={now}
66
71
  isLoading={false}
67
72
  error={null}
68
73
  onSelectItem={() => {}}
69
74
  />,
70
75
  );
71
- expect(getByText('No items.')).toBeInTheDocument();
76
+ expect(getByText('No items')).toBeInTheDocument();
72
77
  });
73
78
 
74
79
  it('shows the error state', () => {
@@ -77,6 +82,7 @@ describe('ConsoleItemList', () => {
77
82
  rows={[]}
78
83
  storyColors={{}}
79
84
  activeItemId={null}
85
+ now={now}
80
86
  isLoading={false}
81
87
  error="HTTP 404"
82
88
  onSelectItem={() => {}}
@@ -13,6 +13,7 @@ export type ConsoleListViewProps = {
13
13
  rows: ConsoleListRow[];
14
14
  storyColors: ConsoleStoryColorSource;
15
15
  activeItemId: string | null;
16
+ now: number;
16
17
  isLoading: boolean;
17
18
  error: string | null;
18
19
  onSelectItem: (item: ConsoleListItem) => void;
@@ -22,6 +23,7 @@ export const ConsoleItemList = ({
22
23
  rows,
23
24
  storyColors,
24
25
  activeItemId,
26
+ now,
25
27
  isLoading,
26
28
  error,
27
29
  onSelectItem,
@@ -39,7 +41,7 @@ export const ConsoleItemList = ({
39
41
  }
40
42
 
41
43
  if (rows.length === 0) {
42
- return <p className="console-list-message">No items.</p>;
44
+ return <p className="console-list-empty">No items</p>;
43
45
  }
44
46
 
45
47
  return (
@@ -58,6 +60,7 @@ export const ConsoleItemList = ({
58
60
  <ConsoleItemSummary
59
61
  item={row.item}
60
62
  isActive={row.item.itemId === activeItemId}
63
+ now={now}
61
64
  onSelect={onSelectItem}
62
65
  />
63
66
  </li>
@@ -5,7 +5,7 @@ import { ConsoleItemSummary } from './ConsoleItemSummary';
5
5
  const meta: Meta<typeof ConsoleItemSummary> = {
6
6
  title: 'Console/ConsoleItemSummary',
7
7
  component: ConsoleItemSummary,
8
- args: { onSelect: () => {} },
8
+ args: { onSelect: () => {}, now: Date.parse('2026-06-19T12:00:00.000Z') },
9
9
  };
10
10
 
11
11
  export default meta;
@@ -1,34 +1,66 @@
1
1
  import { fireEvent, render } from '@testing-library/react';
2
+ import { formatFullTimestamp } from '../../logic/relativeTime';
2
3
  import { consoleListItemsFixture } from '../../testing/fixtures';
3
4
  import { ConsoleItemSummary } from './ConsoleItemSummary';
4
5
 
6
+ const now = Date.parse('2026-06-19T12:00:00.000Z');
5
7
  const prItem = consoleListItemsFixture[0];
6
8
  const issueItem = consoleListItemsFixture[2];
7
9
 
8
10
  describe('ConsoleItemSummary', () => {
9
- it('renders a PR number with the PR prefix', () => {
11
+ it('renders the number, repository and PR type pills for a pull request', () => {
10
12
  const { getByText } = render(
11
- <ConsoleItemSummary item={prItem} isActive={false} onSelect={() => {}} />,
13
+ <ConsoleItemSummary
14
+ item={prItem}
15
+ isActive={false}
16
+ now={now}
17
+ onSelect={() => {}}
18
+ />,
12
19
  );
13
- expect(getByText(`PR #${prItem.number}`)).toBeInTheDocument();
20
+ expect(getByText(`#${prItem.number}`)).toBeInTheDocument();
21
+ expect(getByText(prItem.repo)).toBeInTheDocument();
22
+ expect(getByText('PR')).toBeInTheDocument();
14
23
  expect(getByText(prItem.title)).toBeInTheDocument();
15
24
  });
16
25
 
17
- it('renders an issue number with the hash prefix', () => {
26
+ it('renders the Issue type pill for an issue', () => {
18
27
  const { getByText } = render(
19
28
  <ConsoleItemSummary
20
29
  item={issueItem}
21
30
  isActive={false}
31
+ now={now}
22
32
  onSelect={() => {}}
23
33
  />,
24
34
  );
25
35
  expect(getByText(`#${issueItem.number}`)).toBeInTheDocument();
36
+ expect(getByText('Issue')).toBeInTheDocument();
37
+ });
38
+
39
+ it('renders the opened relative time with the full timestamp title', () => {
40
+ const { getByText } = render(
41
+ <ConsoleItemSummary
42
+ item={prItem}
43
+ isActive={false}
44
+ now={now}
45
+ onSelect={() => {}}
46
+ />,
47
+ );
48
+ const createdAt = getByText('2 days ago');
49
+ expect(createdAt).toHaveAttribute(
50
+ 'title',
51
+ formatFullTimestamp(prItem.createdAt),
52
+ );
26
53
  });
27
54
 
28
55
  it('reports the item on click', () => {
29
56
  const onSelect = jest.fn();
30
57
  const { getByRole } = render(
31
- <ConsoleItemSummary item={prItem} isActive={false} onSelect={onSelect} />,
58
+ <ConsoleItemSummary
59
+ item={prItem}
60
+ isActive={false}
61
+ now={now}
62
+ onSelect={onSelect}
63
+ />,
32
64
  );
33
65
  fireEvent.click(getByRole('button'));
34
66
  expect(onSelect).toHaveBeenCalledWith(prItem);
@@ -36,7 +68,12 @@ describe('ConsoleItemSummary', () => {
36
68
 
37
69
  it('marks the active row', () => {
38
70
  const { getByRole } = render(
39
- <ConsoleItemSummary item={prItem} isActive onSelect={() => {}} />,
71
+ <ConsoleItemSummary
72
+ item={prItem}
73
+ isActive
74
+ now={now}
75
+ onSelect={() => {}}
76
+ />,
40
77
  );
41
78
  expect(getByRole('button')).toHaveAttribute('data-active', 'true');
42
79
  });
@@ -1,15 +1,21 @@
1
+ import {
2
+ formatFullTimestamp,
3
+ formatRelativeTime,
4
+ } from '../../logic/relativeTime';
1
5
  import type { ConsoleListItem } from '../../logic/types';
2
6
  import { ConsoleItemIcon } from '../detail/ConsoleItemIcon';
3
7
 
4
8
  export type ConsoleListItemRowProps = {
5
9
  item: ConsoleListItem;
6
10
  isActive: boolean;
11
+ now: number;
7
12
  onSelect: (item: ConsoleListItem) => void;
8
13
  };
9
14
 
10
15
  export const ConsoleItemSummary = ({
11
16
  item,
12
17
  isActive,
18
+ now,
13
19
  onSelect,
14
20
  }: ConsoleListItemRowProps) => (
15
21
  <button
@@ -26,9 +32,26 @@ export const ConsoleItemSummary = ({
26
32
  isDraft={false}
27
33
  stateReason=""
28
34
  />
29
- <span className="console-item-title">{item.title}</span>
30
- <span className="console-item-number">
31
- {item.isPr ? `PR #${item.number}` : `#${item.number}`}
35
+ <span className="console-item-meta">
36
+ <span className="console-item-title">{item.title}</span>
37
+ <span className="console-item-sub">
38
+ <span className="console-item-pill">#{item.number}</span>
39
+ {item.repo !== '' && (
40
+ <span className="console-item-pill">{item.repo}</span>
41
+ )}
42
+ <span className="console-item-pill">{item.isPr ? 'PR' : 'Issue'}</span>
43
+ {item.createdAt !== '' && (
44
+ <span className="console-item-opened">
45
+ {' · opened '}
46
+ <span
47
+ className="console-item-createdat"
48
+ title={formatFullTimestamp(item.createdAt)}
49
+ >
50
+ {formatRelativeTime(item.createdAt, now)}
51
+ </span>
52
+ </span>
53
+ )}
54
+ </span>
32
55
  </span>
33
56
  </button>
34
57
  );
@@ -12,14 +12,14 @@ export const ConsoleNextActionDateActions = ({
12
12
  <div className="console-op-group">
13
13
  <button
14
14
  type="button"
15
- className="console-op-button"
15
+ className="console-op-button console-op-button-snooze"
16
16
  onClick={() => onSetNextActionDate('snooze_1day')}
17
17
  >
18
18
  +1 day
19
19
  </button>
20
20
  <button
21
21
  type="button"
22
- className="console-op-button"
22
+ className="console-op-button console-op-button-snooze"
23
23
  onClick={() => onSetNextActionDate('snooze_1week')}
24
24
  >
25
25
  {isTodoByHuman ? '+1 week and skip' : '+1 week'}
@@ -7,24 +7,23 @@ export type ConsolePullRequestReviewGroupProps = {
7
7
  const REVIEW_BUTTONS: {
8
8
  action: ConsoleReviewAction;
9
9
  label: string;
10
- color: string;
10
+ variant: string;
11
11
  }[] = [
12
- { action: 'unnecessary', label: 'Unnecessary', color: '#8b949e' },
13
- { action: 'totally_wrong', label: 'Totally wrong', color: '#f85149' },
14
- { action: 'request_changes', label: 'Reject', color: '#d29922' },
15
- { action: 'approve', label: 'Approve', color: '#3fb950' },
12
+ { action: 'unnecessary', label: 'Unnecessary', variant: 'unneeded' },
13
+ { action: 'totally_wrong', label: 'Totally wrong', variant: 'wrong' },
14
+ { action: 'request_changes', label: 'Reject', variant: 'reject' },
15
+ { action: 'approve', label: 'Approve', variant: 'approve' },
16
16
  ];
17
17
 
18
18
  export const ConsolePullRequestReviewActions = ({
19
19
  onReview,
20
20
  }: ConsolePullRequestReviewGroupProps) => (
21
- <div className="console-op-group">
21
+ <div className="console-op-group console-op-group-review">
22
22
  {REVIEW_BUTTONS.map((button) => (
23
23
  <button
24
24
  key={button.action}
25
25
  type="button"
26
- className="console-op-button"
27
- style={{ color: button.color, borderColor: button.color }}
26
+ className={`console-op-button console-op-button-${button.variant}`}
28
27
  onClick={() => onReview(button.action)}
29
28
  >
30
29
  {button.label}
@@ -0,0 +1,76 @@
1
+ import { act, renderHook } from '@testing-library/react';
2
+ import {
3
+ parseItemKeyFromHash,
4
+ parseTabFromPath,
5
+ useConsoleNavigation,
6
+ } from './useConsoleNavigation';
7
+
8
+ describe('parseTabFromPath', () => {
9
+ it('reads a known tab from the project path', () => {
10
+ expect(parseTabFromPath('/projects/umino/triage')).toBe('triage');
11
+ });
12
+
13
+ it('returns null for an unknown tab segment', () => {
14
+ expect(parseTabFromPath('/projects/umino/unknown')).toBeNull();
15
+ });
16
+
17
+ it('returns null when there is no tab segment', () => {
18
+ expect(parseTabFromPath('/projects/umino')).toBeNull();
19
+ });
20
+ });
21
+
22
+ describe('parseItemKeyFromHash', () => {
23
+ it('decodes the item key from the hash', () => {
24
+ expect(parseItemKeyFromHash('#item/PVTI_lADO%20123')).toBe('PVTI_lADO 123');
25
+ });
26
+
27
+ it('returns null when the hash is not an item hash', () => {
28
+ expect(parseItemKeyFromHash('#other')).toBeNull();
29
+ });
30
+ });
31
+
32
+ describe('useConsoleNavigation', () => {
33
+ beforeEach(() => {
34
+ window.history.replaceState({}, '', '/projects/umino/prs?k=token');
35
+ });
36
+
37
+ it('reads the active tab from the path and no selected item', () => {
38
+ const { result } = renderHook(() => useConsoleNavigation('umino'));
39
+ expect(result.current.activeTab).toBe('prs');
40
+ expect(result.current.selectedItemKey).toBeNull();
41
+ });
42
+
43
+ it('builds a project tab href', () => {
44
+ const { result } = renderHook(() => useConsoleNavigation('umino'));
45
+ expect(result.current.tabHref('triage')).toBe('/projects/umino/triage');
46
+ });
47
+
48
+ it('selects a tab and updates the path', () => {
49
+ const { result } = renderHook(() => useConsoleNavigation('umino'));
50
+ act(() => {
51
+ result.current.selectTab('unread');
52
+ });
53
+ expect(result.current.activeTab).toBe('unread');
54
+ expect(window.location.pathname).toBe('/projects/umino/unread');
55
+ });
56
+
57
+ it('opens an item and reflects it in the hash', () => {
58
+ const { result } = renderHook(() => useConsoleNavigation('umino'));
59
+ act(() => {
60
+ result.current.openItem('PVTI_open');
61
+ });
62
+ expect(result.current.selectedItemKey).toBe('PVTI_open');
63
+ expect(window.location.hash).toBe('#item/PVTI_open');
64
+ });
65
+
66
+ it('closes an item and clears the hash', () => {
67
+ window.history.replaceState({}, '', '/projects/umino/prs#item/PVTI_open');
68
+ const { result } = renderHook(() => useConsoleNavigation('umino'));
69
+ expect(result.current.selectedItemKey).toBe('PVTI_open');
70
+ act(() => {
71
+ result.current.closeItem();
72
+ });
73
+ expect(result.current.selectedItemKey).toBeNull();
74
+ expect(window.location.hash).toBe('');
75
+ });
76
+ });