github-issue-tower-defence-management 2.12.1 → 2.13.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/ui-dist/assets/{index-D6QOQvoN.js → index-BTFgj8n5.js} +31 -31
- package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleOfflinePendingActionsPanel.stories.tsx +115 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleOfflinePendingActionsPanel.test.tsx +262 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleOfflinePendingActionsPanel.tsx +123 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleActionQueue.test.ts +283 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleActionQueue.ts +146 -4
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.ts +28 -1
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +2 -2
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +103 -1
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +128 -0
- package/src/adapter/entry-points/console/ui-dist/assets/{index-D6QOQvoN.js → index-BTFgj8n5.js} +31 -31
- package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>TDPM Console</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-BTFgj8n5.js"></script>
|
|
8
8
|
<link rel="stylesheet" crossorigin href="/assets/index-CLHvrafO.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
package/package.json
CHANGED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { ConsoleOfflinePendingActionsPanel } from './ConsoleOfflinePendingActionsPanel';
|
|
3
|
+
|
|
4
|
+
const approveAction = {
|
|
5
|
+
id: 'offline-1',
|
|
6
|
+
message: 'Approved — PR #851',
|
|
7
|
+
color: 'green' as const,
|
|
8
|
+
itemNumber: 851,
|
|
9
|
+
isPr: true,
|
|
10
|
+
currentTitle: 'Add serveConsole subcommand under entry-points',
|
|
11
|
+
currentState: 'open' as const,
|
|
12
|
+
currentPrStatus: {
|
|
13
|
+
found: true,
|
|
14
|
+
isConflicted: false,
|
|
15
|
+
mergeableStatus: 'MERGEABLE' as const,
|
|
16
|
+
isPassedAllCiJob: true,
|
|
17
|
+
isCiStateSuccess: true,
|
|
18
|
+
isBranchOutOfDate: false,
|
|
19
|
+
missingRequiredCheckNames: [],
|
|
20
|
+
},
|
|
21
|
+
fetchError: false,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const rejectAction = {
|
|
25
|
+
id: 'offline-2',
|
|
26
|
+
message: 'Rejected — PR #853',
|
|
27
|
+
color: 'amber' as const,
|
|
28
|
+
itemNumber: 853,
|
|
29
|
+
isPr: true,
|
|
30
|
+
currentTitle: 'Add server-side console API handlers',
|
|
31
|
+
currentState: 'open' as const,
|
|
32
|
+
currentPrStatus: {
|
|
33
|
+
found: true,
|
|
34
|
+
isConflicted: true,
|
|
35
|
+
mergeableStatus: 'CONFLICTING' as const,
|
|
36
|
+
isPassedAllCiJob: false,
|
|
37
|
+
isCiStateSuccess: false,
|
|
38
|
+
isBranchOutOfDate: true,
|
|
39
|
+
missingRequiredCheckNames: ['test', 'build'],
|
|
40
|
+
},
|
|
41
|
+
fetchError: false,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const meta: Meta<typeof ConsoleOfflinePendingActionsPanel> = {
|
|
45
|
+
title: 'Console/ConsoleOfflinePendingActionsPanel',
|
|
46
|
+
component: ConsoleOfflinePendingActionsPanel,
|
|
47
|
+
args: {
|
|
48
|
+
isOnline: true,
|
|
49
|
+
isConfirming: false,
|
|
50
|
+
onConfirm: () => {},
|
|
51
|
+
onDiscard: () => {},
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export default meta;
|
|
56
|
+
|
|
57
|
+
type Story = StoryObj<typeof ConsoleOfflinePendingActionsPanel>;
|
|
58
|
+
|
|
59
|
+
export const SingleActionOnline: Story = {
|
|
60
|
+
args: {
|
|
61
|
+
actions: [approveAction],
|
|
62
|
+
isOnline: true,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const MultipleActionsOnline: Story = {
|
|
67
|
+
args: {
|
|
68
|
+
actions: [approveAction, rejectAction],
|
|
69
|
+
isOnline: true,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const LoadingCurrentState: Story = {
|
|
74
|
+
args: {
|
|
75
|
+
actions: [
|
|
76
|
+
{
|
|
77
|
+
...approveAction,
|
|
78
|
+
currentTitle: null,
|
|
79
|
+
currentState: null,
|
|
80
|
+
currentPrStatus: null,
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
isOnline: true,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const FetchError: Story = {
|
|
88
|
+
args: {
|
|
89
|
+
actions: [
|
|
90
|
+
{
|
|
91
|
+
...approveAction,
|
|
92
|
+
currentTitle: null,
|
|
93
|
+
currentState: null,
|
|
94
|
+
currentPrStatus: null,
|
|
95
|
+
fetchError: true,
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
isOnline: true,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const OfflineWithQueuedActions: Story = {
|
|
103
|
+
args: {
|
|
104
|
+
actions: [approveAction, rejectAction],
|
|
105
|
+
isOnline: false,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const ConfirmingInProgress: Story = {
|
|
110
|
+
args: {
|
|
111
|
+
actions: [approveAction],
|
|
112
|
+
isOnline: true,
|
|
113
|
+
isConfirming: true,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { fireEvent, render } from '@testing-library/react';
|
|
2
|
+
import {
|
|
3
|
+
type ConsoleOfflinePendingActionItem,
|
|
4
|
+
ConsoleOfflinePendingActionsPanel,
|
|
5
|
+
} from './ConsoleOfflinePendingActionsPanel';
|
|
6
|
+
|
|
7
|
+
const approveAction: ConsoleOfflinePendingActionItem = {
|
|
8
|
+
id: 'offline-1',
|
|
9
|
+
message: 'Approved — PR #851',
|
|
10
|
+
color: 'green',
|
|
11
|
+
itemNumber: 851,
|
|
12
|
+
isPr: true,
|
|
13
|
+
currentTitle: 'Add serveConsole subcommand under entry-points',
|
|
14
|
+
currentState: 'open',
|
|
15
|
+
currentPrStatus: null,
|
|
16
|
+
fetchError: false,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const baseProps = {
|
|
20
|
+
actions: [approveAction],
|
|
21
|
+
isOnline: true,
|
|
22
|
+
isConfirming: false,
|
|
23
|
+
onConfirm: () => {},
|
|
24
|
+
onDiscard: () => {},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
describe('ConsoleOfflinePendingActionsPanel', () => {
|
|
28
|
+
it('renders nothing when the action list is empty', () => {
|
|
29
|
+
const { container } = render(
|
|
30
|
+
<ConsoleOfflinePendingActionsPanel {...baseProps} actions={[]} />,
|
|
31
|
+
);
|
|
32
|
+
expect(container.firstChild).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('shows the action count and a confirm-before-sending message when online', () => {
|
|
36
|
+
const { getByText } = render(
|
|
37
|
+
<ConsoleOfflinePendingActionsPanel {...baseProps} />,
|
|
38
|
+
);
|
|
39
|
+
expect(getByText(/1 action held/)).toBeInTheDocument();
|
|
40
|
+
expect(getByText(/confirm each before sending/)).toBeInTheDocument();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('shows an awaiting-connectivity message when offline', () => {
|
|
44
|
+
const { getByText } = render(
|
|
45
|
+
<ConsoleOfflinePendingActionsPanel {...baseProps} isOnline={false} />,
|
|
46
|
+
);
|
|
47
|
+
expect(getByText(/awaiting connectivity/)).toBeInTheDocument();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('shows per-action rows with item target when offline', () => {
|
|
51
|
+
const { getByText, queryByText } = render(
|
|
52
|
+
<ConsoleOfflinePendingActionsPanel {...baseProps} isOnline={false} />,
|
|
53
|
+
);
|
|
54
|
+
expect(getByText(/PR #851 — Add serveConsole/)).toBeInTheDocument();
|
|
55
|
+
expect(getByText('Approved — PR #851')).toBeInTheDocument();
|
|
56
|
+
expect(queryByText('Send')).not.toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('shows Discard button even when offline', () => {
|
|
60
|
+
const { getByText } = render(
|
|
61
|
+
<ConsoleOfflinePendingActionsPanel {...baseProps} isOnline={false} />,
|
|
62
|
+
);
|
|
63
|
+
expect(getByText('Discard')).toBeInTheDocument();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('shows the action message and item label when online', () => {
|
|
67
|
+
const { getByText, container } = render(
|
|
68
|
+
<ConsoleOfflinePendingActionsPanel {...baseProps} />,
|
|
69
|
+
);
|
|
70
|
+
expect(getByText('Approved — PR #851')).toBeInTheDocument();
|
|
71
|
+
expect(
|
|
72
|
+
container.querySelector('.console-offline-panel-item-target')
|
|
73
|
+
?.textContent,
|
|
74
|
+
).toContain('PR #851');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('shows the current item title and state when loaded', () => {
|
|
78
|
+
const { getByText } = render(
|
|
79
|
+
<ConsoleOfflinePendingActionsPanel {...baseProps} />,
|
|
80
|
+
);
|
|
81
|
+
expect(
|
|
82
|
+
getByText(/Add serveConsole subcommand under entry-points/),
|
|
83
|
+
).toBeInTheDocument();
|
|
84
|
+
expect(getByText(/open/)).toBeInTheDocument();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('shows a loading indicator when the current state is not yet fetched', () => {
|
|
88
|
+
const { getByText } = render(
|
|
89
|
+
<ConsoleOfflinePendingActionsPanel
|
|
90
|
+
{...baseProps}
|
|
91
|
+
actions={[{ ...approveAction, currentTitle: null, currentState: null }]}
|
|
92
|
+
/>,
|
|
93
|
+
);
|
|
94
|
+
expect(getByText(/loading current state/)).toBeInTheDocument();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('disables the Send button while the current state is loading', () => {
|
|
98
|
+
const { getByText } = render(
|
|
99
|
+
<ConsoleOfflinePendingActionsPanel
|
|
100
|
+
{...baseProps}
|
|
101
|
+
actions={[{ ...approveAction, currentTitle: null, currentState: null }]}
|
|
102
|
+
/>,
|
|
103
|
+
);
|
|
104
|
+
expect(getByText('Send')).toBeDisabled();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('enables the Send button once the current state is loaded', () => {
|
|
108
|
+
const { getByText } = render(
|
|
109
|
+
<ConsoleOfflinePendingActionsPanel {...baseProps} />,
|
|
110
|
+
);
|
|
111
|
+
expect(getByText('Send')).not.toBeDisabled();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('enables the Send button when the state fetch failed so the operator can still proceed', () => {
|
|
115
|
+
const { getByText } = render(
|
|
116
|
+
<ConsoleOfflinePendingActionsPanel
|
|
117
|
+
{...baseProps}
|
|
118
|
+
actions={[
|
|
119
|
+
{
|
|
120
|
+
...approveAction,
|
|
121
|
+
currentTitle: null,
|
|
122
|
+
currentState: null,
|
|
123
|
+
fetchError: true,
|
|
124
|
+
},
|
|
125
|
+
]}
|
|
126
|
+
/>,
|
|
127
|
+
);
|
|
128
|
+
expect(getByText('Send')).not.toBeDisabled();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('shows a fetch error message when the state could not be loaded', () => {
|
|
132
|
+
const { getByText } = render(
|
|
133
|
+
<ConsoleOfflinePendingActionsPanel
|
|
134
|
+
{...baseProps}
|
|
135
|
+
actions={[
|
|
136
|
+
{
|
|
137
|
+
...approveAction,
|
|
138
|
+
currentTitle: null,
|
|
139
|
+
currentState: null,
|
|
140
|
+
fetchError: true,
|
|
141
|
+
},
|
|
142
|
+
]}
|
|
143
|
+
/>,
|
|
144
|
+
);
|
|
145
|
+
expect(getByText(/Failed to load current state/)).toBeInTheDocument();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('disables the Send button while a confirmation is in progress', () => {
|
|
149
|
+
const { getByText } = render(
|
|
150
|
+
<ConsoleOfflinePendingActionsPanel {...baseProps} isConfirming={true} />,
|
|
151
|
+
);
|
|
152
|
+
expect(getByText('Send')).toBeDisabled();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('calls onConfirm with the action id when Send is clicked', () => {
|
|
156
|
+
const onConfirm = jest.fn();
|
|
157
|
+
const { getByText } = render(
|
|
158
|
+
<ConsoleOfflinePendingActionsPanel
|
|
159
|
+
{...baseProps}
|
|
160
|
+
onConfirm={onConfirm}
|
|
161
|
+
/>,
|
|
162
|
+
);
|
|
163
|
+
fireEvent.click(getByText('Send'));
|
|
164
|
+
expect(onConfirm).toHaveBeenCalledWith('offline-1');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('calls onDiscard with the action id when Discard is clicked', () => {
|
|
168
|
+
const onDiscard = jest.fn();
|
|
169
|
+
const { getByText } = render(
|
|
170
|
+
<ConsoleOfflinePendingActionsPanel
|
|
171
|
+
{...baseProps}
|
|
172
|
+
onDiscard={onDiscard}
|
|
173
|
+
/>,
|
|
174
|
+
);
|
|
175
|
+
fireEvent.click(getByText('Discard'));
|
|
176
|
+
expect(onDiscard).toHaveBeenCalledWith('offline-1');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('uses plural phrasing for the count when more than one action is queued', () => {
|
|
180
|
+
const { getByText } = render(
|
|
181
|
+
<ConsoleOfflinePendingActionsPanel
|
|
182
|
+
{...baseProps}
|
|
183
|
+
actions={[
|
|
184
|
+
approveAction,
|
|
185
|
+
{ ...approveAction, id: 'offline-2', message: 'Rejected — PR #853' },
|
|
186
|
+
]}
|
|
187
|
+
/>,
|
|
188
|
+
);
|
|
189
|
+
expect(getByText(/2 actions held/)).toBeInTheDocument();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('renders a Send and Discard button for each queued action when online', () => {
|
|
193
|
+
const { getAllByText } = render(
|
|
194
|
+
<ConsoleOfflinePendingActionsPanel
|
|
195
|
+
{...baseProps}
|
|
196
|
+
actions={[
|
|
197
|
+
approveAction,
|
|
198
|
+
{ ...approveAction, id: 'offline-2', message: 'Rejected — PR #853' },
|
|
199
|
+
]}
|
|
200
|
+
/>,
|
|
201
|
+
);
|
|
202
|
+
expect(getAllByText('Send')).toHaveLength(2);
|
|
203
|
+
expect(getAllByText('Discard')).toHaveLength(2);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('applies the color modifier class to the action row', () => {
|
|
207
|
+
const { container } = render(
|
|
208
|
+
<ConsoleOfflinePendingActionsPanel {...baseProps} />,
|
|
209
|
+
);
|
|
210
|
+
expect(
|
|
211
|
+
container.querySelector('.console-offline-panel-item-green'),
|
|
212
|
+
).toBeInTheDocument();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('shows PR status information when the action targets a PR and status is loaded', () => {
|
|
216
|
+
const { getByText } = render(
|
|
217
|
+
<ConsoleOfflinePendingActionsPanel
|
|
218
|
+
{...baseProps}
|
|
219
|
+
actions={[
|
|
220
|
+
{
|
|
221
|
+
...approveAction,
|
|
222
|
+
currentPrStatus: {
|
|
223
|
+
found: true,
|
|
224
|
+
isConflicted: false,
|
|
225
|
+
mergeableStatus: 'MERGEABLE',
|
|
226
|
+
isPassedAllCiJob: true,
|
|
227
|
+
isCiStateSuccess: true,
|
|
228
|
+
isBranchOutOfDate: false,
|
|
229
|
+
missingRequiredCheckNames: [],
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
]}
|
|
233
|
+
/>,
|
|
234
|
+
);
|
|
235
|
+
expect(getByText(/CI: pass/)).toBeInTheDocument();
|
|
236
|
+
expect(getByText(/Merge: mergeable/)).toBeInTheDocument();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('does not show PR status when offline even if currentPrStatus is set', () => {
|
|
240
|
+
const { queryByText } = render(
|
|
241
|
+
<ConsoleOfflinePendingActionsPanel
|
|
242
|
+
{...baseProps}
|
|
243
|
+
isOnline={false}
|
|
244
|
+
actions={[
|
|
245
|
+
{
|
|
246
|
+
...approveAction,
|
|
247
|
+
currentPrStatus: {
|
|
248
|
+
found: true,
|
|
249
|
+
isConflicted: false,
|
|
250
|
+
mergeableStatus: 'MERGEABLE',
|
|
251
|
+
isPassedAllCiJob: true,
|
|
252
|
+
isCiStateSuccess: true,
|
|
253
|
+
isBranchOutOfDate: false,
|
|
254
|
+
missingRequiredCheckNames: [],
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
]}
|
|
258
|
+
/>,
|
|
259
|
+
);
|
|
260
|
+
expect(queryByText(/CI: pass/)).not.toBeInTheDocument();
|
|
261
|
+
});
|
|
262
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { ConsoleToastColor } from '../../logic/actionToast';
|
|
2
|
+
import type { ConsolePullRequestStatus } from '../../logic/types';
|
|
3
|
+
|
|
4
|
+
export type ConsoleOfflinePendingActionItem = {
|
|
5
|
+
id: string;
|
|
6
|
+
message: string;
|
|
7
|
+
color: ConsoleToastColor;
|
|
8
|
+
itemNumber: number;
|
|
9
|
+
isPr: boolean;
|
|
10
|
+
currentTitle: string | null;
|
|
11
|
+
currentState: 'open' | 'closed' | null;
|
|
12
|
+
currentPrStatus: ConsolePullRequestStatus | null;
|
|
13
|
+
fetchError: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type ConsoleOfflinePendingActionsPanelProps = {
|
|
17
|
+
actions: ConsoleOfflinePendingActionItem[];
|
|
18
|
+
isOnline: boolean;
|
|
19
|
+
isConfirming: boolean;
|
|
20
|
+
onConfirm: (id: string) => void;
|
|
21
|
+
onDiscard: (id: string) => void;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const itemLabel = (item: ConsoleOfflinePendingActionItem): string =>
|
|
25
|
+
`${item.isPr ? 'PR' : 'Issue'} #${item.itemNumber}`;
|
|
26
|
+
|
|
27
|
+
const stateLabel = (state: 'open' | 'closed' | null): string => {
|
|
28
|
+
if (state === null) return 'loading…';
|
|
29
|
+
return state;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const prStatusLabel = (prStatus: ConsolePullRequestStatus): string => {
|
|
33
|
+
const parts: string[] = [];
|
|
34
|
+
parts.push(`CI: ${prStatus.isCiStateSuccess ? 'pass' : 'fail'}`);
|
|
35
|
+
parts.push(`Merge: ${prStatus.mergeableStatus.toLowerCase()}`);
|
|
36
|
+
if (prStatus.isBranchOutOfDate) {
|
|
37
|
+
parts.push('branch out of date');
|
|
38
|
+
}
|
|
39
|
+
return parts.join(' · ');
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const targetText = (action: ConsoleOfflinePendingActionItem): string => {
|
|
43
|
+
if (action.fetchError) {
|
|
44
|
+
return `${itemLabel(action)} — failed to load current state`;
|
|
45
|
+
}
|
|
46
|
+
if (action.currentTitle === null) {
|
|
47
|
+
return `${itemLabel(action)} — loading current state…`;
|
|
48
|
+
}
|
|
49
|
+
return `${itemLabel(action)} — ${action.currentTitle} (${stateLabel(action.currentState)})`;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const ConsoleOfflinePendingActionsPanel = ({
|
|
53
|
+
actions,
|
|
54
|
+
isOnline,
|
|
55
|
+
isConfirming,
|
|
56
|
+
onConfirm,
|
|
57
|
+
onDiscard,
|
|
58
|
+
}: ConsoleOfflinePendingActionsPanelProps) => {
|
|
59
|
+
if (actions.length === 0) return null;
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<section
|
|
63
|
+
className="console-offline-panel"
|
|
64
|
+
aria-label="Offline actions queue"
|
|
65
|
+
>
|
|
66
|
+
<div className="console-offline-panel-header">
|
|
67
|
+
{actions.length} action{actions.length !== 1 ? 's' : ''} held
|
|
68
|
+
{isOnline
|
|
69
|
+
? ' — confirm each before sending'
|
|
70
|
+
: ' — awaiting connectivity'}
|
|
71
|
+
</div>
|
|
72
|
+
{actions.map((action) => (
|
|
73
|
+
<div
|
|
74
|
+
key={action.id}
|
|
75
|
+
className={`console-offline-panel-item console-offline-panel-item-${action.color}`}
|
|
76
|
+
>
|
|
77
|
+
<div className="console-offline-panel-item-message">
|
|
78
|
+
{action.message}
|
|
79
|
+
</div>
|
|
80
|
+
<div className="console-offline-panel-item-target">
|
|
81
|
+
{targetText(action)}
|
|
82
|
+
</div>
|
|
83
|
+
{action.fetchError && (
|
|
84
|
+
<div className="console-offline-panel-item-fetch-error">
|
|
85
|
+
Failed to load current state — sending will act on the last known
|
|
86
|
+
state
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
{isOnline &&
|
|
90
|
+
action.isPr &&
|
|
91
|
+
action.currentPrStatus !== null &&
|
|
92
|
+
!action.fetchError && (
|
|
93
|
+
<div className="console-offline-panel-item-pr-status">
|
|
94
|
+
{prStatusLabel(action.currentPrStatus)}
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
97
|
+
<div className="console-offline-panel-item-controls">
|
|
98
|
+
{isOnline && (
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
className="console-offline-panel-confirm"
|
|
102
|
+
disabled={
|
|
103
|
+
isConfirming ||
|
|
104
|
+
(action.currentTitle === null && !action.fetchError)
|
|
105
|
+
}
|
|
106
|
+
onClick={() => onConfirm(action.id)}
|
|
107
|
+
>
|
|
108
|
+
Send
|
|
109
|
+
</button>
|
|
110
|
+
)}
|
|
111
|
+
<button
|
|
112
|
+
type="button"
|
|
113
|
+
className="console-offline-panel-discard"
|
|
114
|
+
onClick={() => onDiscard(action.id)}
|
|
115
|
+
>
|
|
116
|
+
Discard
|
|
117
|
+
</button>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
))}
|
|
121
|
+
</section>
|
|
122
|
+
);
|
|
123
|
+
};
|