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.
- package/CHANGELOG.md +14 -0
- package/bin/adapter/entry-points/console/consoleOperationApi.js +28 -1
- package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleServer.js +2 -0
- package/bin/adapter/entry-points/console/consoleServer.js.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/{index-BU6p3cGU.css → index-0IuY3q4G.css} +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-D04N09aG.js +100 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapter/entry-points/console/consoleOperationApi.test.ts +72 -0
- package/src/adapter/entry-points/console/consoleOperationApi.ts +32 -0
- package/src/adapter/entry-points/console/consoleServer.test.ts +49 -0
- package/src/adapter/entry-points/console/consoleServer.ts +3 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentComposer.stories.tsx +30 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentComposer.test.tsx +81 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentComposer.tsx +118 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.stories.tsx +1 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.test.tsx +20 -3
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.tsx +31 -5
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsolePanel.stories.tsx +25 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsolePanel.test.tsx +27 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsolePanel.tsx +4 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.stories.tsx +18 -14
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.test.tsx +40 -13
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.tsx +67 -29
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemList.stories.tsx +1 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemList.test.tsx +7 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemList.tsx +4 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemSummary.stories.tsx +1 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemSummary.test.tsx +43 -6
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemSummary.tsx +26 -3
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleNextActionDateActions.tsx +2 -2
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsolePullRequestReviewActions.tsx +7 -8
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleNavigation.test.ts +76 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleNavigation.ts +111 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.test.ts +37 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.ts +18 -0
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +29 -1
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +40 -6
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.test.tsx +5 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +8 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.test.tsx +26 -9
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +21 -20
- package/src/adapter/entry-points/console/ui/src/index.css +241 -39
- package/src/adapter/entry-points/console/ui-dist/assets/{index-BU6p3cGU.css → index-0IuY3q4G.css} +1 -1
- package/src/adapter/entry-points/console/ui-dist/assets/index-D04N09aG.js +100 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts +1 -0
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleServer.d.ts.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-BvuSQN9s.js +0 -100
- package/src/adapter/entry-points/console/ui-dist/assets/index-Druih-WG.js +0 -100
package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleNavigation.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import { CONSOLE_TABS, type ConsoleTabName } from '../logic/types';
|
|
3
|
+
|
|
4
|
+
const TAB_NAMES = new Set<string>(CONSOLE_TABS.map((tab) => tab.name));
|
|
5
|
+
|
|
6
|
+
export const parseTabFromPath = (pathname: string): ConsoleTabName | null => {
|
|
7
|
+
const segments = pathname.split('/').filter((segment) => segment.length > 0);
|
|
8
|
+
if (segments.length < 3 || segments[0] !== 'projects') {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const candidate = segments[2];
|
|
12
|
+
return TAB_NAMES.has(candidate) ? (candidate as ConsoleTabName) : null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const parseItemKeyFromHash = (hash: string): string | null => {
|
|
16
|
+
const prefix = '#item/';
|
|
17
|
+
if (!hash.startsWith(prefix)) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const encoded = hash.slice(prefix.length);
|
|
21
|
+
if (encoded.length === 0) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return decodeURIComponent(encoded);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type ConsoleNavigation = {
|
|
28
|
+
activeTab: ConsoleTabName;
|
|
29
|
+
selectedItemKey: string | null;
|
|
30
|
+
tabHref: (tab: ConsoleTabName) => string;
|
|
31
|
+
selectTab: (tab: ConsoleTabName) => void;
|
|
32
|
+
openItem: (itemKey: string) => void;
|
|
33
|
+
closeItem: () => void;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const useConsoleNavigation = (
|
|
37
|
+
pjcode: string | null,
|
|
38
|
+
): ConsoleNavigation => {
|
|
39
|
+
const readState = useCallback((): {
|
|
40
|
+
activeTab: ConsoleTabName;
|
|
41
|
+
selectedItemKey: string | null;
|
|
42
|
+
} => {
|
|
43
|
+
if (typeof window === 'undefined') {
|
|
44
|
+
return { activeTab: CONSOLE_TABS[0].name, selectedItemKey: null };
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
activeTab:
|
|
48
|
+
parseTabFromPath(window.location.pathname) ?? CONSOLE_TABS[0].name,
|
|
49
|
+
selectedItemKey: parseItemKeyFromHash(window.location.hash),
|
|
50
|
+
};
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
const [state, setState] = useState(readState);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (typeof window === 'undefined') {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const sync = (): void => {
|
|
60
|
+
setState(readState());
|
|
61
|
+
};
|
|
62
|
+
window.addEventListener('popstate', sync);
|
|
63
|
+
window.addEventListener('hashchange', sync);
|
|
64
|
+
return () => {
|
|
65
|
+
window.removeEventListener('popstate', sync);
|
|
66
|
+
window.removeEventListener('hashchange', sync);
|
|
67
|
+
};
|
|
68
|
+
}, [readState]);
|
|
69
|
+
|
|
70
|
+
const tabHref = useCallback(
|
|
71
|
+
(tab: ConsoleTabName): string =>
|
|
72
|
+
pjcode === null ? `#${tab}` : `/projects/${pjcode}/${tab}`,
|
|
73
|
+
[pjcode],
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const selectTab = useCallback(
|
|
77
|
+
(tab: ConsoleTabName): void => {
|
|
78
|
+
if (typeof window === 'undefined') {
|
|
79
|
+
setState({ activeTab: tab, selectedItemKey: null });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
window.history.pushState({}, '', tabHref(tab));
|
|
83
|
+
setState({ activeTab: tab, selectedItemKey: null });
|
|
84
|
+
},
|
|
85
|
+
[tabHref],
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const openItem = useCallback((itemKey: string): void => {
|
|
89
|
+
if (typeof window !== 'undefined') {
|
|
90
|
+
window.location.hash = `#item/${encodeURIComponent(itemKey)}`;
|
|
91
|
+
}
|
|
92
|
+
setState((current) => ({ ...current, selectedItemKey: itemKey }));
|
|
93
|
+
}, []);
|
|
94
|
+
|
|
95
|
+
const closeItem = useCallback((): void => {
|
|
96
|
+
if (typeof window !== 'undefined' && window.location.hash !== '') {
|
|
97
|
+
const url = `${window.location.pathname}${window.location.search}`;
|
|
98
|
+
window.history.pushState({}, '', url);
|
|
99
|
+
}
|
|
100
|
+
setState((current) => ({ ...current, selectedItemKey: null }));
|
|
101
|
+
}, []);
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
activeTab: state.activeTab,
|
|
105
|
+
selectedItemKey: state.selectedItemKey,
|
|
106
|
+
tabHref,
|
|
107
|
+
selectTab,
|
|
108
|
+
openItem,
|
|
109
|
+
closeItem,
|
|
110
|
+
};
|
|
111
|
+
};
|
package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.test.ts
CHANGED
|
@@ -177,6 +177,43 @@ describe('useConsoleOperations', () => {
|
|
|
177
177
|
expect(stored[issueItem.projectItemId].done).toBe(true);
|
|
178
178
|
});
|
|
179
179
|
|
|
180
|
+
it('posts a comment to the comment endpoint and returns the created comment', async () => {
|
|
181
|
+
const fetchMock: jest.Mock = jest.fn(async () => ({
|
|
182
|
+
ok: true,
|
|
183
|
+
status: 200,
|
|
184
|
+
json: async () => ({
|
|
185
|
+
ok: true,
|
|
186
|
+
comment: {
|
|
187
|
+
author: 'HiromiShikata',
|
|
188
|
+
body: 'Thanks for the parity fix.',
|
|
189
|
+
createdAt: '2026-06-18T03:21:00.000Z',
|
|
190
|
+
},
|
|
191
|
+
}),
|
|
192
|
+
}));
|
|
193
|
+
global.fetch = fetchMock as unknown as typeof fetch;
|
|
194
|
+
const { result } = setup();
|
|
195
|
+
let created: Awaited<
|
|
196
|
+
ReturnType<typeof result.current.operations.addComment>
|
|
197
|
+
> | null = null;
|
|
198
|
+
await act(async () => {
|
|
199
|
+
created = await result.current.operations.addComment(
|
|
200
|
+
issueItem,
|
|
201
|
+
'Thanks for the parity fix.',
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
expect(fetchMock.mock.calls[0][0]).toBe('/api/comment?k=token');
|
|
205
|
+
expect(lastBody(fetchMock)).toMatchObject({
|
|
206
|
+
pjcode: 'umino',
|
|
207
|
+
url: issueItem.url,
|
|
208
|
+
body: 'Thanks for the parity fix.',
|
|
209
|
+
});
|
|
210
|
+
expect(created).toEqual({
|
|
211
|
+
author: 'HiromiShikata',
|
|
212
|
+
body: 'Thanks for the parity fix.',
|
|
213
|
+
createdAt: '2026-06-18T03:21:00.000Z',
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
180
217
|
it('rejects an operation and posts nothing when no pjcode is available', async () => {
|
|
181
218
|
const fetchMock = captureFetch();
|
|
182
219
|
localStorage.clear();
|
package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
type ConsoleIntmuxRequest,
|
|
4
4
|
type ConsoleReviewRequest,
|
|
5
5
|
type ConsoleTriageRequest,
|
|
6
|
+
postConsoleComment,
|
|
6
7
|
postConsoleOperation,
|
|
7
8
|
} from '../lib/consoleApi';
|
|
8
9
|
import {
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
} from '../logic/operations';
|
|
15
16
|
import { overlayKeyForItem } from '../logic/overlay';
|
|
16
17
|
import type {
|
|
18
|
+
ConsoleComment,
|
|
17
19
|
ConsoleFieldOption,
|
|
18
20
|
ConsoleListItem,
|
|
19
21
|
ConsoleTabName,
|
|
@@ -51,6 +53,7 @@ export type ConsoleOperationsApi = {
|
|
|
51
53
|
item: ConsoleListItem,
|
|
52
54
|
action: ConsoleCloseAction,
|
|
53
55
|
) => Promise<void>;
|
|
56
|
+
addComment: (item: ConsoleListItem, body: string) => Promise<ConsoleComment>;
|
|
54
57
|
};
|
|
55
58
|
|
|
56
59
|
const reviewRequest = (
|
|
@@ -232,6 +235,20 @@ export const useConsoleOperations = (
|
|
|
232
235
|
[pjcode, appendToken, markDone],
|
|
233
236
|
);
|
|
234
237
|
|
|
238
|
+
const addComment = useCallback(
|
|
239
|
+
async (item: ConsoleListItem, body: string) => {
|
|
240
|
+
if (pjcode === null) {
|
|
241
|
+
throw missingPjcodeError();
|
|
242
|
+
}
|
|
243
|
+
return postConsoleComment(appendToken, {
|
|
244
|
+
pjcode,
|
|
245
|
+
url: item.url,
|
|
246
|
+
body,
|
|
247
|
+
});
|
|
248
|
+
},
|
|
249
|
+
[pjcode, appendToken],
|
|
250
|
+
);
|
|
251
|
+
|
|
235
252
|
return {
|
|
236
253
|
reviewPullRequest,
|
|
237
254
|
setNextActionDate,
|
|
@@ -239,5 +256,6 @@ export const useConsoleOperations = (
|
|
|
239
256
|
setStatus,
|
|
240
257
|
setInTmuxByHuman,
|
|
241
258
|
closeIssue,
|
|
259
|
+
addComment,
|
|
242
260
|
};
|
|
243
261
|
};
|
|
@@ -20,10 +20,38 @@ describe('createConsoleApiClient', () => {
|
|
|
20
20
|
const body = await client.fetchItemBody('https://github.com/o/r/issues/1');
|
|
21
21
|
expect(body).toBe('# Title');
|
|
22
22
|
const requested = fetchMock.mock.calls[0][0] as string;
|
|
23
|
-
expect(requested).toContain('
|
|
23
|
+
expect(requested).toContain('/api/itembody?url=');
|
|
24
24
|
expect(requested).toContain('&k=token');
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
+
it('anchors every read endpoint at the server root regardless of route', async () => {
|
|
28
|
+
const readers: ((url: string) => Promise<unknown>)[] = [];
|
|
29
|
+
const client = createConsoleApiClient(appendToken);
|
|
30
|
+
readers.push((url) => client.fetchItemBody(url));
|
|
31
|
+
readers.push((url) => client.fetchComments(url));
|
|
32
|
+
readers.push((url) => client.fetchPrFiles(url));
|
|
33
|
+
readers.push((url) => client.fetchPrCommits(url));
|
|
34
|
+
readers.push((url) => client.fetchRelatedPrs(url));
|
|
35
|
+
readers.push((url) => client.fetchIssueState(url));
|
|
36
|
+
const expectedPaths = [
|
|
37
|
+
'/api/itembody',
|
|
38
|
+
'/api/comments',
|
|
39
|
+
'/api/prfiles',
|
|
40
|
+
'/api/prcommits',
|
|
41
|
+
'/api/relatedprs',
|
|
42
|
+
'/api/issuetitle',
|
|
43
|
+
];
|
|
44
|
+
for (let index = 0; index < readers.length; index += 1) {
|
|
45
|
+
const fetchMock = mockFetchOnce({});
|
|
46
|
+
await readers[index]('https://github.com/o/r/issues/1');
|
|
47
|
+
const requested = fetchMock.mock.calls[0][0] as string;
|
|
48
|
+
expect(requested.startsWith(expectedPaths[index])).toBe(true);
|
|
49
|
+
expect(requested.startsWith('/api/')).toBe(true);
|
|
50
|
+
expect(requested.startsWith('./')).toBe(false);
|
|
51
|
+
expect(requested.startsWith('/projects')).toBe(false);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
27
55
|
it('parses comments', async () => {
|
|
28
56
|
mockFetchOnce({
|
|
29
57
|
comments: [
|
|
@@ -156,19 +156,19 @@ export const createConsoleApiClient = (
|
|
|
156
156
|
appendToken: AppendToken,
|
|
157
157
|
): ConsoleApiClient => ({
|
|
158
158
|
fetchItemBody: async (url) => {
|
|
159
|
-
const payload = await requestJson(appendToken, '
|
|
159
|
+
const payload = await requestJson(appendToken, '/api/itembody', url);
|
|
160
160
|
return isRecord(payload) ? getString(payload.body) : '';
|
|
161
161
|
},
|
|
162
162
|
fetchComments: async (url) =>
|
|
163
|
-
parseComments(await requestJson(appendToken, '
|
|
163
|
+
parseComments(await requestJson(appendToken, '/api/comments', url)),
|
|
164
164
|
fetchPrFiles: async (url) =>
|
|
165
|
-
parseFiles(await requestJson(appendToken, '
|
|
165
|
+
parseFiles(await requestJson(appendToken, '/api/prfiles', url)),
|
|
166
166
|
fetchPrCommits: async (url) =>
|
|
167
|
-
parseCommits(await requestJson(appendToken, '
|
|
167
|
+
parseCommits(await requestJson(appendToken, '/api/prcommits', url)),
|
|
168
168
|
fetchRelatedPrs: async (url) =>
|
|
169
|
-
parseRelatedPrs(await requestJson(appendToken, '
|
|
169
|
+
parseRelatedPrs(await requestJson(appendToken, '/api/relatedprs', url)),
|
|
170
170
|
fetchIssueState: async (url) =>
|
|
171
|
-
parseState(await requestJson(appendToken, '
|
|
171
|
+
parseState(await requestJson(appendToken, '/api/issuetitle', url)),
|
|
172
172
|
});
|
|
173
173
|
|
|
174
174
|
export const postConsoleOperation = async (
|
|
@@ -185,3 +185,37 @@ export const postConsoleOperation = async (
|
|
|
185
185
|
throw new Error(`HTTP ${response.status}`);
|
|
186
186
|
}
|
|
187
187
|
};
|
|
188
|
+
|
|
189
|
+
export const COMMENT_OPERATION_PATH = '/api/comment';
|
|
190
|
+
|
|
191
|
+
export type ConsoleCommentRequest = {
|
|
192
|
+
pjcode: string;
|
|
193
|
+
url: string;
|
|
194
|
+
body: string;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const parsePostedComment = (payload: unknown): ConsoleComment => {
|
|
198
|
+
if (!isRecord(payload) || !isRecord(payload.comment)) {
|
|
199
|
+
throw new Error('comment was not returned');
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
author: getString(payload.comment.author),
|
|
203
|
+
body: getString(payload.comment.body),
|
|
204
|
+
createdAt: getString(payload.comment.createdAt),
|
|
205
|
+
};
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export const postConsoleComment = async (
|
|
209
|
+
appendToken: AppendToken,
|
|
210
|
+
request: ConsoleCommentRequest,
|
|
211
|
+
): Promise<ConsoleComment> => {
|
|
212
|
+
const response = await fetch(appendToken(COMMENT_OPERATION_PATH), {
|
|
213
|
+
method: 'POST',
|
|
214
|
+
headers: { 'Content-Type': 'application/json' },
|
|
215
|
+
body: JSON.stringify(request),
|
|
216
|
+
});
|
|
217
|
+
if (!response.ok) {
|
|
218
|
+
throw new Error(`HTTP ${response.status}`);
|
|
219
|
+
}
|
|
220
|
+
return parsePostedComment(await response.json());
|
|
221
|
+
};
|
|
@@ -47,6 +47,11 @@ const buildOperations = (): ConsoleOperationsApi => ({
|
|
|
47
47
|
setStatus: jest.fn(async () => {}),
|
|
48
48
|
setInTmuxByHuman: jest.fn(async () => {}),
|
|
49
49
|
closeIssue: jest.fn(async () => {}),
|
|
50
|
+
addComment: jest.fn(async () => ({
|
|
51
|
+
author: 'HiromiShikata',
|
|
52
|
+
body: 'comment body',
|
|
53
|
+
createdAt: '2026-06-19T11:58:00.000Z',
|
|
54
|
+
})),
|
|
50
55
|
});
|
|
51
56
|
|
|
52
57
|
describe('ConsoleItemDetailContainer', () => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ConsoleCommentComposer } from '../components/detail/ConsoleCommentComposer';
|
|
1
2
|
import { ConsoleItemDetail } from '../components/detail/ConsoleItemDetail';
|
|
2
3
|
import { ConsoleOperationMenu } from '../components/operations/ConsoleOperationMenu';
|
|
3
4
|
import type { ConsoleCaches } from '../hooks/useConsoleCaches';
|
|
@@ -94,6 +95,13 @@ export const ConsoleItemDetailContainer = ({
|
|
|
94
95
|
commitsError={detail.commitsError}
|
|
95
96
|
relatedPullRequests={detail.relatedPullRequests}
|
|
96
97
|
now={now}
|
|
98
|
+
commentComposer={
|
|
99
|
+
<ConsoleCommentComposer
|
|
100
|
+
isPr={item.isPr}
|
|
101
|
+
now={now}
|
|
102
|
+
onSubmit={(body) => operations.addComment(item, body)}
|
|
103
|
+
/>
|
|
104
|
+
}
|
|
97
105
|
operationBar={
|
|
98
106
|
<ConsoleOperationMenu
|
|
99
107
|
tab={tab}
|
|
@@ -84,8 +84,17 @@ describe('ConsolePage', () => {
|
|
|
84
84
|
expect(getByText('Add serveConsole subcommand')).toBeInTheDocument();
|
|
85
85
|
});
|
|
86
86
|
fireEvent.click(getByText('Add serveConsole subcommand'));
|
|
87
|
-
expect(await findByText('
|
|
88
|
-
expect(
|
|
87
|
+
expect(await findByText('Approve')).toBeInTheDocument();
|
|
88
|
+
expect(window.location.hash).toBe('#item/PVTI_1');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('renders the per-tab count sub-heading and snapshot time', async () => {
|
|
92
|
+
const { getByText } = render(<ConsolePage />);
|
|
93
|
+
await waitFor(() => {
|
|
94
|
+
expect(getByText('Add serveConsole subcommand')).toBeInTheDocument();
|
|
95
|
+
});
|
|
96
|
+
expect(getByText('1 items awaiting quality check')).toBeInTheDocument();
|
|
97
|
+
expect(getByText('snapshot: 2026-06-19T00:00:00.000Z')).toBeInTheDocument();
|
|
89
98
|
});
|
|
90
99
|
|
|
91
100
|
it('keeps a tab driven to zero at zero and does not revive its badge after switching tabs', async () => {
|
|
@@ -95,18 +104,18 @@ describe('ConsolePage', () => {
|
|
|
95
104
|
});
|
|
96
105
|
expect(
|
|
97
106
|
getByText('Awaiting Quality Check')
|
|
98
|
-
.closest('
|
|
107
|
+
.closest('a')
|
|
99
108
|
?.querySelector('.console-tab-badge')?.textContent,
|
|
100
109
|
).toBe('1');
|
|
101
110
|
|
|
102
111
|
fireEvent.click(getByText('Add serveConsole subcommand'));
|
|
103
|
-
expect(await findByText('
|
|
112
|
+
expect(await findByText('Approve')).toBeInTheDocument();
|
|
104
113
|
fireEvent.click(getByText('Approve'));
|
|
105
114
|
|
|
106
115
|
await waitFor(() => {
|
|
107
116
|
expect(
|
|
108
117
|
getByText('Awaiting Quality Check')
|
|
109
|
-
.closest('
|
|
118
|
+
.closest('a')
|
|
110
119
|
?.querySelector('.console-tab-badge')?.textContent,
|
|
111
120
|
).toBe('0');
|
|
112
121
|
});
|
|
@@ -118,11 +127,19 @@ describe('ConsolePage', () => {
|
|
|
118
127
|
).toBeInTheDocument();
|
|
119
128
|
});
|
|
120
129
|
|
|
121
|
-
const
|
|
122
|
-
'button',
|
|
123
|
-
);
|
|
130
|
+
const prsTabLink = queryByText('Awaiting Quality Check')?.closest('a');
|
|
124
131
|
const prsBadge =
|
|
125
|
-
|
|
132
|
+
prsTabLink?.querySelector('.console-tab-badge')?.textContent ?? '0';
|
|
126
133
|
expect(prsBadge).toBe('0');
|
|
127
134
|
});
|
|
135
|
+
|
|
136
|
+
it('shows every tab even when its count is zero', async () => {
|
|
137
|
+
const { getByText, queryByText } = render(<ConsolePage />);
|
|
138
|
+
await waitFor(() => {
|
|
139
|
+
expect(getByText('Add serveConsole subcommand')).toBeInTheDocument();
|
|
140
|
+
});
|
|
141
|
+
expect(queryByText('Triage')).not.toBeNull();
|
|
142
|
+
expect(queryByText('Failed Preparation')).not.toBeNull();
|
|
143
|
+
expect(queryByText('Todo by human')).not.toBeNull();
|
|
144
|
+
});
|
|
128
145
|
});
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { useMemo
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
2
|
import { ConsoleProjectSummary } from '../components/layout/ConsoleProjectSummary';
|
|
3
3
|
import { ConsoleTabList } from '../components/layout/ConsoleTabList';
|
|
4
4
|
import { ConsoleItemList } from '../components/list/ConsoleItemList';
|
|
5
5
|
import { useConsoleCaches } from '../hooks/useConsoleCaches';
|
|
6
|
+
import { useConsoleNavigation } from '../hooks/useConsoleNavigation';
|
|
6
7
|
import { useConsoleOperations } from '../hooks/useConsoleOperations';
|
|
7
8
|
import { useConsoleOverlay } from '../hooks/useConsoleOverlay';
|
|
8
9
|
import { useConsolePjcode } from '../hooks/useConsolePjcode';
|
|
@@ -34,12 +35,8 @@ const OVERLAY_NAMESPACE_FALLBACK = 'console';
|
|
|
34
35
|
export const ConsolePage = () => {
|
|
35
36
|
const pjcode = useConsolePjcode();
|
|
36
37
|
const { snapshots, isLoading, error } = useConsoleTabData(pjcode);
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
);
|
|
40
|
-
const [selectedItem, setSelectedItem] = useState<ConsoleListItem | null>(
|
|
41
|
-
null,
|
|
42
|
-
);
|
|
38
|
+
const navigation = useConsoleNavigation(pjcode);
|
|
39
|
+
const { activeTab, selectedItemKey } = navigation;
|
|
43
40
|
|
|
44
41
|
const overlayState = useConsoleOverlay(pjcode ?? OVERLAY_NAMESPACE_FALLBACK);
|
|
45
42
|
const caches = useConsoleCaches();
|
|
@@ -77,11 +74,18 @@ export const ConsolePage = () => {
|
|
|
77
74
|
const storyColors = activeSnapshot?.storyColors ?? {};
|
|
78
75
|
const statusOptions = activeSnapshot?.statusOptions ?? [];
|
|
79
76
|
const storyOptions = activeSnapshot?.storyOptions ?? [];
|
|
77
|
+
const generatedAt = activeSnapshot?.generatedAt ?? null;
|
|
80
78
|
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
const selectedItem = useMemo<ConsoleListItem | null>(() => {
|
|
80
|
+
if (selectedItemKey === null || activeSnapshot === null) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
return (
|
|
84
|
+
activeSnapshot.items.find(
|
|
85
|
+
(item) => item.projectItemId === selectedItemKey,
|
|
86
|
+
) ?? null
|
|
87
|
+
);
|
|
88
|
+
}, [selectedItemKey, activeSnapshot]);
|
|
85
89
|
|
|
86
90
|
const overlayStatusForSelected = ((): ConsoleOverlayStatus | null => {
|
|
87
91
|
if (selectedItem === null) {
|
|
@@ -102,26 +106,23 @@ export const ConsolePage = () => {
|
|
|
102
106
|
<ConsoleTabList
|
|
103
107
|
activeTab={activeTab}
|
|
104
108
|
counts={counts}
|
|
105
|
-
|
|
109
|
+
pjcode={pjcode}
|
|
110
|
+
generatedAt={generatedAt}
|
|
111
|
+
tabHref={navigation.tabHref}
|
|
112
|
+
onSelectTab={navigation.selectTab}
|
|
106
113
|
/>
|
|
107
114
|
{selectedItem === null ? (
|
|
108
115
|
<ConsoleItemList
|
|
109
116
|
rows={rows}
|
|
110
117
|
storyColors={storyColors}
|
|
111
118
|
activeItemId={null}
|
|
119
|
+
now={now}
|
|
112
120
|
isLoading={isLoading}
|
|
113
121
|
error={error}
|
|
114
|
-
onSelectItem={
|
|
122
|
+
onSelectItem={(item) => navigation.openItem(item.projectItemId)}
|
|
115
123
|
/>
|
|
116
124
|
) : (
|
|
117
125
|
<div className="console-detail-screen">
|
|
118
|
-
<button
|
|
119
|
-
type="button"
|
|
120
|
-
className="console-back-button"
|
|
121
|
-
onClick={() => setSelectedItem(null)}
|
|
122
|
-
>
|
|
123
|
-
← Back to list
|
|
124
|
-
</button>
|
|
125
126
|
<ConsoleItemDetailContainer
|
|
126
127
|
tab={activeTab}
|
|
127
128
|
item={selectedItem}
|