github-issue-tower-defence-management 2.12.1 → 2.14.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/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/bin/domain/usecases/ActionAnnouncementUseCase.js +4 -2
- package/bin/domain/usecases/ActionAnnouncementUseCase.js.map +1 -1
- package/bin/domain/usecases/AnalyzeStoriesUseCase.js +1 -1
- package/bin/domain/usecases/AnalyzeStoriesUseCase.js.map +1 -1
- package/bin/domain/usecases/CliErrorReportUseCase.js +1 -1
- package/bin/domain/usecases/CliErrorReportUseCase.js.map +1 -1
- package/bin/domain/usecases/CreateEstimationIssueUseCase.js +2 -1
- package/bin/domain/usecases/CreateEstimationIssueUseCase.js.map +1 -1
- package/bin/domain/usecases/DailySecurityScanUseCase.js +3 -3
- package/bin/domain/usecases/DailySecurityScanUseCase.js.map +1 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +3 -3
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +4 -0
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/SubscriptionDisabledIssueUseCase.js +1 -1
- package/bin/domain/usecases/SubscriptionDisabledIssueUseCase.js.map +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
- package/src/domain/usecases/ActionAnnouncementUseCase.test.ts +91 -0
- package/src/domain/usecases/ActionAnnouncementUseCase.ts +4 -2
- package/src/domain/usecases/AnalyzeStoriesUseCase.test.ts +2 -0
- package/src/domain/usecases/AnalyzeStoriesUseCase.ts +1 -1
- package/src/domain/usecases/CliErrorReportUseCase.test.ts +11 -0
- package/src/domain/usecases/CliErrorReportUseCase.ts +1 -1
- package/src/domain/usecases/CreateEstimationIssueUseCase.test.ts +2 -0
- package/src/domain/usecases/CreateEstimationIssueUseCase.ts +2 -1
- package/src/domain/usecases/DailySecurityScanUseCase.test.ts +6 -0
- package/src/domain/usecases/DailySecurityScanUseCase.ts +3 -3
- package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +25 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +3 -3
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +20 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +4 -0
- package/src/domain/usecases/SubscriptionDisabledIssueUseCase.test.ts +3 -0
- package/src/domain/usecases/SubscriptionDisabledIssueUseCase.ts +1 -1
- package/types/domain/usecases/ActionAnnouncementUseCase.d.ts.map +1 -1
- package/types/domain/usecases/CreateEstimationIssueUseCase.d.ts.map +1 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleActionQueue.ts
CHANGED
|
@@ -1,14 +1,33 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { postConsoleOperation } from '../lib/consoleApi';
|
|
2
3
|
import {
|
|
3
4
|
ACTION_TOAST_DELAY_MS,
|
|
4
5
|
type ConsoleToastColor,
|
|
5
6
|
} from '../logic/actionToast';
|
|
6
7
|
|
|
8
|
+
export type ConsoleOfflinePayload = {
|
|
9
|
+
itemUrl: string;
|
|
10
|
+
projectItemId: string;
|
|
11
|
+
itemNumber: number;
|
|
12
|
+
repo: string;
|
|
13
|
+
isPr: boolean;
|
|
14
|
+
apiPath: string;
|
|
15
|
+
requestBody: Record<string, unknown>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type ConsoleOfflineQueuedAction = {
|
|
19
|
+
id: string;
|
|
20
|
+
message: string;
|
|
21
|
+
color: ConsoleToastColor;
|
|
22
|
+
enqueuedAt: number;
|
|
23
|
+
} & ConsoleOfflinePayload;
|
|
24
|
+
|
|
7
25
|
export type ConsoleQueuedAction = {
|
|
8
26
|
message: string;
|
|
9
27
|
color: ConsoleToastColor;
|
|
10
28
|
commit: () => Promise<void>;
|
|
11
29
|
advance: () => void;
|
|
30
|
+
offline?: ConsoleOfflinePayload;
|
|
12
31
|
};
|
|
13
32
|
|
|
14
33
|
export type ConsolePendingActionView = {
|
|
@@ -23,6 +42,57 @@ export type ConsoleActionError = {
|
|
|
23
42
|
reason: string;
|
|
24
43
|
};
|
|
25
44
|
|
|
45
|
+
const OFFLINE_QUEUE_STORAGE_KEY = 'console-offline-action-queue';
|
|
46
|
+
|
|
47
|
+
const isConsoleOfflineQueuedAction = (
|
|
48
|
+
item: unknown,
|
|
49
|
+
): item is ConsoleOfflineQueuedAction => {
|
|
50
|
+
if (typeof item !== 'object' || item === null) return false;
|
|
51
|
+
const a = item as Record<string, unknown>;
|
|
52
|
+
return (
|
|
53
|
+
typeof a.id === 'string' &&
|
|
54
|
+
typeof a.message === 'string' &&
|
|
55
|
+
typeof a.color === 'string' &&
|
|
56
|
+
typeof a.enqueuedAt === 'number' &&
|
|
57
|
+
typeof a.itemUrl === 'string' &&
|
|
58
|
+
typeof a.projectItemId === 'string' &&
|
|
59
|
+
typeof a.itemNumber === 'number' &&
|
|
60
|
+
typeof a.repo === 'string' &&
|
|
61
|
+
typeof a.isPr === 'boolean' &&
|
|
62
|
+
typeof a.apiPath === 'string' &&
|
|
63
|
+
typeof a.requestBody === 'object' &&
|
|
64
|
+
a.requestBody !== null
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const loadOfflineQueue = (): ConsoleOfflineQueuedAction[] => {
|
|
69
|
+
try {
|
|
70
|
+
const raw = localStorage.getItem(OFFLINE_QUEUE_STORAGE_KEY);
|
|
71
|
+
if (raw === null) return [];
|
|
72
|
+
const parsed: unknown = JSON.parse(raw);
|
|
73
|
+
if (!Array.isArray(parsed)) return [];
|
|
74
|
+
return parsed.filter(isConsoleOfflineQueuedAction);
|
|
75
|
+
} catch {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const saveOfflineQueue = (actions: ConsoleOfflineQueuedAction[]): void => {
|
|
81
|
+
localStorage.setItem(OFFLINE_QUEUE_STORAGE_KEY, JSON.stringify(actions));
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const generateId = (): string => {
|
|
85
|
+
if (
|
|
86
|
+
typeof crypto !== 'undefined' &&
|
|
87
|
+
typeof crypto.randomUUID === 'function'
|
|
88
|
+
) {
|
|
89
|
+
return crypto.randomUUID();
|
|
90
|
+
}
|
|
91
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}-${Math.random().toString(36).slice(2)}`;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const isNetworkError = (error: unknown): boolean => error instanceof TypeError;
|
|
95
|
+
|
|
26
96
|
const errorReason = (error: unknown): string => {
|
|
27
97
|
if (error instanceof Error && error.message.length > 0) {
|
|
28
98
|
return error.message;
|
|
@@ -41,15 +111,24 @@ const computeProgress = (elapsedMs: number): number =>
|
|
|
41
111
|
export type ConsoleActionQueue = {
|
|
42
112
|
pending: ConsolePendingActionView | null;
|
|
43
113
|
error: ConsoleActionError | null;
|
|
114
|
+
offlineActions: ConsoleOfflineQueuedAction[];
|
|
44
115
|
enqueue: (action: ConsoleQueuedAction) => void;
|
|
45
116
|
showError: (message: string, reason: string) => void;
|
|
46
117
|
undo: () => void;
|
|
47
118
|
dismissError: () => void;
|
|
119
|
+
confirmOfflineAction: (id: string) => Promise<void>;
|
|
120
|
+
discardOfflineAction: (id: string) => void;
|
|
48
121
|
};
|
|
49
122
|
|
|
50
123
|
export const useConsoleActionQueue = (): ConsoleActionQueue => {
|
|
51
124
|
const [pending, setPending] = useState<ConsolePendingActionView | null>(null);
|
|
52
125
|
const [error, setError] = useState<ConsoleActionError | null>(null);
|
|
126
|
+
const [offlineActions, setOfflineActions] =
|
|
127
|
+
useState<ConsoleOfflineQueuedAction[]>(loadOfflineQueue);
|
|
128
|
+
const offlineActionsRef =
|
|
129
|
+
useRef<ConsoleOfflineQueuedAction[]>(offlineActions);
|
|
130
|
+
offlineActionsRef.current = offlineActions;
|
|
131
|
+
|
|
53
132
|
const actionRef = useRef<ConsoleQueuedAction | null>(null);
|
|
54
133
|
const startRef = useRef<number>(0);
|
|
55
134
|
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
@@ -62,12 +141,35 @@ export const useConsoleActionQueue = (): ConsoleActionQueue => {
|
|
|
62
141
|
}
|
|
63
142
|
}, []);
|
|
64
143
|
|
|
65
|
-
const
|
|
66
|
-
action.
|
|
67
|
-
|
|
144
|
+
const addToOfflineQueue = useCallback((action: ConsoleQueuedAction): void => {
|
|
145
|
+
if (action.offline === undefined) return;
|
|
146
|
+
const offlineAction: ConsoleOfflineQueuedAction = {
|
|
147
|
+
id: generateId(),
|
|
148
|
+
message: action.message,
|
|
149
|
+
color: action.color,
|
|
150
|
+
enqueuedAt: Date.now(),
|
|
151
|
+
...action.offline,
|
|
152
|
+
};
|
|
153
|
+
setOfflineActions((prev) => {
|
|
154
|
+
const next = [...prev, offlineAction];
|
|
155
|
+
saveOfflineQueue(next);
|
|
156
|
+
return next;
|
|
68
157
|
});
|
|
69
158
|
}, []);
|
|
70
159
|
|
|
160
|
+
const runCommit = useCallback(
|
|
161
|
+
(action: ConsoleQueuedAction): void => {
|
|
162
|
+
action.commit().catch((cause: unknown) => {
|
|
163
|
+
if (isNetworkError(cause) && action.offline !== undefined) {
|
|
164
|
+
addToOfflineQueue(action);
|
|
165
|
+
} else {
|
|
166
|
+
setError({ message: action.message, reason: errorReason(cause) });
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
},
|
|
170
|
+
[addToOfflineQueue],
|
|
171
|
+
);
|
|
172
|
+
|
|
71
173
|
const commitPending = useCallback((): void => {
|
|
72
174
|
const action = actionRef.current;
|
|
73
175
|
clearTimer();
|
|
@@ -94,6 +196,36 @@ export const useConsoleActionQueue = (): ConsoleActionQueue => {
|
|
|
94
196
|
setError({ message, reason });
|
|
95
197
|
}, []);
|
|
96
198
|
|
|
199
|
+
const discardOfflineAction = useCallback((id: string): void => {
|
|
200
|
+
setOfflineActions((prev) => {
|
|
201
|
+
const next = prev.filter((a) => a.id !== id);
|
|
202
|
+
saveOfflineQueue(next);
|
|
203
|
+
return next;
|
|
204
|
+
});
|
|
205
|
+
}, []);
|
|
206
|
+
|
|
207
|
+
const confirmOfflineAction = useCallback(
|
|
208
|
+
async (id: string): Promise<void> => {
|
|
209
|
+
const action = offlineActionsRef.current.find((a) => a.id === id);
|
|
210
|
+
if (action === undefined) return;
|
|
211
|
+
try {
|
|
212
|
+
await postConsoleOperation(action.apiPath, action.requestBody);
|
|
213
|
+
discardOfflineAction(id);
|
|
214
|
+
} catch (cause: unknown) {
|
|
215
|
+
if (isNetworkError(cause)) {
|
|
216
|
+
setError({
|
|
217
|
+
message: action.message,
|
|
218
|
+
reason: 'Still offline — action is still held in the queue',
|
|
219
|
+
});
|
|
220
|
+
} else {
|
|
221
|
+
setError({ message: action.message, reason: errorReason(cause) });
|
|
222
|
+
discardOfflineAction(id);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
[discardOfflineAction],
|
|
227
|
+
);
|
|
228
|
+
|
|
97
229
|
const enqueue = useCallback(
|
|
98
230
|
(action: ConsoleQueuedAction): void => {
|
|
99
231
|
if (actionRef.current !== null && !committedRef.current) {
|
|
@@ -131,5 +263,15 @@ export const useConsoleActionQueue = (): ConsoleActionQueue => {
|
|
|
131
263
|
|
|
132
264
|
useEffect(() => clearTimer, [clearTimer]);
|
|
133
265
|
|
|
134
|
-
return {
|
|
266
|
+
return {
|
|
267
|
+
pending,
|
|
268
|
+
error,
|
|
269
|
+
offlineActions,
|
|
270
|
+
enqueue,
|
|
271
|
+
showError,
|
|
272
|
+
undo,
|
|
273
|
+
dismissError,
|
|
274
|
+
confirmOfflineAction,
|
|
275
|
+
discardOfflineAction,
|
|
276
|
+
};
|
|
135
277
|
};
|
package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.ts
CHANGED
|
@@ -76,7 +76,7 @@ export type ConsoleOperationsApi = {
|
|
|
76
76
|
) => Promise<void>;
|
|
77
77
|
};
|
|
78
78
|
|
|
79
|
-
const reviewRequest = (
|
|
79
|
+
export const reviewRequest = (
|
|
80
80
|
pjcode: string,
|
|
81
81
|
item: ConsoleListItem,
|
|
82
82
|
prUrl: string,
|
|
@@ -129,6 +129,33 @@ const reviewRequest = (
|
|
|
129
129
|
};
|
|
130
130
|
};
|
|
131
131
|
|
|
132
|
+
export const buildTriageRequest = (
|
|
133
|
+
pjcode: string,
|
|
134
|
+
item: ConsoleListItem,
|
|
135
|
+
action:
|
|
136
|
+
| ConsoleNextActionDateAction
|
|
137
|
+
| ConsoleCloseAction
|
|
138
|
+
| 'set_story'
|
|
139
|
+
| 'set_status',
|
|
140
|
+
extra?: { statusName?: string; storyOptionId?: string },
|
|
141
|
+
): ConsoleTriageRequest => ({
|
|
142
|
+
pjcode,
|
|
143
|
+
action,
|
|
144
|
+
issueUrl: item.url,
|
|
145
|
+
projectItemId: item.projectItemId,
|
|
146
|
+
...(extra ?? {}),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
export const buildIntmuxRequest = (
|
|
150
|
+
pjcode: string,
|
|
151
|
+
item: ConsoleListItem,
|
|
152
|
+
): ConsoleIntmuxRequest => ({
|
|
153
|
+
pjcode,
|
|
154
|
+
action: 'set_intmux',
|
|
155
|
+
issueUrl: item.url,
|
|
156
|
+
projectItemId: item.projectItemId,
|
|
157
|
+
});
|
|
158
|
+
|
|
132
159
|
const missingPjcodeError = (): Error =>
|
|
133
160
|
new Error('No project specified in the URL path.');
|
|
134
161
|
|
|
@@ -293,9 +293,9 @@ const readOperationErrorReason = async (
|
|
|
293
293
|
return raw;
|
|
294
294
|
};
|
|
295
295
|
|
|
296
|
-
export const postConsoleOperation = async (
|
|
296
|
+
export const postConsoleOperation = async <T extends Record<string, unknown>>(
|
|
297
297
|
apiPath: string,
|
|
298
|
-
body:
|
|
298
|
+
body: T,
|
|
299
299
|
): Promise<void> => {
|
|
300
300
|
const response = await fetch(apiPath, {
|
|
301
301
|
method: 'POST',
|
|
@@ -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
|
|
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}
|