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.
@@ -3,16 +3,28 @@ import { ConsoleCommentComposer } from '../components/detail/ConsoleCommentCompo
3
3
  import type { ConsoleAddInlineComment } from '../components/detail/ConsoleFileDiff';
4
4
  import { ConsoleItemDetail } from '../components/detail/ConsoleItemDetail';
5
5
  import { ConsoleOperationMenu } from '../components/operations/ConsoleOperationMenu';
6
+ import type { ConsoleOfflinePayload } from '../hooks/useConsoleActionQueue';
6
7
  import type { ConsoleCaches } from '../hooks/useConsoleCaches';
7
8
  import { useConsoleItemDetailData } from '../hooks/useConsoleItemDetailData';
8
- import type { ConsoleOperationsApi } from '../hooks/useConsoleOperations';
9
+ import {
10
+ buildIntmuxRequest,
11
+ buildTriageRequest,
12
+ type ConsoleOperationsApi,
13
+ INTMUX_OPERATION_PATH,
14
+ REVIEW_OPERATION_PATH,
15
+ reviewRequest,
16
+ TRIAGE_OPERATION_PATH,
17
+ } from '../hooks/useConsoleOperations';
9
18
  import { buildImageProxyUrl } from '../lib/imageProxy';
10
19
  import type { ConsoleActionKind } from '../logic/actionToast';
11
20
  import { resolveStoryColorEnum } from '../logic/grouping';
12
21
  import {
13
22
  AWAITING_WORKSPACE_NAME,
23
+ type ConsoleCloseAction,
24
+ type ConsoleNextActionDateAction,
14
25
  type ConsoleOperationHandlers,
15
26
  type ConsolePendingReviewComment,
27
+ type ConsoleReviewAction,
16
28
  } from '../logic/operations';
17
29
  import { mergePostedComments } from '../logic/postedComments';
