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.
@@ -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-D6QOQvoN.js"></script>
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-issue-tower-defence-management",
3
- "version": "2.12.1",
3
+ "version": "2.13.0",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "scripts": {
@@ -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
+ };