github-issue-tower-defence-management 2.6.1 → 2.8.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 +14 -0
- package/README.md +0 -2
- package/bin/adapter/entry-points/cli/index.js +3 -6
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/cli/projectConfig.js +0 -3
- package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleAirplaneSnapshotApi.js +347 -0
- package/bin/adapter/entry-points/console/consoleAirplaneSnapshotApi.js.map +1 -0
- package/bin/adapter/entry-points/console/ui-dist/assets/index-DkMqSpkk.js +88 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/bin/adapter/entry-points/console/webServer.js +28 -0
- package/bin/adapter/entry-points/console/webServer.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +1 -3
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/domain/usecases/CheckIssueReviewReadinessUseCase.js +4 -2
- package/bin/domain/usecases/CheckIssueReviewReadinessUseCase.js.map +1 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +1 -6
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/IssueRejectionEvaluator.js +0 -23
- package/bin/domain/usecases/IssueRejectionEvaluator.js.map +1 -1
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +39 -69
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js +3 -2
- package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js.map +1 -1
- package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js +0 -43
- package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/autoStatusCheckComments.js +1 -11
- package/bin/domain/usecases/autoStatusCheckComments.js.map +1 -1
- package/bin/domain/usecases/extractNextStepAgentFromComments.js +14 -0
- package/bin/domain/usecases/extractNextStepAgentFromComments.js.map +1 -0
- package/bin/domain/usecases/triagerAgentName.js +7 -0
- package/bin/domain/usecases/triagerAgentName.js.map +1 -0
- package/jest.config.js +1 -0
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.test.ts +0 -2
- package/src/adapter/entry-points/cli/index.ts +4 -10
- package/src/adapter/entry-points/cli/projectConfig.ts +0 -8
- package/src/adapter/entry-points/console/consoleAirplaneSnapshotApi.test.ts +658 -0
- package/src/adapter/entry-points/console/consoleAirplaneSnapshotApi.ts +482 -0
- package/src/adapter/entry-points/console/ui/jest-globals.cjs +26 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleFileDiff.test.tsx +10 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleFileDiff.tsx +23 -16
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleAirplaneModeButton.stories.tsx +56 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleAirplaneModeButton.test.tsx +111 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleAirplaneModeButton.tsx +92 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.stories.tsx +15 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.test.tsx +19 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.tsx +30 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleErrorToast.stories.tsx +10 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleUndoToast.test.tsx +1 -3
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleUndoToast.tsx +8 -7
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useAirplaneMode.test.ts +304 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useAirplaneMode.ts +167 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleActionQueue.test.ts +15 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleActionQueue.ts +6 -1
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleCaches.test.ts +77 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleCaches.ts +48 -3
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleFeaturesConfig.test.ts +51 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleFeaturesConfig.ts +39 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.test.ts +215 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.ts +20 -1
- package/src/adapter/entry-points/console/ui/src/features/console/lib/airplaneSnapshot.test.ts +361 -0
- package/src/adapter/entry-points/console/ui/src/features/console/lib/airplaneSnapshot.ts +401 -0
- package/src/adapter/entry-points/console/ui/src/features/console/lib/resourceCache.ts +4 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.test.tsx +209 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +27 -2
- package/src/adapter/entry-points/console/ui-dist/assets/index-DkMqSpkk.js +88 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/src/adapter/entry-points/console/webServer.test.ts +189 -0
- package/src/adapter/entry-points/console/webServer.ts +38 -0
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +0 -7
- package/src/domain/usecases/CheckIssueReviewReadinessUseCase.test.ts +26 -4
- package/src/domain/usecases/CheckIssueReviewReadinessUseCase.ts +5 -3
- package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +0 -28
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +0 -6
- package/src/domain/usecases/IssueRejectionEvaluator.test.ts +0 -119
- package/src/domain/usecases/IssueRejectionEvaluator.ts +0 -21
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +185 -327
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +68 -138
- package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.test.ts +4 -4
- package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.ts +11 -5
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +0 -313
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.ts +1 -102
- package/src/domain/usecases/autoStatusCheckComments.ts +0 -17
- package/src/domain/usecases/extractNextStepAgentFromComments.ts +13 -0
- package/src/domain/usecases/isHumanComment.test.ts +0 -13
- package/src/domain/usecases/triagerAgentName.ts +4 -0
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/cli/projectConfig.d.ts +0 -1
- package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleAirplaneSnapshotApi.d.ts +90 -0
- package/types/adapter/entry-points/console/consoleAirplaneSnapshotApi.d.ts.map +1 -0
- package/types/adapter/entry-points/console/webServer.d.ts +1 -0
- package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/domain/usecases/CheckIssueReviewReadinessUseCase.d.ts.map +1 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +1 -3
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
- package/types/domain/usecases/IssueRejectionEvaluator.d.ts +0 -1
- package/types/domain/usecases/IssueRejectionEvaluator.d.ts.map +1 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +0 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.d.ts.map +1 -1
- package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts +0 -1
- package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/autoStatusCheckComments.d.ts +0 -4
- package/types/domain/usecases/autoStatusCheckComments.d.ts.map +1 -1
- package/types/domain/usecases/extractNextStepAgentFromComments.d.ts +5 -0
- package/types/domain/usecases/extractNextStepAgentFromComments.d.ts.map +1 -0
- package/types/domain/usecases/triagerAgentName.d.ts +3 -0
- package/types/domain/usecases/triagerAgentName.d.ts.map +1 -0
- package/bin/adapter/entry-points/console/ui-dist/assets/index-CaxYBHiT.js +0 -86
- package/src/adapter/entry-points/console/ui-dist/assets/index-CaxYBHiT.js +0 -86
- package/src/domain/usecases/TriagerApprovalDispatchUseCase.test.ts +0 -1051
- package/src/domain/usecases/TriagerApprovalDispatchUseCase.ts +0 -274
- package/src/domain/usecases/awaitingOwnerApprovalMessage.ts +0 -5
- package/src/domain/usecases/isPullRequestDeclaredUnnecessary.test.ts +0 -162
- package/src/domain/usecases/isPullRequestDeclaredUnnecessary.ts +0 -29
- package/src/domain/usecases/isWaitingForOwnerApproval.test.ts +0 -91
- package/src/domain/usecases/isWaitingForOwnerApproval.ts +0 -17
- package/src/domain/usecases/returnedToAwaitingWorkspaceMessage.ts +0 -5
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { fireEvent, render } from '@testing-library/react';
|
|
2
|
+
import {
|
|
3
|
+
ConsoleAirplaneModeButton,
|
|
4
|
+
type ConsoleAirplaneModeButtonProps,
|
|
5
|
+
} from './ConsoleAirplaneModeButton';
|
|
6
|
+
|
|
7
|
+
const baseProps: ConsoleAirplaneModeButtonProps = {
|
|
8
|
+
status: 'off',
|
|
9
|
+
progress: null,
|
|
10
|
+
capturedAt: null,
|
|
11
|
+
failures: [],
|
|
12
|
+
onStartSync: jest.fn(),
|
|
13
|
+
onTurnOff: jest.fn(),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
describe('ConsoleAirplaneModeButton', () => {
|
|
17
|
+
it('renders a start button when status is off', () => {
|
|
18
|
+
const { getByRole } = render(<ConsoleAirplaneModeButton {...baseProps} />);
|
|
19
|
+
const button = getByRole('button', { name: /airplane mode/i });
|
|
20
|
+
expect(button).not.toBeNull();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('calls onStartSync when the start button is clicked', () => {
|
|
24
|
+
const onStartSync = jest.fn();
|
|
25
|
+
const { getByRole } = render(
|
|
26
|
+
<ConsoleAirplaneModeButton
|
|
27
|
+
{...baseProps}
|
|
28
|
+
status="off"
|
|
29
|
+
onStartSync={onStartSync}
|
|
30
|
+
/>,
|
|
31
|
+
);
|
|
32
|
+
fireEvent.click(getByRole('button', { name: /airplane mode/i }));
|
|
33
|
+
expect(onStartSync).toHaveBeenCalledTimes(1);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('shows progress text when status is syncing', () => {
|
|
37
|
+
const { getByRole } = render(
|
|
38
|
+
<ConsoleAirplaneModeButton
|
|
39
|
+
{...baseProps}
|
|
40
|
+
status="syncing"
|
|
41
|
+
progress={{ fetched: 10, total: 100 }}
|
|
42
|
+
/>,
|
|
43
|
+
);
|
|
44
|
+
const status = getByRole('status');
|
|
45
|
+
expect(status.textContent).toContain('10/100');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('shows Syncing… when progress total is zero', () => {
|
|
49
|
+
const { getByRole } = render(
|
|
50
|
+
<ConsoleAirplaneModeButton
|
|
51
|
+
{...baseProps}
|
|
52
|
+
status="syncing"
|
|
53
|
+
progress={{ fetched: 0, total: 0 }}
|
|
54
|
+
/>,
|
|
55
|
+
);
|
|
56
|
+
expect(getByRole('status').textContent).toContain('Syncing');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('does not show a done/on indicator while syncing', () => {
|
|
60
|
+
const { queryByText } = render(
|
|
61
|
+
<ConsoleAirplaneModeButton
|
|
62
|
+
{...baseProps}
|
|
63
|
+
status="syncing"
|
|
64
|
+
progress={{ fetched: 3, total: 10 }}
|
|
65
|
+
/>,
|
|
66
|
+
);
|
|
67
|
+
expect(queryByText(/turn off/i)).toBeNull();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('shows capturedAt and turn-off button when status is on', () => {
|
|
71
|
+
const { getByText } = render(
|
|
72
|
+
<ConsoleAirplaneModeButton
|
|
73
|
+
{...baseProps}
|
|
74
|
+
status="on"
|
|
75
|
+
capturedAt="2026-01-01T00:00:00Z"
|
|
76
|
+
/>,
|
|
77
|
+
);
|
|
78
|
+
expect(getByText(/2026/)).not.toBeNull();
|
|
79
|
+
expect(getByText(/turn off/i)).not.toBeNull();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('calls onTurnOff when turn-off button is clicked', () => {
|
|
83
|
+
const onTurnOff = jest.fn();
|
|
84
|
+
const { getByRole } = render(
|
|
85
|
+
<ConsoleAirplaneModeButton
|
|
86
|
+
{...baseProps}
|
|
87
|
+
status="on"
|
|
88
|
+
capturedAt="2026-01-01T00:00:00Z"
|
|
89
|
+
onTurnOff={onTurnOff}
|
|
90
|
+
/>,
|
|
91
|
+
);
|
|
92
|
+
fireEvent.click(getByRole('button', { name: /turn off/i }));
|
|
93
|
+
expect(onTurnOff).toHaveBeenCalledTimes(1);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('shows error state with retry button when status is error', () => {
|
|
97
|
+
const onStartSync = jest.fn();
|
|
98
|
+
const { getByRole, getByText } = render(
|
|
99
|
+
<ConsoleAirplaneModeButton
|
|
100
|
+
{...baseProps}
|
|
101
|
+
status="error"
|
|
102
|
+
failures={['https://github.com/o/r/issues/1']}
|
|
103
|
+
onStartSync={onStartSync}
|
|
104
|
+
/>,
|
|
105
|
+
);
|
|
106
|
+
expect(getByText(/sync failed/i)).not.toBeNull();
|
|
107
|
+
const retryBtn = getByRole('button', { name: /retry/i });
|
|
108
|
+
fireEvent.click(retryBtn);
|
|
109
|
+
expect(onStartSync).toHaveBeenCalledTimes(1);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AirplaneModeStatus,
|
|
3
|
+
AirplaneSyncProgress,
|
|
4
|
+
} from '../../hooks/useAirplaneMode';
|
|
5
|
+
|
|
6
|
+
export type ConsoleAirplaneModeButtonProps = {
|
|
7
|
+
status: AirplaneModeStatus;
|
|
8
|
+
progress: AirplaneSyncProgress | null;
|
|
9
|
+
capturedAt: string | null;
|
|
10
|
+
failures: string[];
|
|
11
|
+
onStartSync: () => void;
|
|
12
|
+
onTurnOff: () => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const progressLabel = (progress: AirplaneSyncProgress | null): string => {
|
|
16
|
+
if (progress === null || progress.total === 0) {
|
|
17
|
+
return 'Syncing…';
|
|
18
|
+
}
|
|
19
|
+
return `Syncing ${progress.fetched}/${progress.total}`;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const ConsoleAirplaneModeButton = ({
|
|
23
|
+
status,
|
|
24
|
+
progress,
|
|
25
|
+
capturedAt,
|
|
26
|
+
failures,
|
|
27
|
+
onStartSync,
|
|
28
|
+
onTurnOff,
|
|
29
|
+
}: ConsoleAirplaneModeButtonProps) => {
|
|
30
|
+
if (status === 'on') {
|
|
31
|
+
return (
|
|
32
|
+
<div className="console-airplane-mode console-airplane-mode--on">
|
|
33
|
+
<span className="console-airplane-mode-label">
|
|
34
|
+
✈ Airplane mode
|
|
35
|
+
{capturedAt !== null
|
|
36
|
+
? `: ${new Date(capturedAt).toLocaleString()}`
|
|
37
|
+
: ''}
|
|
38
|
+
</span>
|
|
39
|
+
<button
|
|
40
|
+
type="button"
|
|
41
|
+
className="console-airplane-mode-toggle"
|
|
42
|
+
onClick={onTurnOff}
|
|
43
|
+
>
|
|
44
|
+
Turn off
|
|
45
|
+
</button>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (status === 'syncing') {
|
|
51
|
+
return (
|
|
52
|
+
<div className="console-airplane-mode console-airplane-mode--syncing">
|
|
53
|
+
<span
|
|
54
|
+
className="console-airplane-mode-progress"
|
|
55
|
+
role="status"
|
|
56
|
+
aria-live="polite"
|
|
57
|
+
>
|
|
58
|
+
{progressLabel(progress)}
|
|
59
|
+
</span>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (status === 'error') {
|
|
65
|
+
return (
|
|
66
|
+
<div className="console-airplane-mode console-airplane-mode--error">
|
|
67
|
+
<span className="console-airplane-mode-error-label" role="alert">
|
|
68
|
+
Sync failed{failures.length > 0 ? `: ${failures.length} item(s)` : ''}
|
|
69
|
+
</span>
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
className="console-airplane-mode-toggle"
|
|
73
|
+
onClick={onStartSync}
|
|
74
|
+
>
|
|
75
|
+
Retry
|
|
76
|
+
</button>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className="console-airplane-mode console-airplane-mode--off">
|
|
83
|
+
<button
|
|
84
|
+
type="button"
|
|
85
|
+
className="console-airplane-mode-toggle"
|
|
86
|
+
onClick={onStartSync}
|
|
87
|
+
>
|
|
88
|
+
✈ Airplane mode
|
|
89
|
+
</button>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
};
|
|
@@ -16,6 +16,13 @@ const meta: Meta<typeof ConsoleTabList> = {
|
|
|
16
16
|
tabHref: (tab: ConsoleTabName) => `/projects/acme/${tab}`,
|
|
17
17
|
onSelectTab: () => {},
|
|
18
18
|
onSelectProject: () => {},
|
|
19
|
+
airplaneModeEnabled: true,
|
|
20
|
+
airplaneModeStatus: 'off',
|
|
21
|
+
airplaneModeProgress: null,
|
|
22
|
+
airplaneModeCapturedAt: null,
|
|
23
|
+
airplaneModeFailures: [],
|
|
24
|
+
onAirplaneModeStartSync: () => {},
|
|
25
|
+
onAirplaneModeTurnOff: () => {},
|
|
19
26
|
},
|
|
20
27
|
};
|
|
21
28
|
|
|
@@ -39,6 +46,14 @@ export const AllTabsWithCounts: Story = {
|
|
|
39
46
|
},
|
|
40
47
|
};
|
|
41
48
|
|
|
49
|
+
export const AirplaneModeDisabled: Story = {
|
|
50
|
+
args: {
|
|
51
|
+
activeTab: 'prs',
|
|
52
|
+
counts,
|
|
53
|
+
airplaneModeEnabled: false,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
42
57
|
export const ZeroCountTabsHidden: Story = {
|
|
43
58
|
args: {
|
|
44
59
|
activeTab: 'prs',
|
|
@@ -19,6 +19,13 @@ const baseProps = {
|
|
|
19
19
|
tabHref: (tab: ConsoleTabName) => `/projects/acme/${tab}`,
|
|
20
20
|
onSelectTab: () => {},
|
|
21
21
|
onSelectProject: () => {},
|
|
22
|
+
airplaneModeEnabled: true,
|
|
23
|
+
airplaneModeStatus: 'off' as const,
|
|
24
|
+
airplaneModeProgress: null,
|
|
25
|
+
airplaneModeCapturedAt: null,
|
|
26
|
+
airplaneModeFailures: [],
|
|
27
|
+
onAirplaneModeStartSync: () => {},
|
|
28
|
+
onAirplaneModeTurnOff: () => {},
|
|
22
29
|
};
|
|
23
30
|
|
|
24
31
|
describe('ConsoleTabList', () => {
|
|
@@ -230,4 +237,16 @@ describe('ConsoleTabList', () => {
|
|
|
230
237
|
baseElement.querySelector('.console-tab-pjname-dropdown'),
|
|
231
238
|
).toBeNull();
|
|
232
239
|
});
|
|
240
|
+
|
|
241
|
+
it('does not render the airplane mode button when airplaneModeEnabled is false', () => {
|
|
242
|
+
const { baseElement } = render(
|
|
243
|
+
<ConsoleTabList
|
|
244
|
+
{...baseProps}
|
|
245
|
+
activeTab="prs"
|
|
246
|
+
counts={counts}
|
|
247
|
+
airplaneModeEnabled={false}
|
|
248
|
+
/>,
|
|
249
|
+
);
|
|
250
|
+
expect(baseElement.querySelector('.console-airplane-mode')).toBeNull();
|
|
251
|
+
});
|
|
233
252
|
});
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { type ReactNode, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { createPortal } from 'react-dom';
|
|
3
|
+
import type {
|
|
4
|
+
AirplaneModeStatus,
|
|
5
|
+
AirplaneSyncProgress,
|
|
6
|
+
} from '../../hooks/useAirplaneMode';
|
|
3
7
|
import { CONSOLE_TABS, type ConsoleTabName } from '../../logic/types';
|
|
8
|
+
import { ConsoleAirplaneModeButton } from './ConsoleAirplaneModeButton';
|
|
4
9
|
|
|
5
10
|
export type ConsoleTabBarProps = {
|
|
6
11
|
activeTab: ConsoleTabName;
|
|
@@ -13,6 +18,13 @@ export type ConsoleTabBarProps = {
|
|
|
13
18
|
onSelectTab: (tab: ConsoleTabName) => void;
|
|
14
19
|
onSelectProject: (pjcode: string) => void;
|
|
15
20
|
settingsButton?: ReactNode;
|
|
21
|
+
airplaneModeEnabled: boolean;
|
|
22
|
+
airplaneModeStatus: AirplaneModeStatus;
|
|
23
|
+
airplaneModeProgress: AirplaneSyncProgress | null;
|
|
24
|
+
airplaneModeCapturedAt: string | null;
|
|
25
|
+
airplaneModeFailures: string[];
|
|
26
|
+
onAirplaneModeStartSync: () => void;
|
|
27
|
+
onAirplaneModeTurnOff: () => void;
|
|
16
28
|
};
|
|
17
29
|
|
|
18
30
|
export const ConsoleTabList = ({
|
|
@@ -26,6 +38,13 @@ export const ConsoleTabList = ({
|
|
|
26
38
|
onSelectTab,
|
|
27
39
|
onSelectProject,
|
|
28
40
|
settingsButton,
|
|
41
|
+
airplaneModeEnabled,
|
|
42
|
+
airplaneModeStatus,
|
|
43
|
+
airplaneModeProgress,
|
|
44
|
+
airplaneModeCapturedAt,
|
|
45
|
+
airplaneModeFailures,
|
|
46
|
+
onAirplaneModeStartSync,
|
|
47
|
+
onAirplaneModeTurnOff,
|
|
29
48
|
}: ConsoleTabBarProps) => {
|
|
30
49
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
|
31
50
|
const [dropdownPos, setDropdownPos] = useState<{
|
|
@@ -153,7 +172,7 @@ export const ConsoleTabList = ({
|
|
|
153
172
|
)}
|
|
154
173
|
</div>
|
|
155
174
|
)}
|
|
156
|
-
{generatedAt !== null && (
|
|
175
|
+
{generatedAt !== null && airplaneModeStatus !== 'on' && (
|
|
157
176
|
<span
|
|
158
177
|
className="console-tab-geninfo"
|
|
159
178
|
data-from-cache={fromCache ? 'true' : undefined}
|
|
@@ -164,6 +183,16 @@ export const ConsoleTabList = ({
|
|
|
164
183
|
{settingsButton !== undefined && (
|
|
165
184
|
<span className="console-tab-settings">{settingsButton}</span>
|
|
166
185
|
)}
|
|
186
|
+
{airplaneModeEnabled && (
|
|
187
|
+
<ConsoleAirplaneModeButton
|
|
188
|
+
status={airplaneModeStatus}
|
|
189
|
+
progress={airplaneModeProgress}
|
|
190
|
+
capturedAt={airplaneModeCapturedAt}
|
|
191
|
+
failures={airplaneModeFailures}
|
|
192
|
+
onStartSync={onAirplaneModeStartSync}
|
|
193
|
+
onTurnOff={onAirplaneModeTurnOff}
|
|
194
|
+
/>
|
|
195
|
+
)}
|
|
167
196
|
</nav>
|
|
168
197
|
);
|
|
169
198
|
};
|
|
@@ -5,6 +5,7 @@ const meta: Meta<typeof ConsoleErrorToast> = {
|
|
|
5
5
|
title: 'Console/ConsoleErrorToast',
|
|
6
6
|
component: ConsoleErrorToast,
|
|
7
7
|
args: {
|
|
8
|
+
title: 'Approved — PR #851',
|
|
8
9
|
message: 'Operation failed: HTTP 422 Review cannot be requested',
|
|
9
10
|
onDismiss: () => {},
|
|
10
11
|
},
|
|
@@ -18,6 +19,15 @@ export const ReviewRejectedByGitHub: Story = {};
|
|
|
18
19
|
|
|
19
20
|
export const NetworkFailure: Story = {
|
|
20
21
|
args: {
|
|
22
|
+
title: 'Approved — PR #851',
|
|
21
23
|
message: 'Operation failed: network down',
|
|
22
24
|
},
|
|
23
25
|
};
|
|
26
|
+
|
|
27
|
+
export const AirplaneModeGuard: Story = {
|
|
28
|
+
args: {
|
|
29
|
+
title: 'Airplane mode',
|
|
30
|
+
message:
|
|
31
|
+
'Operation failed: This action requires a network connection. Turn off airplane mode and try again.',
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -62,9 +62,7 @@ describe('ConsoleErrorToast', () => {
|
|
|
62
62
|
/>,
|
|
63
63
|
);
|
|
64
64
|
expect(getByText('Operation failed: HTTP 422')).toBeInTheDocument();
|
|
65
|
-
expect(
|
|
66
|
-
container.querySelector('.console-undo-toast-error'),
|
|
67
|
-
).toBeInTheDocument();
|
|
65
|
+
expect(container.querySelector('.console-error-toast')).toBeInTheDocument();
|
|
68
66
|
expect(getByRole('alert')).toBeInTheDocument();
|
|
69
67
|
});
|
|
70
68
|
|
|
@@ -36,23 +36,24 @@ export const ConsoleUndoToast = ({
|
|
|
36
36
|
);
|
|
37
37
|
|
|
38
38
|
export type ConsoleErrorToastProps = {
|
|
39
|
+
title?: string;
|
|
39
40
|
message: string;
|
|
40
41
|
onDismiss: () => void;
|
|
41
42
|
};
|
|
42
43
|
|
|
43
44
|
export const ConsoleErrorToast = ({
|
|
45
|
+
title,
|
|
44
46
|
message,
|
|
45
47
|
onDismiss,
|
|
46
48
|
}: ConsoleErrorToastProps) => (
|
|
47
|
-
<div
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
<span className="console-undo-toast-message">{message}</span>
|
|
49
|
+
<div className="console-error-toast" role="alert" aria-live="assertive">
|
|
50
|
+
{title !== undefined && (
|
|
51
|
+
<span className="console-error-toast-title">{title}</span>
|
|
52
|
+
)}
|
|
53
|
+
<span className="console-error-toast-message">{message}</span>
|
|
53
54
|
<button
|
|
54
55
|
type="button"
|
|
55
|
-
className="console-
|
|
56
|
+
className="console-error-toast-dismiss"
|
|
56
57
|
onClick={onDismiss}
|
|
57
58
|
>
|
|
58
59
|
Dismiss
|
package/src/adapter/entry-points/console/ui/src/features/console/hooks/useAirplaneMode.test.ts
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { act, renderHook, waitFor } from '@testing-library/react';
|
|
2
|
+
import {
|
|
3
|
+
type AirplaneSnapshot,
|
|
4
|
+
readAirplaneModeFlag,
|
|
5
|
+
writeAirplaneModeFlag,
|
|
6
|
+
} from '../lib/airplaneSnapshot';
|
|
7
|
+
import { useAirplaneMode } from './useAirplaneMode';
|
|
8
|
+
|
|
9
|
+
const makeMinimalSnapshot = (
|
|
10
|
+
overrides: Partial<AirplaneSnapshot> = {},
|
|
11
|
+
): AirplaneSnapshot => ({
|
|
12
|
+
capturedAt: '2026-01-01T00:00:00Z',
|
|
13
|
+
tabs: {},
|
|
14
|
+
items: {},
|
|
15
|
+
failures: [],
|
|
16
|
+
...overrides,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const encodeSseEvent = (data: unknown): string =>
|
|
20
|
+
`data: ${JSON.stringify(data)}\n\n`;
|
|
21
|
+
|
|
22
|
+
const buildSseBody = (events: unknown[]): ReadableStream<Uint8Array> => {
|
|
23
|
+
const encoder = new TextEncoder();
|
|
24
|
+
const chunks = events.map((e) => encoder.encode(encodeSseEvent(e)));
|
|
25
|
+
return new ReadableStream<Uint8Array>({
|
|
26
|
+
start(controller) {
|
|
27
|
+
for (const chunk of chunks) {
|
|
28
|
+
controller.enqueue(chunk);
|
|
29
|
+
}
|
|
30
|
+
controller.close();
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const mockFetchSse = (events: unknown[]): void => {
|
|
36
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
37
|
+
ok: true,
|
|
38
|
+
status: 200,
|
|
39
|
+
body: buildSseBody(events),
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const mockCacheApi = (): {
|
|
44
|
+
stored: AirplaneSnapshot | null;
|
|
45
|
+
cacheStore: Map<string, string>;
|
|
46
|
+
} => {
|
|
47
|
+
let stored: AirplaneSnapshot | null = null;
|
|
48
|
+
const cacheStore = new Map<string, string>();
|
|
49
|
+
|
|
50
|
+
const mockCache = {
|
|
51
|
+
put: jest.fn(async (_key: string, response: Response) => {
|
|
52
|
+
const text = await response.text();
|
|
53
|
+
cacheStore.set('snapshot', text);
|
|
54
|
+
stored = JSON.parse(text) as AirplaneSnapshot;
|
|
55
|
+
}),
|
|
56
|
+
match: jest.fn(async () => {
|
|
57
|
+
if (!cacheStore.has('snapshot')) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
return new Response(cacheStore.get('snapshot'), {
|
|
61
|
+
headers: { 'Content-Type': 'application/json' },
|
|
62
|
+
});
|
|
63
|
+
}),
|
|
64
|
+
delete: jest.fn(),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
Object.defineProperty(global, 'caches', {
|
|
68
|
+
writable: true,
|
|
69
|
+
value: {
|
|
70
|
+
open: jest.fn().mockResolvedValue(mockCache),
|
|
71
|
+
delete: jest.fn().mockResolvedValue(true),
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return { stored: stored, cacheStore };
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
describe('useAirplaneMode', () => {
|
|
79
|
+
beforeEach(() => {
|
|
80
|
+
localStorage.clear();
|
|
81
|
+
mockCacheApi();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('starts in off status', () => {
|
|
85
|
+
const { result } = renderHook(() => useAirplaneMode());
|
|
86
|
+
expect(result.current.status).toBe('off');
|
|
87
|
+
expect(result.current.snapshot).toBeNull();
|
|
88
|
+
expect(result.current.progress).toBeNull();
|
|
89
|
+
expect(result.current.failures).toEqual([]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('does not report on while a fetch is outstanding', async () => {
|
|
93
|
+
let resolveStream!: () => void;
|
|
94
|
+
const streamBlocked = new Promise<void>((res) => {
|
|
95
|
+
resolveStream = res;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const encoder = new TextEncoder();
|
|
99
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
100
|
+
ok: true,
|
|
101
|
+
status: 200,
|
|
102
|
+
body: new ReadableStream<Uint8Array>({
|
|
103
|
+
start(controller) {
|
|
104
|
+
controller.enqueue(
|
|
105
|
+
encoder.encode(
|
|
106
|
+
encodeSseEvent({ type: 'progress', fetched: 0, total: 5 }),
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
streamBlocked
|
|
110
|
+
.then(() => {
|
|
111
|
+
controller.close();
|
|
112
|
+
})
|
|
113
|
+
.catch(() => {});
|
|
114
|
+
},
|
|
115
|
+
}),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const { result } = renderHook(() => useAirplaneMode());
|
|
119
|
+
|
|
120
|
+
act(() => {
|
|
121
|
+
result.current.startSync();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await waitFor(() => {
|
|
125
|
+
expect(result.current.status).toBe('syncing');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(result.current.status).not.toBe('on');
|
|
129
|
+
|
|
130
|
+
resolveStream();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('transitions to on only after the done event with no failures', async () => {
|
|
134
|
+
const snapshot = makeMinimalSnapshot();
|
|
135
|
+
mockFetchSse([
|
|
136
|
+
{ type: 'progress', fetched: 0, total: 1 },
|
|
137
|
+
{ type: 'progress', fetched: 1, total: 1 },
|
|
138
|
+
{ type: 'done', snapshot },
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
const { result } = renderHook(() => useAirplaneMode());
|
|
142
|
+
|
|
143
|
+
act(() => {
|
|
144
|
+
result.current.startSync();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await waitFor(() => {
|
|
148
|
+
expect(result.current.status).toBe('on');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
expect(result.current.snapshot).not.toBeNull();
|
|
152
|
+
expect(result.current.snapshot?.capturedAt).toBe('2026-01-01T00:00:00Z');
|
|
153
|
+
expect(result.current.failures).toEqual([]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('sets status to error and does not turn on when the done event has failures', async () => {
|
|
157
|
+
const snapshot = makeMinimalSnapshot({
|
|
158
|
+
failures: ['https://github.com/o/r/issues/99'],
|
|
159
|
+
});
|
|
160
|
+
mockFetchSse([
|
|
161
|
+
{ type: 'progress', fetched: 0, total: 1 },
|
|
162
|
+
{ type: 'done', snapshot },
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
const { result } = renderHook(() => useAirplaneMode());
|
|
166
|
+
|
|
167
|
+
act(() => {
|
|
168
|
+
result.current.startSync();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
await waitFor(() => {
|
|
172
|
+
expect(result.current.status).toBe('error');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
expect(result.current.status).toBe('error');
|
|
176
|
+
expect(result.current.failures).toContain(
|
|
177
|
+
'https://github.com/o/r/issues/99',
|
|
178
|
+
);
|
|
179
|
+
expect(result.current.snapshot).toBeNull();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('sets status to error when fetch fails', async () => {
|
|
183
|
+
global.fetch = jest.fn().mockRejectedValue(new Error('network down'));
|
|
184
|
+
|
|
185
|
+
const { result } = renderHook(() => useAirplaneMode());
|
|
186
|
+
|
|
187
|
+
act(() => {
|
|
188
|
+
result.current.startSync();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
await waitFor(() => {
|
|
192
|
+
expect(result.current.status).toBe('error');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
expect(result.current.failures).toContain('network down');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('turnOff resets to off and clears snapshot', async () => {
|
|
199
|
+
const snapshot = makeMinimalSnapshot();
|
|
200
|
+
mockFetchSse([
|
|
201
|
+
{ type: 'progress', fetched: 0, total: 0 },
|
|
202
|
+
{ type: 'done', snapshot },
|
|
203
|
+
]);
|
|
204
|
+
|
|
205
|
+
const { result } = renderHook(() => useAirplaneMode());
|
|
206
|
+
|
|
207
|
+
act(() => {
|
|
208
|
+
result.current.startSync();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
await waitFor(() => {
|
|
212
|
+
expect(result.current.status).toBe('on');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
act(() => {
|
|
216
|
+
result.current.turnOff();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
expect(result.current.status).toBe('off');
|
|
220
|
+
expect(result.current.snapshot).toBeNull();
|
|
221
|
+
expect(result.current.failures).toEqual([]);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('loads stored snapshot from Cache API when flag is on at mount', async () => {
|
|
225
|
+
const snapshot = makeMinimalSnapshot({
|
|
226
|
+
capturedAt: '2025-06-01T00:00:00Z',
|
|
227
|
+
});
|
|
228
|
+
writeAirplaneModeFlag(true);
|
|
229
|
+
|
|
230
|
+
const cacheStore = new Map<string, string>();
|
|
231
|
+
cacheStore.set('snapshot', JSON.stringify(snapshot));
|
|
232
|
+
|
|
233
|
+
const mockCache = {
|
|
234
|
+
put: jest.fn(),
|
|
235
|
+
match: jest.fn().mockResolvedValue(
|
|
236
|
+
new Response(cacheStore.get('snapshot'), {
|
|
237
|
+
headers: { 'Content-Type': 'application/json' },
|
|
238
|
+
}),
|
|
239
|
+
),
|
|
240
|
+
};
|
|
241
|
+
Object.defineProperty(global, 'caches', {
|
|
242
|
+
writable: true,
|
|
243
|
+
value: {
|
|
244
|
+
open: jest.fn().mockResolvedValue(mockCache),
|
|
245
|
+
delete: jest.fn().mockResolvedValue(true),
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const { result } = renderHook(() => useAirplaneMode());
|
|
250
|
+
|
|
251
|
+
await waitFor(() => {
|
|
252
|
+
expect(result.current.status).toBe('on');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
expect(result.current.snapshot?.capturedAt).toBe('2025-06-01T00:00:00Z');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('advances progress events during sync', async () => {
|
|
259
|
+
mockFetchSse([
|
|
260
|
+
{ type: 'progress', fetched: 0, total: 2 },
|
|
261
|
+
{ type: 'progress', fetched: 1, total: 2 },
|
|
262
|
+
{ type: 'progress', fetched: 2, total: 2 },
|
|
263
|
+
{ type: 'done', snapshot: makeMinimalSnapshot() },
|
|
264
|
+
]);
|
|
265
|
+
|
|
266
|
+
const { result } = renderHook(() => useAirplaneMode());
|
|
267
|
+
|
|
268
|
+
act(() => {
|
|
269
|
+
result.current.startSync();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
await waitFor(() => {
|
|
273
|
+
expect(result.current.progress?.total).toBe(2);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
await waitFor(() => {
|
|
277
|
+
expect(result.current.status).toBe('on');
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('sets status to error and does not write the flag when storeAirplaneSnapshot rejects', async () => {
|
|
282
|
+
Object.defineProperty(global, 'caches', {
|
|
283
|
+
writable: true,
|
|
284
|
+
value: {
|
|
285
|
+
open: jest.fn().mockRejectedValue(new Error('Cache API unavailable')),
|
|
286
|
+
delete: jest.fn().mockResolvedValue(true),
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
mockFetchSse([{ type: 'done', snapshot: makeMinimalSnapshot() }]);
|
|
290
|
+
|
|
291
|
+
const { result } = renderHook(() => useAirplaneMode());
|
|
292
|
+
|
|
293
|
+
act(() => {
|
|
294
|
+
result.current.startSync();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
await waitFor(() => {
|
|
298
|
+
expect(result.current.status).toBe('error');
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
expect(readAirplaneModeFlag()).toBe(false);
|
|
302
|
+
expect(result.current.snapshot).toBeNull();
|
|
303
|
+
});
|
|
304
|
+
});
|