18
30
  import type {
@@ -30,13 +42,70 @@ export type ConsoleQueueActionInput = {
30
42
  kind: ConsoleActionKind;
31
43
  item: ConsoleListItem;
32
44
  commit: () => Promise<void>;
45
+ offline?: ConsoleOfflinePayload;
33
46
  };
34
47
 
48
+ const itemOfflineBase = (item: ConsoleListItem) => ({
49
+ itemUrl: item.url,
50
+ projectItemId: item.projectItemId,
51
+ itemNumber: item.number,
52
+ repo: item.repo,
53
+ isPr: item.isPr,
54
+ });
55
+
56
+ const buildReviewOfflinePayload = (
57
+ pjcode: string,
58
+ item: ConsoleListItem,
59
+ prUrl: string,
60
+ action: ConsoleReviewAction,
61
+ pendingComments: ConsolePendingReviewComment[],
62
+ ): ConsoleOfflinePayload => ({
63
+ ...itemOfflineBase(item),
64
+ apiPath: REVIEW_OPERATION_PATH,
65
+ requestBody: reviewRequest(
66
+ pjcode,
67
+ item,
68
+ prUrl,
69
+ action,
70
+ pendingComments,
71
+ ) as unknown as Record<string, unknown>,
72
+ });
73
+
74
+ const buildTriageOfflinePayload = (
75
+ pjcode: string,
76
+ item: ConsoleListItem,
77
+ action:
78
+ | ConsoleNextActionDateAction
79
+ | ConsoleCloseAction
80
+ | 'set_story'
81
+ | 'set_status',
82
+ extra?: { statusName?: string; storyOptionId?: string },
83
+ ): ConsoleOfflinePayload => ({
84
+ ...itemOfflineBase(item),
85
+ apiPath: TRIAGE_OPERATION_PATH,
86
+ requestBody: buildTriageRequest(
87
+ pjcode,
88
+ item,
89
+ action,
90
+ extra,
91
+ ) as unknown as Record<string, unknown>,
92
+ });
93
+
94
+ const buildIntmuxOfflinePayload = (
95
+ pjcode: string,
96
+ item: ConsoleListItem,
97
+ ): ConsoleOfflinePayload => ({
98
+ ...itemOfflineBase(item),
99
+ apiPath: INTMUX_OPERATION_PATH,
100
+ requestBody: buildIntmuxRequest(pjcode, item),
101
+ });
102
+
35
103
  export type ConsoleItemDetailContainerProps = {
36
104
  tab: ConsoleTabName;
37
105
  item: ConsoleListItem;
38
106
  caches: ConsoleCaches;
39
107
  operations: ConsoleOperationsApi;
108
+ pjcode?: string | null;
40
109
  statusOptions: ConsoleFieldOption[];
41
110
  storyColors: ConsoleStoryColorSource;
42
111
  storyName: string | null;
@@ -52,6 +121,7 @@ export const ConsoleItemDetailContainer = ({
52
121
  item,
53
122
  caches,
54
123
  operations,
124
+ pjcode,
55
125
  statusOptions,
56
126
  storyColors,
57
127
  storyName,
@@ -116,6 +186,16 @@ export const ConsoleItemDetailContainer = ({
116
186
  item,
117
187
  commit: () =>
118
188
  operations.reviewPullRequest(item, prUrl, action, reviewComments),
189
+ offline:
190
+ pjcode != null
191
+ ? buildReviewOfflinePayload(
192
+ pjcode,
193
+ item,
194
+ prUrl,
195
+ action,
196
+ reviewComments,
197
+ )
198
+ : undefined,
119
199
  });
120
200
  },
121
201
  onSetNextActionDate: (action) => {
@@ -123,6 +203,10 @@ export const ConsoleItemDetailContainer = ({
123
203
  kind: { type: 'next_action_date', action },
124
204
  item,
125
205
  commit: () => operations.setNextActionDate(item, action),
206
+ offline:
207
+ pjcode != null
208
+ ? buildTriageOfflinePayload(pjcode, item, action)
209
+ : undefined,
126
210
  });
127
211
  },
128
212
  onSetStory: (option: ConsoleFieldOption) => {
@@ -130,6 +214,12 @@ export const ConsoleItemDetailContainer = ({
130
214
  kind: { type: 'set_story', optionName: option.name },
131
215
  item,
132
216
  commit: () => operations.setStory(item, option),
217
+ offline:
218
+ pjcode != null
219
+ ? buildTriageOfflinePayload(pjcode, item, 'set_story', {
220
+ storyOptionId: option.id,
221
+ })
222
+ : undefined,
133
223
  });
134
224
  },
135
225
  onSetStatus: (option: ConsoleFieldOption) => {
@@ -137,6 +227,12 @@ export const ConsoleItemDetailContainer = ({
137
227
  kind: { type: 'set_status', optionName: option.name },
138
228
  item,
139
229
  commit: () => operations.setStatus(item, option),
230
+ offline:
231
+ pjcode != null
232
+ ? buildTriageOfflinePayload(pjcode, item, 'set_status', {
233
+ statusName: option.name,
234
+ })
235
+ : undefined,
140
236
  });
141
237
  },
142
238
  onSetInTmuxByHuman: (option: ConsoleFieldOption) => {
@@ -144,6 +240,8 @@ export const ConsoleItemDetailContainer = ({
144
240
  kind: { type: 'set_in_tmux_by_human', optionName: option.name },
145
241
  item,
146
242
  commit: () => operations.setInTmuxByHuman(item, option),
243
+ offline:
244
+ pjcode != null ? buildIntmuxOfflinePayload(pjcode, item) : undefined,
147
245
  });
148
246
  },
149
247
  onClose: (action) => {
@@ -151,6 +249,10 @@ export const ConsoleItemDetailContainer = ({
151
249
  kind: { type: 'close', action },
152
250
  item,
153
251
  commit: () => operations.closeIssue(item, action),
252
+ offline:
253
+ pjcode != null
254
+ ? buildTriageOfflinePayload(pjcode, item, action)
255
+ : undefined,
154
256
  });
155
257
  },
156
258
  onOkAndAwaitingWorkspace: (option: ConsoleFieldOption) => {
@@ -4,6 +4,10 @@ import { ConsoleTimerSettingsModalDialog } from '../components/layout/ConsoleTim
4
4
  import { ConsoleItemList } from '../components/list/ConsoleItemList';
5
5
  import { ConsoleQueuedList } from '../components/list/ConsoleQueuedList';
6
6
  import { ConsoleStoryList } from '../components/list/ConsoleStoryList';
7
+ import {
8
+ type ConsoleOfflinePendingActionItem,
9
+ ConsoleOfflinePendingActionsPanel,
10
+ } from '../components/operations/ConsoleOfflinePendingActionsPanel';
7
11
  import {
8
12
  ConsoleErrorToast,
9
13
  ConsoleUndoToast,
@@ -52,8 +56,10 @@ import { findNextNonEmptyTabToRight } from '../logic/tabAdvance';
52
56
  import { findNextPjcodeWithMinutes } from '../logic/timerSettings';
53
57
  import type {
54
58
  ConsoleColor,
59
+ ConsoleIssueState,
55
60
  ConsoleListItem,
56
61
  ConsoleOverlayStatus,
62
+ ConsolePullRequestStatus,
57
63
  ConsoleStoryEntry,
58
64
  ConsoleTabName,
59
65
  } from '../logic/types';
@@ -151,6 +157,119 @@ export const ConsolePage = () => {
151
157
  const actionQueue = useConsoleActionQueue();
152
158
  const now = Date.now();
153
159
 
160
+ const [isOnline, setIsOnline] = useState(() => navigator.onLine);
161
+ useEffect(() => {
162
+ const handleOnline = (): void => setIsOnline(true);
163
+ const handleOffline = (): void => setIsOnline(false);
164
+ window.addEventListener('online', handleOnline);
165
+ window.addEventListener('offline', handleOffline);
166
+ return () => {
167
+ window.removeEventListener('online', handleOnline);
168
+ window.removeEventListener('offline', handleOffline);
169
+ };
170
+ }, []);
171
+
172
+ const [offlineItemStates, setOfflineItemStates] = useState<
173
+ Map<string, ConsoleIssueState | null>
174
+ >(new Map());
175
+
176
+ const [offlinePrStatuses, setOfflinePrStatuses] = useState<
177
+ Map<string, ConsolePullRequestStatus | null>
178
+ >(new Map());
179
+
180
+ const [offlineFetchErrors, setOfflineFetchErrors] = useState<Set<string>>(
181
+ new Set(),
182
+ );
183
+
184
+ useEffect(() => {
185
+ if (!isOnline || actionQueue.offlineActions.length === 0) return;
186
+ for (const action of actionQueue.offlineActions) {
187
+ if (offlineItemStates.has(action.id) || offlineFetchErrors.has(action.id))
188
+ continue;
189
+ setOfflineItemStates((prev) => new Map(prev).set(action.id, null));
190
+ caches.client
191
+ .fetchIssueState(action.itemUrl)
192
+ .then((state) => {
193
+ setOfflineItemStates((prev) => new Map(prev).set(action.id, state));
194
+ if (action.isPr) {
195
+ setOfflinePrStatuses((prev) => new Map(prev).set(action.id, null));
196
+ caches.client
197
+ .fetchPullRequestStatus(action.itemUrl)
198
+ .then((prStatus) => {
199
+ setOfflinePrStatuses((prev) =>
200
+ new Map(prev).set(action.id, prStatus),
201
+ );
202
+ })
203
+ .catch((fetchError: unknown) => {
204
+ console.warn(
205
+ 'Failed to fetch PR status for offline action:',
206
+ fetchError,
207
+ );
208
+ });
209
+ }
210
+ })
211
+ .catch((fetchError: unknown) => {
212
+ console.warn(
213
+ 'Failed to fetch current state for offline action:',
214
+ fetchError,
215
+ );
216
+ setOfflineFetchErrors((prev) => new Set(prev).add(action.id));
217
+ });
218
+ }
219
+ }, [
220
+ isOnline,
221
+ actionQueue.offlineActions,
222
+ caches.client,
223
+ offlineItemStates,
224
+ offlineFetchErrors,
225
+ ]);
226
+
227
+ const [isConfirmingOffline, setIsConfirmingOffline] = useState(false);
228
+
229
+ const handleConfirmOffline = useCallback(
230
+ async (id: string): Promise<void> => {
231
+ setIsConfirmingOffline(true);
232
+ try {
233
+ await actionQueue.confirmOfflineAction(id);
234
+ } finally {
235
+ setIsConfirmingOffline(false);
236
+ }
237
+ },
238
+ [actionQueue],
239
+ );
240
+
241
+ const offlinePanelActions = useMemo(
242
+ (): ConsoleOfflinePendingActionItem[] =>
243
+ actionQueue.offlineActions.map((a) => {
244
+ const state = offlineItemStates.get(a.id) ?? null;
245
+ const prStatus = offlinePrStatuses.get(a.id) ?? null;
246
+ const fetchError = offlineFetchErrors.has(a.id);
247
+ return {
248
+ id: a.id,
249
+ message: a.message,
250
+ color: a.color,
251
+ itemNumber: a.itemNumber,
252
+ isPr: a.isPr,
253
+ currentTitle: fetchError ? null : (state?.title ?? null),
254
+ currentState: fetchError
255
+ ? null
256
+ : state === null
257
+ ? null
258
+ : state.merged
259
+ ? 'closed'
260
+ : (state.state as 'open' | 'closed'),
261
+ currentPrStatus: prStatus,
262
+ fetchError,
263
+ };
264
+ }),
265
+ [
266
+ actionQueue.offlineActions,
267
+ offlineItemStates,
268
+ offlinePrStatuses,
269
+ offlineFetchErrors,
270
+ ],
271
+ );
272
+
154
273
  const activeSnapshot = snapshots[activeTab];
155
274
  const pendingItems = useMemo(() => {
156
275
  if (activeSnapshot === null) {
@@ -294,6 +413,7 @@ export const ConsolePage = () => {
294
413
  message: formatActionToast(input.kind, input.item, activeTab),
295
414
  color: actionToastColor(input.kind),
296
415
  commit: input.commit,
416
+ offline: input.offline,
297
417
  advance: () => {
298
418
  if (actionAdvances(input.kind, activeTab)) {
299
419
  if (
@@ -506,6 +626,13 @@ export const ConsolePage = () => {
506
626
  onDismiss={actionQueue.dismissError}
507
627
  />
508
628
  )}
629
+ <ConsoleOfflinePendingActionsPanel
630
+ actions={offlinePanelActions}
631
+ isOnline={isOnline}
632
+ isConfirming={isConfirmingOffline}
633
+ onConfirm={handleConfirmOffline}
634
+ onDiscard={actionQueue.discardOfflineAction}
635
+ />
509
636
  <ConsoleTabList
510
637
  activeTab={activeTab}
511
638
  counts={counts}
@@ -585,6 +712,7 @@ export const ConsolePage = () => {
585
712
  item={selectedItem}
586
713
  caches={caches}
587
714
  operations={operations}
715
+ pjcode={pjcode}
588
716
  statusOptions={statusOptions}
589
717
  storyColors={storyColors}
590
718
  storyName={storyNameForSelected}