github-issue-tower-defence-management 1.116.9 → 1.117.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.
- package/CHANGELOG.md +7 -0
- 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/issue/ApiV3CheerioRestIssueRepository.js +1 -0
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.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/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/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/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/issue/ApiV3CheerioRestIssueRepository.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/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
|
@@ -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();
|
package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
ConsoleListItem,
|
|
23
23
|
ConsoleTabName,
|
|
24
24
|
} from '../logic/types';
|
|
25
|
+
import type { ConsoleCaches } from './useConsoleCaches';
|
|
25
26
|
import type { ConsoleOverlayState } from './useConsoleOverlay';
|
|
26
27
|
import { useConsoleToken } from './useConsoleToken';
|
|
27
28
|
|
|
@@ -113,15 +114,29 @@ export const useConsoleOperations = (
|
|
|
113
114
|
pjcode: string | null,
|
|
114
115
|
mode: ConsoleTabName,
|
|
115
116
|
overlayState: ConsoleOverlayState,
|
|
117
|
+
caches?: ConsoleCaches,
|
|
116
118
|
): ConsoleOperationsApi => {
|
|
117
119
|
const { appendToken } = useConsoleToken();
|
|
118
120
|
const { patchOverlay } = overlayState;
|
|
119
121
|
|
|
122
|
+
const invalidateItemContent = useCallback(
|
|
123
|
+
(item: ConsoleListItem) => {
|
|
124
|
+
if (caches === undefined) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const key = `${item.repo}#${item.number}`;
|
|
128
|
+
caches.body.invalidate(key);
|
|
129
|
+
caches.comments.invalidate(key);
|
|
130
|
+
},
|
|
131
|
+
[caches],
|
|
132
|
+
);
|
|
133
|
+
|
|
120
134
|
const markDone = useCallback(
|
|
121
135
|
(item: ConsoleListItem) => {
|
|
136
|
+
invalidateItemContent(item);
|
|
122
137
|
patchOverlay(overlayKeyForItem(item), { done: true }, mode);
|
|
123
138
|
},
|
|
124
|
-
[patchOverlay, mode],
|
|
139
|
+
[invalidateItemContent, patchOverlay, mode],
|
|
125
140
|
);
|
|
126
141
|
|
|
127
142
|
const reviewPullRequest = useCallback(
|
|
@@ -173,13 +188,14 @@ export const useConsoleOperations = (
|
|
|
173
188
|
storyOptionId: option.id,
|
|
174
189
|
};
|
|
175
190
|
await postConsoleOperation(appendToken, TRIAGE_OPERATION_PATH, request);
|
|
191
|
+
invalidateItemContent(item);
|
|
176
192
|
patchOverlay(
|
|
177
193
|
overlayKeyForItem(item),
|
|
178
194
|
{ done: true, story: { name: option.name, color: option.color } },
|
|
179
195
|
mode,
|
|
180
196
|
);
|
|
181
197
|
},
|
|
182
|
-
[pjcode, appendToken, patchOverlay, mode],
|
|
198
|
+
[pjcode, appendToken, invalidateItemContent, patchOverlay, mode],
|
|
183
199
|
);
|
|
184
200
|
|
|
185
201
|
const setStatus = useCallback(
|
|
@@ -195,13 +211,14 @@ export const useConsoleOperations = (
|
|
|
195
211
|
statusName: option.name,
|
|
196
212
|
};
|
|
197
213
|
await postConsoleOperation(appendToken, TRIAGE_OPERATION_PATH, request);
|
|
214
|
+
invalidateItemContent(item);
|
|
198
215
|
patchOverlay(
|
|
199
216
|
overlayKeyForItem(item),
|
|
200
217
|
{ done: true, status: { name: option.name, color: option.color } },
|
|
201
218
|
mode,
|
|
202
219
|
);
|
|
203
220
|
},
|
|
204
|
-
[pjcode, appendToken, patchOverlay, mode],
|
|
221
|
+
[pjcode, appendToken, invalidateItemContent, patchOverlay, mode],
|
|
205
222
|
);
|
|
206
223
|
|
|
207
224
|
const setInTmuxByHuman = useCallback(
|
|
@@ -216,13 +233,14 @@ export const useConsoleOperations = (
|
|
|
216
233
|
projectItemId: item.projectItemId,
|
|
217
234
|
};
|
|
218
235
|
await postConsoleOperation(appendToken, INTMUX_OPERATION_PATH, request);
|
|
236
|
+
invalidateItemContent(item);
|
|
219
237
|
patchOverlay(
|
|
220
238
|
overlayKeyForItem(item),
|
|
221
239
|
{ done: true, status: { name: option.name, color: option.color } },
|
|
222
240
|
mode,
|
|
223
241
|
);
|
|
224
242
|
},
|
|
225
|
-
[pjcode, appendToken, patchOverlay, mode],
|
|
243
|
+
[pjcode, appendToken, invalidateItemContent, patchOverlay, mode],
|
|
226
244
|
);
|
|
227
245
|
|
|
228
246
|
const closeIssue = useCallback(
|
|
@@ -247,13 +265,15 @@ export const useConsoleOperations = (
|
|
|
247
265
|
if (pjcode === null) {
|
|
248
266
|
throw missingPjcodeError();
|
|
249
267
|
}
|
|
250
|
-
|
|
268
|
+
const comment = await postConsoleComment(appendToken, {
|
|
251
269
|
pjcode,
|
|
252
270
|
url: item.url,
|
|
253
271
|
body,
|
|
254
272
|
});
|
|
273
|
+
invalidateItemContent(item);
|
|
274
|
+
return comment;
|
|
255
275
|
},
|
|
256
|
-
[pjcode, appendToken],
|
|
276
|
+
[pjcode, appendToken, invalidateItemContent],
|
|
257
277
|
);
|
|
258
278
|
|
|
259
279
|
const addInlineReviewComment = useCallback(
|
|
@@ -191,6 +191,7 @@ describe('createConsoleApiClient', () => {
|
|
|
191
191
|
found: true,
|
|
192
192
|
status: {
|
|
193
193
|
isConflicted: true,
|
|
194
|
+
mergeableStatus: 'CONFLICTING',
|
|
194
195
|
isPassedAllCiJob: false,
|
|
195
196
|
isCiStateSuccess: false,
|
|
196
197
|
isBranchOutOfDate: true,
|
|
@@ -203,6 +204,7 @@ describe('createConsoleApiClient', () => {
|
|
|
203
204
|
).toEqual({
|
|
204
205
|
found: true,
|
|
205
206
|
isConflicted: true,
|
|
207
|
+
mergeableStatus: 'CONFLICTING',
|
|
206
208
|
isPassedAllCiJob: false,
|
|
207
209
|
isCiStateSuccess: false,
|
|
208
210
|
isBranchOutOfDate: true,
|
|
@@ -210,6 +212,24 @@ describe('createConsoleApiClient', () => {
|
|
|
210
212
|
});
|
|
211
213
|
});
|
|
212
214
|
|
|
215
|
+
it('defaults the mergeable status to unknown when the server omits it', async () => {
|
|
216
|
+
mockFetchOnce({
|
|
217
|
+
found: true,
|
|
218
|
+
status: {
|
|
219
|
+
isConflicted: false,
|
|
220
|
+
isPassedAllCiJob: true,
|
|
221
|
+
isCiStateSuccess: true,
|
|
222
|
+
isBranchOutOfDate: false,
|
|
223
|
+
missingRequiredCheckNames: [],
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
const client = createConsoleApiClient(appendToken);
|
|
227
|
+
const status = await client.fetchPullRequestStatus(
|
|
228
|
+
'https://github.com/o/r/pull/1',
|
|
229
|
+
);
|
|
230
|
+
expect(status.mergeableStatus).toBe('UNKNOWN');
|
|
231
|
+
});
|
|
232
|
+
|
|
213
233
|
it('returns a not-found pull request status when the server reports none', async () => {
|
|
214
234
|
mockFetchOnce({ found: false, status: null });
|
|
215
235
|
const client = createConsoleApiClient(appendToken);
|
|
@@ -218,6 +238,7 @@ describe('createConsoleApiClient', () => {
|
|
|
218
238
|
).toEqual({
|
|
219
239
|
found: false,
|
|
220
240
|
isConflicted: false,
|
|
241
|
+
mergeableStatus: 'UNKNOWN',
|
|
221
242
|
isPassedAllCiJob: false,
|
|
222
243
|
isCiStateSuccess: false,
|
|
223
244
|
isBranchOutOfDate: false,
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
ConsoleComment,
|
|
4
4
|
ConsoleCommit,
|
|
5
5
|
ConsoleIssueState,
|
|
6
|
+
ConsoleMergeableStatus,
|
|
6
7
|
ConsolePullRequestStatus,
|
|
7
8
|
ConsoleRelatedPullRequest,
|
|
8
9
|
} from '../logic/types';
|
|
@@ -136,6 +137,16 @@ const parseStringArray = (value: unknown): string[] =>
|
|
|
136
137
|
? value.filter((name): name is string => typeof name === 'string')
|
|
137
138
|
: [];
|
|
138
139
|
|
|
140
|
+
const parseMergeableStatus = (value: unknown): ConsoleMergeableStatus => {
|
|
141
|
+
if (value === 'MERGEABLE') {
|
|
142
|
+
return 'MERGEABLE';
|
|
143
|
+
}
|
|
144
|
+
if (value === 'CONFLICTING') {
|
|
145
|
+
return 'CONFLICTING';
|
|
146
|
+
}
|
|
147
|
+
return 'UNKNOWN';
|
|
148
|
+
};
|
|
149
|
+
|
|
139
150
|
const parseRelatedPrs = (payload: unknown): ConsoleRelatedPullRequest[] => {
|
|
140
151
|
if (!isRecord(payload) || !Array.isArray(payload.relatedPullRequests)) {
|
|
141
152
|
return [];
|
|
@@ -146,6 +157,7 @@ const parseRelatedPrs = (payload: unknown): ConsoleRelatedPullRequest[] => {
|
|
|
146
157
|
createdAt: getString(pr.createdAt),
|
|
147
158
|
isDraft: getBoolean(pr.isDraft),
|
|
148
159
|
isConflicted: getBoolean(pr.isConflicted),
|
|
160
|
+
mergeableStatus: parseMergeableStatus(pr.mergeableStatus),
|
|
149
161
|
isPassedAllCiJob: getBoolean(pr.isPassedAllCiJob),
|
|
150
162
|
isCiStateSuccess: getBoolean(pr.isCiStateSuccess),
|
|
151
163
|
isResolvedAllReviewComments: getBoolean(pr.isResolvedAllReviewComments),
|
|
@@ -160,6 +172,7 @@ const parsePullRequestStatus = (payload: unknown): ConsolePullRequestStatus => {
|
|
|
160
172
|
return {
|
|
161
173
|
found: false,
|
|
162
174
|
isConflicted: false,
|
|
175
|
+
mergeableStatus: 'UNKNOWN',
|
|
163
176
|
isPassedAllCiJob: false,
|
|
164
177
|
isCiStateSuccess: false,
|
|
165
178
|
isBranchOutOfDate: false,
|
|
@@ -170,6 +183,7 @@ const parsePullRequestStatus = (payload: unknown): ConsolePullRequestStatus => {
|
|
|
170
183
|
return {
|
|
171
184
|
found: true,
|
|
172
185
|
isConflicted: getBoolean(status.isConflicted),
|
|
186
|
+
mergeableStatus: parseMergeableStatus(status.mergeableStatus),
|
|
173
187
|
isPassedAllCiJob: getBoolean(status.isPassedAllCiJob),
|
|
174
188
|
isCiStateSuccess: getBoolean(status.isCiStateSuccess),
|
|
175
189
|
isBranchOutOfDate: getBoolean(status.isBranchOutOfDate),
|