github-issue-tower-defence-management 1.116.9 → 1.117.1
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/README.md +3 -3
- package/bin/adapter/entry-points/console/consoleReadApi.js +13 -1
- package/bin/adapter/entry-points/console/consoleReadApi.js.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-CJJsRF8i.js +101 -0
- package/bin/adapter/entry-points/console/ui-dist/assets/{index-D1V2fi29.css → index-DGVKvwux.css} +1 -1
- package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/bin/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.js +9 -2
- package/bin/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.js.map +1 -1
- package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js +3 -2
- package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js.map +1 -1
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +1 -0
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js +13 -3
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/console/consoleReadApi.test.ts +4 -0
- package/src/adapter/entry-points/console/consoleReadApi.ts +18 -0
- package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +2 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.stories.tsx +2 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.test.tsx +3 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.tsx +1 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.test.tsx +1 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.tsx +1 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestStatusBadges.stories.tsx +15 -5
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestStatusBadges.test.tsx +38 -8
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestStatusBadges.tsx +35 -12
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleDetailPrefetch.test.ts +117 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleDetailPrefetch.ts +83 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleItemDetailData.test.ts +2 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleItemDetailData.ts +1 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.test.ts +67 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.ts +26 -6
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +21 -0
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +14 -0
- package/src/adapter/entry-points/console/ui/src/features/console/lib/resourceCache.test.ts +48 -0
- package/src/adapter/entry-points/console/ui/src/features/console/lib/resourceCache.ts +24 -0
- package/src/adapter/entry-points/console/ui/src/features/console/logic/types.ts +4 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.test.tsx +1 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +9 -1
- package/src/adapter/entry-points/console/ui/src/features/console/testing/fixtures.ts +1 -0
- package/src/adapter/entry-points/console/ui/src/index.css +10 -1
- package/src/adapter/entry-points/console/ui-dist/assets/index-CJJsRF8i.js +101 -0
- package/src/adapter/entry-points/console/ui-dist/assets/{index-D1V2fi29.css → index-DGVKvwux.css} +1 -1
- package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/src/adapter/entry-points/console/webServer.test.ts +2 -0
- package/src/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.test.ts +40 -4
- package/src/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.ts +12 -6
- package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.test.ts +107 -29
- package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.ts +7 -2
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +1 -0
- package/src/domain/usecases/CheckIssueReviewReadinessUseCase.test.ts +1 -0
- package/src/domain/usecases/IssueRejectionEvaluator.test.ts +1 -0
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +84 -4
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +16 -3
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +1 -0
- package/src/domain/usecases/StartPreparationUseCase.test.ts +9 -0
- package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +1 -0
- package/src/domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository.ts +1 -0
- package/src/domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver.ts +4 -1
- package/types/adapter/entry-points/console/consoleReadApi.d.ts +4 -0
- package/types/adapter/entry-points/console/consoleReadApi.d.ts.map +1 -1
- package/types/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.d.ts +1 -1
- package/types/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.d.ts.map +1 -1
- package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts +1 -1
- package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts.map +1 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +1 -0
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository.d.ts +1 -1
- package/types/domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver.d.ts +1 -1
- package/types/domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver.d.ts.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-Bm0wt3Mn.js +0 -101
- package/src/adapter/entry-points/console/ui-dist/assets/index-Bm0wt3Mn.js +0 -101
|
@@ -130,6 +130,7 @@ describe('ConsoleItemDetail', () => {
|
|
|
130
130
|
pullRequestStatus={{
|
|
131
131
|
found: true,
|
|
132
132
|
isConflicted: true,
|
|
133
|
+
mergeableStatus: 'CONFLICTING',
|
|
133
134
|
isPassedAllCiJob: false,
|
|
134
135
|
isCiStateSuccess: false,
|
|
135
136
|
isBranchOutOfDate: true,
|
|
@@ -161,6 +162,7 @@ describe('ConsoleItemDetail', () => {
|
|
|
161
162
|
pullRequestStatus={{
|
|
162
163
|
found: true,
|
|
163
164
|
isConflicted: false,
|
|
165
|
+
mergeableStatus: 'MERGEABLE',
|
|
164
166
|
isPassedAllCiJob: true,
|
|
165
167
|
isCiStateSuccess: true,
|
|
166
168
|
isBranchOutOfDate: false,
|
|
@@ -169,7 +171,7 @@ describe('ConsoleItemDetail', () => {
|
|
|
169
171
|
/>,
|
|
170
172
|
);
|
|
171
173
|
expect(getByText('CI passing')).toBeInTheDocument();
|
|
172
|
-
expect(
|
|
174
|
+
expect(getByText('No conflict')).toBeInTheDocument();
|
|
173
175
|
expect(queryByText('Out of date')).toBeNull();
|
|
174
176
|
});
|
|
175
177
|
|
|
@@ -158,7 +158,7 @@ export const ConsoleItemDetail = ({
|
|
|
158
158
|
{item.isPr && pullRequestStatus?.found && (
|
|
159
159
|
<div className="console-detail-pr-status-row">
|
|
160
160
|
<ConsolePullRequestStatusBadges
|
|
161
|
-
|
|
161
|
+
mergeableStatus={pullRequestStatus.mergeableStatus}
|
|
162
162
|
isPassedAllCiJob={pullRequestStatus.isPassedAllCiJob}
|
|
163
163
|
isCiStateSuccess={pullRequestStatus.isCiStateSuccess}
|
|
164
164
|
isBranchOutOfDate={pullRequestStatus.isBranchOutOfDate}
|
|
@@ -64,7 +64,7 @@ export const ConsolePullRequestDetail = ({
|
|
|
64
64
|
<span className="console-pr-section-state">draft</span>
|
|
65
65
|
)}
|
|
66
66
|
<ConsolePullRequestStatusBadges
|
|
67
|
-
|
|
67
|
+
mergeableStatus={pullRequest.mergeableStatus}
|
|
68
68
|
isPassedAllCiJob={pullRequest.isPassedAllCiJob}
|
|
69
69
|
isCiStateSuccess={pullRequest.isCiStateSuccess}
|
|
70
70
|
isBranchOutOfDate={pullRequest.isBranchOutOfDate}
|
|
@@ -10,9 +10,9 @@ export default meta;
|
|
|
10
10
|
|
|
11
11
|
type Story = StoryObj<typeof ConsolePullRequestStatusBadges>;
|
|
12
12
|
|
|
13
|
-
export const
|
|
13
|
+
export const PassingAndMergeable: Story = {
|
|
14
14
|
args: {
|
|
15
|
-
|
|
15
|
+
mergeableStatus: 'MERGEABLE',
|
|
16
16
|
isPassedAllCiJob: true,
|
|
17
17
|
isCiStateSuccess: true,
|
|
18
18
|
isBranchOutOfDate: false,
|
|
@@ -22,7 +22,7 @@ export const Passing: Story = {
|
|
|
22
22
|
|
|
23
23
|
export const FailingWithMissingChecks: Story = {
|
|
24
24
|
args: {
|
|
25
|
-
|
|
25
|
+
mergeableStatus: 'MERGEABLE',
|
|
26
26
|
isPassedAllCiJob: false,
|
|
27
27
|
isCiStateSuccess: false,
|
|
28
28
|
isBranchOutOfDate: false,
|
|
@@ -30,12 +30,22 @@ export const FailingWithMissingChecks: Story = {
|
|
|
30
30
|
},
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
-
export const
|
|
33
|
+
export const ConflictingAndOutOfDate: Story = {
|
|
34
34
|
args: {
|
|
35
|
-
|
|
35
|
+
mergeableStatus: 'CONFLICTING',
|
|
36
36
|
isPassedAllCiJob: false,
|
|
37
37
|
isCiStateSuccess: false,
|
|
38
38
|
isBranchOutOfDate: true,
|
|
39
39
|
missingRequiredCheckNames: ['build', 'test'],
|
|
40
40
|
},
|
|
41
41
|
};
|
|
42
|
+
|
|
43
|
+
export const CheckingMergeStatus: Story = {
|
|
44
|
+
args: {
|
|
45
|
+
mergeableStatus: 'UNKNOWN',
|
|
46
|
+
isPassedAllCiJob: true,
|
|
47
|
+
isCiStateSuccess: true,
|
|
48
|
+
isBranchOutOfDate: false,
|
|
49
|
+
missingRequiredCheckNames: [],
|
|
50
|
+
},
|
|
51
|
+
};
|
|
@@ -5,7 +5,7 @@ describe('ConsolePullRequestStatusBadges', () => {
|
|
|
5
5
|
it('renders a passing CI badge when all CI jobs passed and the CI state is success', () => {
|
|
6
6
|
const { getByText, queryByText } = render(
|
|
7
7
|
<ConsolePullRequestStatusBadges
|
|
8
|
-
|
|
8
|
+
mergeableStatus="MERGEABLE"
|
|
9
9
|
isPassedAllCiJob={true}
|
|
10
10
|
isCiStateSuccess={true}
|
|
11
11
|
isBranchOutOfDate={false}
|
|
@@ -13,14 +13,13 @@ describe('ConsolePullRequestStatusBadges', () => {
|
|
|
13
13
|
/>,
|
|
14
14
|
);
|
|
15
15
|
expect(getByText('CI passing')).toBeInTheDocument();
|
|
16
|
-
expect(queryByText('Conflict')).toBeNull();
|
|
17
16
|
expect(queryByText('Out of date')).toBeNull();
|
|
18
17
|
});
|
|
19
18
|
|
|
20
19
|
it('renders a failing CI badge with missing required checks', () => {
|
|
21
20
|
const { getByText } = render(
|
|
22
21
|
<ConsolePullRequestStatusBadges
|
|
23
|
-
|
|
22
|
+
mergeableStatus="MERGEABLE"
|
|
24
23
|
isPassedAllCiJob={false}
|
|
25
24
|
isCiStateSuccess={true}
|
|
26
25
|
isBranchOutOfDate={false}
|
|
@@ -31,10 +30,25 @@ describe('ConsolePullRequestStatusBadges', () => {
|
|
|
31
30
|
expect(getByText(/missing: build, test/)).toBeInTheDocument();
|
|
32
31
|
});
|
|
33
32
|
|
|
34
|
-
it('renders the conflict badge when the pull request is
|
|
35
|
-
const { getByText } = render(
|
|
33
|
+
it('renders the green no-conflict badge when the pull request is mergeable', () => {
|
|
34
|
+
const { getByText, queryByText } = render(
|
|
36
35
|
<ConsolePullRequestStatusBadges
|
|
37
|
-
|
|
36
|
+
mergeableStatus="MERGEABLE"
|
|
37
|
+
isPassedAllCiJob={true}
|
|
38
|
+
isCiStateSuccess={true}
|
|
39
|
+
isBranchOutOfDate={false}
|
|
40
|
+
missingRequiredCheckNames={[]}
|
|
41
|
+
/>,
|
|
42
|
+
);
|
|
43
|
+
expect(getByText('No conflict')).toBeInTheDocument();
|
|
44
|
+
expect(queryByText('Conflict')).toBeNull();
|
|
45
|
+
expect(queryByText('Checking merge status')).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('renders the red conflict badge when the pull request is conflicting', () => {
|
|
49
|
+
const { getByText, queryByText } = render(
|
|
50
|
+
<ConsolePullRequestStatusBadges
|
|
51
|
+
mergeableStatus="CONFLICTING"
|
|
38
52
|
isPassedAllCiJob={true}
|
|
39
53
|
isCiStateSuccess={true}
|
|
40
54
|
isBranchOutOfDate={false}
|
|
@@ -42,12 +56,28 @@ describe('ConsolePullRequestStatusBadges', () => {
|
|
|
42
56
|
/>,
|
|
43
57
|
);
|
|
44
58
|
expect(getByText('Conflict')).toBeInTheDocument();
|
|
59
|
+
expect(queryByText('No conflict')).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('renders the gray checking badge when the merge status is unknown', () => {
|
|
63
|
+
const { getByText, queryByText } = render(
|
|
64
|
+
<ConsolePullRequestStatusBadges
|
|
65
|
+
mergeableStatus="UNKNOWN"
|
|
66
|
+
isPassedAllCiJob={true}
|
|
67
|
+
isCiStateSuccess={true}
|
|
68
|
+
isBranchOutOfDate={false}
|
|
69
|
+
missingRequiredCheckNames={[]}
|
|
70
|
+
/>,
|
|
71
|
+
);
|
|
72
|
+
expect(getByText('Checking merge status')).toBeInTheDocument();
|
|
73
|
+
expect(queryByText('Conflict')).toBeNull();
|
|
74
|
+
expect(queryByText('No conflict')).toBeNull();
|
|
45
75
|
});
|
|
46
76
|
|
|
47
77
|
it('renders the out-of-date badge when the branch is behind the base branch', () => {
|
|
48
78
|
const { getByText } = render(
|
|
49
79
|
<ConsolePullRequestStatusBadges
|
|
50
|
-
|
|
80
|
+
mergeableStatus="MERGEABLE"
|
|
51
81
|
isPassedAllCiJob={true}
|
|
52
82
|
isCiStateSuccess={true}
|
|
53
83
|
isBranchOutOfDate={true}
|
|
@@ -60,7 +90,7 @@ describe('ConsolePullRequestStatusBadges', () => {
|
|
|
60
90
|
it('treats a successful CI state but unfinished jobs as failing', () => {
|
|
61
91
|
const { getByText } = render(
|
|
62
92
|
<ConsolePullRequestStatusBadges
|
|
63
|
-
|
|
93
|
+
mergeableStatus="MERGEABLE"
|
|
64
94
|
isPassedAllCiJob={false}
|
|
65
95
|
isCiStateSuccess={true}
|
|
66
96
|
isBranchOutOfDate={false}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { colorFromEnum } from '../../logic/colors';
|
|
2
|
-
import type { ConsoleColor } from '../../logic/types';
|
|
2
|
+
import type { ConsoleColor, ConsoleMergeableStatus } from '../../logic/types';
|
|
3
3
|
|
|
4
4
|
export type ConsolePullRequestStatusBadgesProps = {
|
|
5
|
-
|
|
5
|
+
mergeableStatus: ConsoleMergeableStatus;
|
|
6
6
|
isPassedAllCiJob: boolean;
|
|
7
7
|
isCiStateSuccess: boolean;
|
|
8
8
|
isBranchOutOfDate: boolean;
|
|
@@ -18,8 +18,32 @@ const badgeStyle = (color: ConsoleColor) => {
|
|
|
18
18
|
};
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
const MERGEABLE_BADGE: Record<
|
|
22
|
+
ConsoleMergeableStatus,
|
|
23
|
+
{ color: ConsoleColor; label: string; title: string; modifier: string }
|
|
24
|
+
> = {
|
|
25
|
+
MERGEABLE: {
|
|
26
|
+
color: 'GREEN',
|
|
27
|
+
label: 'No conflict',
|
|
28
|
+
title: 'This pull request has no merge conflicts',
|
|
29
|
+
modifier: 'console-detail-mergeable-chip-ok',
|
|
30
|
+
},
|
|
31
|
+
CONFLICTING: {
|
|
32
|
+
color: 'RED',
|
|
33
|
+
label: 'Conflict',
|
|
34
|
+
title: 'This pull request has merge conflicts',
|
|
35
|
+
modifier: 'console-detail-mergeable-chip-conflict',
|
|
36
|
+
},
|
|
37
|
+
UNKNOWN: {
|
|
38
|
+
color: 'GRAY',
|
|
39
|
+
label: 'Checking merge status',
|
|
40
|
+
title: 'GitHub has not finished computing the merge status yet',
|
|
41
|
+
modifier: 'console-detail-mergeable-chip-unknown',
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
21
45
|
export const ConsolePullRequestStatusBadges = ({
|
|
22
|
-
|
|
46
|
+
mergeableStatus,
|
|
23
47
|
isPassedAllCiJob,
|
|
24
48
|
isCiStateSuccess,
|
|
25
49
|
isBranchOutOfDate,
|
|
@@ -32,6 +56,7 @@ export const ConsolePullRequestStatusBadges = ({
|
|
|
32
56
|
!ciPassing && missingRequiredCheckNames.length > 0
|
|
33
57
|
? `missing: ${missingRequiredCheckNames.join(', ')}`
|
|
34
58
|
: null;
|
|
59
|
+
const mergeableBadge = MERGEABLE_BADGE[mergeableStatus];
|
|
35
60
|
|
|
36
61
|
return (
|
|
37
62
|
<span className="console-detail-pr-status">
|
|
@@ -48,15 +73,13 @@ export const ConsolePullRequestStatusBadges = ({
|
|
|
48
73
|
</span>
|
|
49
74
|
)}
|
|
50
75
|
</span>
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
</span>
|
|
59
|
-
)}
|
|
76
|
+
<span
|
|
77
|
+
className={`console-detail-status-chip console-detail-mergeable-chip ${mergeableBadge.modifier}`}
|
|
78
|
+
style={badgeStyle(mergeableBadge.color)}
|
|
79
|
+
title={mergeableBadge.title}
|
|
80
|
+
>
|
|
81
|
+
{mergeableBadge.label}
|
|
82
|
+
</span>
|
|
60
83
|
{isBranchOutOfDate && (
|
|
61
84
|
<span
|
|
62
85
|
className="console-detail-status-chip console-detail-outofdate-chip"
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react';
|
|
2
|
+
import { ResourceCache } from '../lib/resourceCache';
|
|
3
|
+
import type { ConsoleListItem } from '../logic/types';
|
|
4
|
+
import type { ConsoleCaches } from './useConsoleCaches';
|
|
5
|
+
import { useConsoleDetailPrefetch } from './useConsoleDetailPrefetch';
|
|
6
|
+
|
|
7
|
+
const makeItem = (
|
|
8
|
+
number: number,
|
|
9
|
+
overrides: Partial<ConsoleListItem> = {},
|
|
10
|
+
): ConsoleListItem => ({
|
|
11
|
+
number,
|
|
12
|
+
title: `Item ${number}`,
|
|
13
|
+
url: `https://github.com/o/r/issues/${number}`,
|
|
14
|
+
repo: 'o/r',
|
|
15
|
+
nameWithOwner: 'o/r',
|
|
16
|
+
projectItemId: `PVTI_${number}`,
|
|
17
|
+
itemId: `PVTI_${number}`,
|
|
18
|
+
isPr: false,
|
|
19
|
+
story: 'Story',
|
|
20
|
+
status: null,
|
|
21
|
+
nextActionDate: null,
|
|
22
|
+
nextActionHour: null,
|
|
23
|
+
dependedIssueUrls: [],
|
|
24
|
+
labels: [],
|
|
25
|
+
createdAt: '2026-06-10T00:00:00.000Z',
|
|
26
|
+
...overrides,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const buildCaches = (): ConsoleCaches => {
|
|
30
|
+
const never = () => new Promise<never>(() => {});
|
|
31
|
+
return {
|
|
32
|
+
client: {} as ConsoleCaches['client'],
|
|
33
|
+
body: new ResourceCache<string>(never),
|
|
34
|
+
comments: new ResourceCache(never),
|
|
35
|
+
files: new ResourceCache(never),
|
|
36
|
+
commits: new ResourceCache(never),
|
|
37
|
+
relatedPrs: new ResourceCache(never),
|
|
38
|
+
state: new ResourceCache(never),
|
|
39
|
+
prStatus: new ResourceCache(never),
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
describe('useConsoleDetailPrefetch', () => {
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
jest.useFakeTimers();
|
|
46
|
+
const idleWindow = window as unknown as Record<string, unknown>;
|
|
47
|
+
idleWindow.requestIdleCallback = undefined;
|
|
48
|
+
idleWindow.cancelIdleCallback = undefined;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
jest.runAllTimers();
|
|
53
|
+
jest.useRealTimers();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('prefetches the next items after the idle defer elapses', () => {
|
|
57
|
+
const caches = buildCaches();
|
|
58
|
+
const bodyPrefetch = jest.spyOn(caches.body, 'prefetch');
|
|
59
|
+
const relatedPrefetch = jest.spyOn(caches.relatedPrs, 'prefetch');
|
|
60
|
+
const items = [makeItem(1), makeItem(2), makeItem(3)];
|
|
61
|
+
renderHook(() => useConsoleDetailPrefetch(caches, items[0], items));
|
|
62
|
+
|
|
63
|
+
expect(bodyPrefetch).not.toHaveBeenCalled();
|
|
64
|
+
jest.runAllTimers();
|
|
65
|
+
|
|
66
|
+
expect(bodyPrefetch).toHaveBeenCalledWith('o/r#2', items[1].url);
|
|
67
|
+
expect(bodyPrefetch).toHaveBeenCalledWith('o/r#3', items[2].url);
|
|
68
|
+
expect(relatedPrefetch).toHaveBeenCalledWith('o/r#2', items[1].url);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('prefetches PR resources for pull request items', () => {
|
|
72
|
+
const caches = buildCaches();
|
|
73
|
+
const filesPrefetch = jest.spyOn(caches.files, 'prefetch');
|
|
74
|
+
const prStatusPrefetch = jest.spyOn(caches.prStatus, 'prefetch');
|
|
75
|
+
const items = [
|
|
76
|
+
makeItem(1),
|
|
77
|
+
makeItem(2, { isPr: true, url: 'https://github.com/o/r/pull/2' }),
|
|
78
|
+
];
|
|
79
|
+
renderHook(() => useConsoleDetailPrefetch(caches, items[0], items));
|
|
80
|
+
jest.runAllTimers();
|
|
81
|
+
|
|
82
|
+
expect(filesPrefetch).toHaveBeenCalledWith('o/r#2', items[1].url);
|
|
83
|
+
expect(prStatusPrefetch).toHaveBeenCalledWith('o/r#2', items[1].url);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('limits prefetch to the next ten items', () => {
|
|
87
|
+
const caches = buildCaches();
|
|
88
|
+
const bodyPrefetch = jest.spyOn(caches.body, 'prefetch');
|
|
89
|
+
const items = Array.from({ length: 15 }, (_unused, index) =>
|
|
90
|
+
makeItem(index + 1),
|
|
91
|
+
);
|
|
92
|
+
renderHook(() => useConsoleDetailPrefetch(caches, items[0], items));
|
|
93
|
+
jest.runAllTimers();
|
|
94
|
+
|
|
95
|
+
expect(bodyPrefetch).toHaveBeenCalledTimes(10);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('does nothing when no item is selected', () => {
|
|
99
|
+
const caches = buildCaches();
|
|
100
|
+
const bodyPrefetch = jest.spyOn(caches.body, 'prefetch');
|
|
101
|
+
const items = [makeItem(1), makeItem(2)];
|
|
102
|
+
renderHook(() => useConsoleDetailPrefetch(caches, null, items));
|
|
103
|
+
jest.runAllTimers();
|
|
104
|
+
|
|
105
|
+
expect(bodyPrefetch).not.toHaveBeenCalled();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('does not prefetch when the selected item is the last in the list', () => {
|
|
109
|
+
const caches = buildCaches();
|
|
110
|
+
const bodyPrefetch = jest.spyOn(caches.body, 'prefetch');
|
|
111
|
+
const items = [makeItem(1), makeItem(2)];
|
|
112
|
+
renderHook(() => useConsoleDetailPrefetch(caches, items[1], items));
|
|
113
|
+
jest.runAllTimers();
|
|
114
|
+
|
|
115
|
+
expect(bodyPrefetch).not.toHaveBeenCalled();
|
|
116
|
+
});
|
|
117
|
+
});
|
package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleDetailPrefetch.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import type { ConsoleListItem } from '../logic/types';
|
|
3
|
+
import type { ConsoleCaches } from './useConsoleCaches';
|
|
4
|
+
|
|
5
|
+
const PREFETCH_AHEAD_COUNT = 10;
|
|
6
|
+
const PREFETCH_DEFER_MS = 500;
|
|
7
|
+
const PREFETCH_IDLE_TIMEOUT_MS = 2000;
|
|
8
|
+
|
|
9
|
+
type IdleHandle = { cancel: () => void };
|
|
10
|
+
|
|
11
|
+
const scheduleOnIdle = (task: () => void): IdleHandle => {
|
|
12
|
+
const idleWindow = window as Window & {
|
|
13
|
+
requestIdleCallback?: (
|
|
14
|
+
callback: () => void,
|
|
15
|
+
options?: { timeout: number },
|
|
16
|
+
) => number;
|
|
17
|
+
cancelIdleCallback?: (handle: number) => void;
|
|
18
|
+
};
|
|
19
|
+
if (
|
|
20
|
+
typeof idleWindow.requestIdleCallback === 'function' &&
|
|
21
|
+
typeof idleWindow.cancelIdleCallback === 'function'
|
|
22
|
+
) {
|
|
23
|
+
const handle = idleWindow.requestIdleCallback(task, {
|
|
24
|
+
timeout: PREFETCH_IDLE_TIMEOUT_MS,
|
|
25
|
+
});
|
|
26
|
+
return { cancel: () => idleWindow.cancelIdleCallback?.(handle) };
|
|
27
|
+
}
|
|
28
|
+
const handle = window.setTimeout(task, 0);
|
|
29
|
+
return { cancel: () => window.clearTimeout(handle) };
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const prefetchItem = (caches: ConsoleCaches, item: ConsoleListItem): void => {
|
|
33
|
+
const key = `${item.repo}#${item.number}`;
|
|
34
|
+
caches.body.prefetch(key, item.url);
|
|
35
|
+
caches.state.prefetch(key, item.url);
|
|
36
|
+
caches.comments.prefetch(key, item.url);
|
|
37
|
+
if (item.isPr) {
|
|
38
|
+
caches.files.prefetch(key, item.url);
|
|
39
|
+
caches.commits.prefetch(key, item.url);
|
|
40
|
+
caches.prStatus.prefetch(key, item.url);
|
|
41
|
+
} else {
|
|
42
|
+
caches.relatedPrs.prefetch(key, item.url);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const useConsoleDetailPrefetch = (
|
|
47
|
+
caches: ConsoleCaches,
|
|
48
|
+
selectedItem: ConsoleListItem | null,
|
|
49
|
+
orderedPendingItems: ConsoleListItem[],
|
|
50
|
+
): void => {
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (selectedItem === null) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const selectedIndex = orderedPendingItems.findIndex(
|
|
56
|
+
(item) => item.projectItemId === selectedItem.projectItemId,
|
|
57
|
+
);
|
|
58
|
+
if (selectedIndex === -1) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const upcoming = orderedPendingItems.slice(
|
|
62
|
+
selectedIndex + 1,
|
|
63
|
+
selectedIndex + 1 + PREFETCH_AHEAD_COUNT,
|
|
64
|
+
);
|
|
65
|
+
if (upcoming.length === 0) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let idleHandle: IdleHandle | null = null;
|
|
70
|
+
const deferHandle = window.setTimeout(() => {
|
|
71
|
+
idleHandle = scheduleOnIdle(() => {
|
|
72
|
+
for (const item of upcoming) {
|
|
73
|
+
prefetchItem(caches, item);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}, PREFETCH_DEFER_MS);
|
|
77
|
+
|
|
78
|
+
return () => {
|
|
79
|
+
window.clearTimeout(deferHandle);
|
|
80
|
+
idleHandle?.cancel();
|
|
81
|
+
};
|
|
82
|
+
}, [caches, selectedItem, orderedPendingItems]);
|
|
83
|
+
};
|
|
@@ -68,6 +68,7 @@ const buildCaches = (related: ConsoleRelatedPullRequest[]): ConsoleCaches => {
|
|
|
68
68
|
fetchPullRequestStatus: async (): Promise<ConsolePullRequestStatus> => ({
|
|
69
69
|
found: true,
|
|
70
70
|
isConflicted: true,
|
|
71
|
+
mergeableStatus: 'CONFLICTING',
|
|
71
72
|
isPassedAllCiJob: false,
|
|
72
73
|
isCiStateSuccess: false,
|
|
73
74
|
isBranchOutOfDate: true,
|
|
@@ -117,6 +118,7 @@ describe('useConsoleItemDetailData', () => {
|
|
|
117
118
|
createdAt: '2026-06-10T00:00:00.000Z',
|
|
118
119
|
isDraft: false,
|
|
119
120
|
isConflicted: false,
|
|
121
|
+
mergeableStatus: 'MERGEABLE',
|
|
120
122
|
isPassedAllCiJob: true,
|
|
121
123
|
isCiStateSuccess: true,
|
|
122
124
|
isResolvedAllReviewComments: true,
|
package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.test.ts
CHANGED
|
@@ -1,15 +1,31 @@
|
|
|
1
1
|
import { act, renderHook } from '@testing-library/react';
|
|
2
|
+
import { ResourceCache } from '../lib/resourceCache';
|
|
2
3
|
import { overlayStorageKey } from '../logic/overlay';
|
|
3
4
|
import {
|
|
4
5
|
consoleListItemsFixture,
|
|
5
6
|
consoleStatusOptionsFixture,
|
|
6
7
|
} from '../testing/fixtures';
|
|
8
|
+
import type { ConsoleCaches } from './useConsoleCaches';
|
|
7
9
|
import { useConsoleOperations } from './useConsoleOperations';
|
|
8
10
|
import { useConsoleOverlay } from './useConsoleOverlay';
|
|
9
11
|
|
|
10
12
|
const prItem = consoleListItemsFixture[0];
|
|
11
13
|
const issueItem = consoleListItemsFixture[2];
|
|
12
14
|
|
|
15
|
+
const buildOperationCaches = (): ConsoleCaches => {
|
|
16
|
+
const never = () => new Promise<never>(() => {});
|
|
17
|
+
return {
|
|
18
|
+
client: {} as ConsoleCaches['client'],
|
|
19
|
+
body: new ResourceCache<string>(never),
|
|
20
|
+
comments: new ResourceCache(never),
|
|
21
|
+
files: new ResourceCache(never),
|
|
22
|
+
commits: new ResourceCache(never),
|
|
23
|
+
relatedPrs: new ResourceCache(never),
|
|
24
|
+
state: new ResourceCache(never),
|
|
25
|
+
prStatus: new ResourceCache(never),
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
13
29
|
const captureFetch = (): jest.Mock => {
|
|
14
30
|
const fetchMock = jest.fn(async () => ({
|
|
15
31
|
ok: true,
|
|
@@ -256,6 +272,57 @@ describe('useConsoleOperations', () => {
|
|
|
256
272
|
});
|
|
257
273
|
});
|
|
258
274
|
|
|
275
|
+
it('invalidates the operated item body and comments cache on a review', async () => {
|
|
276
|
+
captureFetch();
|
|
277
|
+
localStorage.clear();
|
|
278
|
+
window.history.replaceState({}, '', '/projects/umino/prs?k=token');
|
|
279
|
+
const caches = buildOperationCaches();
|
|
280
|
+
const bodyInvalidate = jest.spyOn(caches.body, 'invalidate');
|
|
281
|
+
const commentsInvalidate = jest.spyOn(caches.comments, 'invalidate');
|
|
282
|
+
const { result } = renderHook(() => {
|
|
283
|
+
const overlay = useConsoleOverlay('umino');
|
|
284
|
+
const operations = useConsoleOperations('umino', 'prs', overlay, caches);
|
|
285
|
+
return { overlay, operations };
|
|
286
|
+
});
|
|
287
|
+
await act(async () => {
|
|
288
|
+
await result.current.operations.reviewPullRequest(
|
|
289
|
+
prItem,
|
|
290
|
+
prItem.url,
|
|
291
|
+
'approve',
|
|
292
|
+
);
|
|
293
|
+
});
|
|
294
|
+
const key = `${prItem.repo}#${prItem.number}`;
|
|
295
|
+
expect(bodyInvalidate).toHaveBeenCalledWith(key);
|
|
296
|
+
expect(commentsInvalidate).toHaveBeenCalledWith(key);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('invalidates the operated item cache after posting a comment', async () => {
|
|
300
|
+
const fetchMock: jest.Mock = jest.fn(async () => ({
|
|
301
|
+
ok: true,
|
|
302
|
+
status: 200,
|
|
303
|
+
json: async () => ({
|
|
304
|
+
ok: true,
|
|
305
|
+
comment: { author: 'a', body: 'b', createdAt: 'c' },
|
|
306
|
+
}),
|
|
307
|
+
}));
|
|
308
|
+
global.fetch = fetchMock as unknown as typeof fetch;
|
|
309
|
+
localStorage.clear();
|
|
310
|
+
window.history.replaceState({}, '', '/projects/umino/prs?k=token');
|
|
311
|
+
const caches = buildOperationCaches();
|
|
312
|
+
const commentsInvalidate = jest.spyOn(caches.comments, 'invalidate');
|
|
313
|
+
const { result } = renderHook(() => {
|
|
314
|
+
const overlay = useConsoleOverlay('umino');
|
|
315
|
+
const operations = useConsoleOperations('umino', 'prs', overlay, caches);
|
|
316
|
+
return { overlay, operations };
|
|
317
|
+
});
|
|
318
|
+
await act(async () => {
|
|
319
|
+
await result.current.operations.addComment(issueItem, 'hello');
|
|
320
|
+
});
|
|
321
|
+
expect(commentsInvalidate).toHaveBeenCalledWith(
|
|
322
|
+
`${issueItem.repo}#${issueItem.number}`,
|
|
323
|
+
);
|
|
324
|
+
});
|
|
325
|
+
|
|
259
326
|
it('rejects an operation and posts nothing when no pjcode is available', async () => {
|
|
260
327
|
const fetchMock = captureFetch();
|
|
261
328
|
localStorage.clear();
|