github-issue-tower-defence-management 1.99.1 → 1.100.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.
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>TDPM Console</title>
7
- <script type="module" crossorigin src="/assets/index-D79mM9lz.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-poCP-zfO.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/assets/index-N39d52Zc.css">
9
9
  </head>
10
10
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-issue-tower-defence-management",
3
- "version": "1.99.1",
3
+ "version": "1.100.0",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "scripts": {
@@ -1,10 +1,23 @@
1
1
  import { act, renderHook } from '@testing-library/react';
2
+ import type { ConsoleTabName } from '../logic/types';
2
3
  import {
3
4
  parseItemKeyFromHash,
4
5
  parseTabFromPath,
5
6
  useConsoleNavigation,
6
7
  } from './useConsoleNavigation';
7
8
 
9
+ const counts = (
10
+ overrides: Partial<Record<ConsoleTabName, number>> = {},
11
+ ): Record<ConsoleTabName, number> => ({
12
+ 'workflow-blocker': 0,
13
+ prs: 0,
14
+ triage: 0,
15
+ unread: 0,
16
+ 'failed-preparation': 0,
17
+ 'todo-by-human': 0,
18
+ ...overrides,
19
+ });
20
+
8
21
  describe('parseTabFromPath', () => {
9
22
  it('reads a known tab from the project path', () => {
10
23
  expect(parseTabFromPath('/projects/umino/triage')).toBe('triage');
@@ -35,18 +48,24 @@ describe('useConsoleNavigation', () => {
35
48
  });
36
49
 
37
50
  it('reads the active tab from the path and no selected item', () => {
38
- const { result } = renderHook(() => useConsoleNavigation('umino'));
51
+ const { result } = renderHook(() =>
52
+ useConsoleNavigation('umino', counts()),
53
+ );
39
54
  expect(result.current.activeTab).toBe('prs');
40
55
  expect(result.current.selectedItemKey).toBeNull();
41
56
  });
42
57
 
43
58
  it('builds a project tab href', () => {
44
- const { result } = renderHook(() => useConsoleNavigation('umino'));
59
+ const { result } = renderHook(() =>
60
+ useConsoleNavigation('umino', counts()),
61
+ );
45
62
  expect(result.current.tabHref('triage')).toBe('/projects/umino/triage');
46
63
  });
47
64
 
48
65
  it('selects a tab and updates the path', () => {
49
- const { result } = renderHook(() => useConsoleNavigation('umino'));
66
+ const { result } = renderHook(() =>
67
+ useConsoleNavigation('umino', counts()),
68
+ );
50
69
  act(() => {
51
70
  result.current.selectTab('unread');
52
71
  });
@@ -55,7 +74,9 @@ describe('useConsoleNavigation', () => {
55
74
  });
56
75
 
57
76
  it('opens an item and reflects it in the hash', () => {
58
- const { result } = renderHook(() => useConsoleNavigation('umino'));
77
+ const { result } = renderHook(() =>
78
+ useConsoleNavigation('umino', counts()),
79
+ );
59
80
  act(() => {
60
81
  result.current.openItem('PVTI_open');
61
82
  });
@@ -65,7 +86,9 @@ describe('useConsoleNavigation', () => {
65
86
 
66
87
  it('closes an item and clears the hash', () => {
67
88
  window.history.replaceState({}, '', '/projects/umino/prs#item/PVTI_open');
68
- const { result } = renderHook(() => useConsoleNavigation('umino'));
89
+ const { result } = renderHook(() =>
90
+ useConsoleNavigation('umino', counts()),
91
+ );
69
92
  expect(result.current.selectedItemKey).toBe('PVTI_open');
70
93
  act(() => {
71
94
  result.current.closeItem();
@@ -74,3 +97,62 @@ describe('useConsoleNavigation', () => {
74
97
  expect(window.location.hash).toBe('');
75
98
  });
76
99
  });
100
+
101
+ describe('useConsoleNavigation default tab without a tab segment', () => {
102
+ beforeEach(() => {
103
+ window.history.replaceState({}, '', '/projects/umino?k=token');
104
+ });
105
+
106
+ it('lands on the left-most tab when all tabs are non-empty', () => {
107
+ const { result } = renderHook(() =>
108
+ useConsoleNavigation(
109
+ 'umino',
110
+ counts({
111
+ 'workflow-blocker': 3,
112
+ prs: 5,
113
+ triage: 2,
114
+ unread: 9,
115
+ 'failed-preparation': 1,
116
+ 'todo-by-human': 4,
117
+ }),
118
+ ),
119
+ );
120
+ expect(result.current.activeTab).toBe('workflow-blocker');
121
+ });
122
+
123
+ it('skips the empty left-most tab and lands on the next non-empty tab', () => {
124
+ const { result } = renderHook(() =>
125
+ useConsoleNavigation(
126
+ 'umino',
127
+ counts({ 'workflow-blocker': 0, prs: 0, triage: 8 }),
128
+ ),
129
+ );
130
+ expect(result.current.activeTab).toBe('triage');
131
+ });
132
+
133
+ it('falls back to the first tab when every tab is empty', () => {
134
+ const { result } = renderHook(() =>
135
+ useConsoleNavigation('umino', counts()),
136
+ );
137
+ expect(result.current.activeTab).toBe('workflow-blocker');
138
+ });
139
+
140
+ it('updates the default tab when counts arrive after the initial render', () => {
141
+ const { result, rerender } = renderHook(
142
+ ({ tabCounts }: { tabCounts: Record<ConsoleTabName, number> }) =>
143
+ useConsoleNavigation('umino', tabCounts),
144
+ { initialProps: { tabCounts: counts() } },
145
+ );
146
+ expect(result.current.activeTab).toBe('workflow-blocker');
147
+ rerender({ tabCounts: counts({ unread: 6 }) });
148
+ expect(result.current.activeTab).toBe('unread');
149
+ });
150
+
151
+ it('keeps the tab from the path even when counts are present', () => {
152
+ window.history.replaceState({}, '', '/projects/umino/triage?k=token');
153
+ const { result } = renderHook(() =>
154
+ useConsoleNavigation('umino', counts({ unread: 6 })),
155
+ );
156
+ expect(result.current.activeTab).toBe('triage');
157
+ });
158
+ });
@@ -1,4 +1,5 @@
1
1
  import { useCallback, useEffect, useState } from 'react';
2
+ import { resolveDefaultActiveTab } from '../logic/tabAdvance';
2
3
  import { CONSOLE_TABS, type ConsoleTabName } from '../logic/types';
3
4
 
4
5
  const TAB_NAMES = new Set<string>(CONSOLE_TABS.map((tab) => tab.name));
@@ -35,20 +36,21 @@ export type ConsoleNavigation = {
35
36
 
36
37
  export const useConsoleNavigation = (
37
38
  pjcode: string | null,
39
+ counts: Record<ConsoleTabName, number>,
38
40
  ): ConsoleNavigation => {
39
41
  const readState = useCallback((): {
40
42
  activeTab: ConsoleTabName;
41
43
  selectedItemKey: string | null;
42
44
  } => {
45
+ const fallbackTab = resolveDefaultActiveTab(counts);
43
46
  if (typeof window === 'undefined') {
44
- return { activeTab: CONSOLE_TABS[0].name, selectedItemKey: null };
47
+ return { activeTab: fallbackTab, selectedItemKey: null };
45
48
  }
46
49
  return {
47
- activeTab:
48
- parseTabFromPath(window.location.pathname) ?? CONSOLE_TABS[0].name,
50
+ activeTab: parseTabFromPath(window.location.pathname) ?? fallbackTab,
49
51
  selectedItemKey: parseItemKeyFromHash(window.location.hash),
50
52
  };
51
- }, []);
53
+ }, [counts]);
52
54
 
53
55
  const [state, setState] = useState(readState);
54
56
 
@@ -67,6 +69,21 @@ export const useConsoleNavigation = (
67
69
  };
68
70
  }, [readState]);
69
71
 
72
+ useEffect(() => {
73
+ if (typeof window === 'undefined') {
74
+ return;
75
+ }
76
+ if (parseTabFromPath(window.location.pathname) !== null) {
77
+ return;
78
+ }
79
+ const fallbackTab = resolveDefaultActiveTab(counts);
80
+ setState((current) =>
81
+ current.activeTab === fallbackTab
82
+ ? current
83
+ : { ...current, activeTab: fallbackTab },
84
+ );
85
+ }, [counts]);
86
+
70
87
  const tabHref = useCallback(
71
88
  (tab: ConsoleTabName): string =>
72
89
  pjcode === null ? `#${tab}` : `/projects/${pjcode}/${tab}`,
@@ -1,4 +1,7 @@
1
- import { findNextNonEmptyTabToRight } from './tabAdvance';
1
+ import {
2
+ findNextNonEmptyTabToRight,
3
+ resolveDefaultActiveTab,
4
+ } from './tabAdvance';
2
5
  import type { ConsoleTabName } from './types';
3
6
 
4
7
  const counts = (
@@ -47,3 +50,32 @@ describe('findNextNonEmptyTabToRight', () => {
47
50
  ).toBeNull();
48
51
  });
49
52
  });
53
+
54
+ describe('resolveDefaultActiveTab', () => {
55
+ it('returns the left-most tab when every tab is non-empty', () => {
56
+ expect(
57
+ resolveDefaultActiveTab(
58
+ counts({
59
+ 'workflow-blocker': 3,
60
+ prs: 5,
61
+ triage: 2,
62
+ unread: 9,
63
+ 'failed-preparation': 1,
64
+ 'todo-by-human': 4,
65
+ }),
66
+ ),
67
+ ).toBe('workflow-blocker');
68
+ });
69
+
70
+ it('skips empty left-most tabs and returns the first non-empty tab', () => {
71
+ expect(
72
+ resolveDefaultActiveTab(
73
+ counts({ 'workflow-blocker': 0, prs: 0, triage: 8 }),
74
+ ),
75
+ ).toBe('triage');
76
+ });
77
+
78
+ it('falls back to the first tab when every tab is empty', () => {
79
+ expect(resolveDefaultActiveTab(counts({}))).toBe('workflow-blocker');
80
+ });
81
+ });
@@ -17,3 +17,14 @@ export const findNextNonEmptyTabToRight = (
17
17
  }
18
18
  return null;
19
19
  };
20
+
21
+ export const resolveDefaultActiveTab = (
22
+ counts: Record<ConsoleTabName, number>,
23
+ ): ConsoleTabName => {
24
+ for (const tab of CONSOLE_TABS) {
25
+ if ((counts[tab.name] ?? 0) > 0) {
26
+ return tab.name;
27
+ }
28
+ }
29
+ return CONSOLE_TABS[0].name;
30
+ };
@@ -52,15 +52,7 @@ const OVERLAY_NAMESPACE_FALLBACK = 'console';
52
52
  export const ConsolePage = () => {
53
53
  const pjcode = useConsolePjcode();
54
54
  const { snapshots, isLoading, error } = useConsoleTabData(pjcode);
55
- const navigation = useConsoleNavigation(pjcode);
56
- const { activeTab, selectedItemKey, openItem, closeItem, selectTab } =
57
- navigation;
58
-
59
55
  const overlayState = useConsoleOverlay(pjcode ?? OVERLAY_NAMESPACE_FALLBACK);
60
- const caches = useConsoleCaches();
61
- const operations = useConsoleOperations(pjcode, activeTab, overlayState);
62
- const actionQueue = useConsoleActionQueue();
63
- const now = Date.now();
64
56
 
65
57
  const counts = useMemo(() => {
66
58
  const result = emptyCounts();
@@ -77,6 +69,15 @@ export const ConsolePage = () => {
77
69
  return result;
78
70
  }, [snapshots, overlayState.overlay]);
79
71
 
72
+ const navigation = useConsoleNavigation(pjcode, counts);
73
+ const { activeTab, selectedItemKey, openItem, closeItem, selectTab } =
74
+ navigation;
75
+
76
+ const caches = useConsoleCaches();
77
+ const operations = useConsoleOperations(pjcode, activeTab, overlayState);
78
+ const actionQueue = useConsoleActionQueue();
79
+ const now = Date.now();
80
+
80
81
  const activeSnapshot = snapshots[activeTab];
81
82
  const pendingItems = useMemo(() => {
82
83
  if (activeSnapshot === null) {