github-issue-tower-defence-management 2.7.0 → 2.9.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/.github/workflows/umino-project.yml +0 -8
- package/CHANGELOG.md +14 -0
- package/README.md +1 -1
- package/bin/adapter/entry-points/cli/index.js +3 -1
- package/bin/adapter/entry-points/cli/index.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/consoleOperationApi.js +12 -0
- package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-BSa2JTna.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/repositories/issue/ApiV3CheerioRestIssueRepository.js +5 -1
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/adapter/repositories/projectFieldDefinition.js +1 -0
- package/bin/adapter/repositories/projectFieldDefinition.js.map +1 -1
- package/jest.config.js +1 -0
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.ts +4 -1
- 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/consoleOperationApi.test.ts +84 -0
- package/src/adapter/entry-points/console/consoleOperationApi.ts +17 -0
- package/src/adapter/entry-points/console/ui/e2e/consoleScenario.spec.ts +6 -0
- package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +5 -1
- 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/ConsoleNextActionDateActions.test.tsx +7 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleNextActionDateActions.tsx +14 -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/logic/actionToast.test.ts +22 -0
- package/src/adapter/entry-points/console/ui/src/features/console/logic/actionToast.ts +6 -0
- package/src/adapter/entry-points/console/ui/src/features/console/logic/operations.ts +5 -1
- 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-BSa2JTna.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/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +63 -8
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +7 -1
- package/src/adapter/repositories/projectFieldDefinition.test.ts +1 -0
- package/src/adapter/repositories/projectFieldDefinition.ts +1 -0
- package/src/domain/entities/Project.ts +1 -0
- package/src/domain/usecases/ClearPastNextActionDateHourUseCase.test.ts +1 -0
- package/types/adapter/entry-points/cli/index.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/consoleOperationApi.d.ts.map +1 -1
- 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/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/adapter/repositories/projectFieldDefinition.d.ts.map +1 -1
- package/types/domain/entities/Project.d.ts +1 -0
- package/types/domain/entities/Project.d.ts.map +1 -1
- 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
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ConsoleChangedFile,
|
|
3
|
+
ConsoleComment,
|
|
4
|
+
ConsoleCommit,
|
|
5
|
+
ConsoleFieldOption,
|
|
6
|
+
ConsoleIssueState,
|
|
7
|
+
ConsoleListItem,
|
|
8
|
+
ConsolePullRequestStatus,
|
|
9
|
+
ConsoleRelatedPullRequest,
|
|
10
|
+
ConsoleStoryColorSource,
|
|
11
|
+
ConsoleStoryEntry,
|
|
12
|
+
ConsoleTabName,
|
|
13
|
+
} from '../logic/types';
|
|
14
|
+
|
|
15
|
+
export type AirplaneTabSnapshot = {
|
|
16
|
+
items: ConsoleListItem[];
|
|
17
|
+
generatedAt: string;
|
|
18
|
+
statusOptions: ConsoleFieldOption[];
|
|
19
|
+
storyOptions: ConsoleFieldOption[];
|
|
20
|
+
storyColors: ConsoleStoryColorSource;
|
|
21
|
+
stories: ConsoleStoryEntry[];
|
|
22
|
+
defaultNameWithOwner: string | null;
|
|
23
|
+
fromCache: boolean;
|
|
24
|
+
storyOrder: string[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type AirplaneItemSnapshot = {
|
|
28
|
+
body: string;
|
|
29
|
+
comments: ConsoleComment[];
|
|
30
|
+
state: ConsoleIssueState;
|
|
31
|
+
files: ConsoleChangedFile[] | null;
|
|
32
|
+
commits: ConsoleCommit[] | null;
|
|
33
|
+
prStatus: ConsolePullRequestStatus | null;
|
|
34
|
+
relatedPrs: ConsoleRelatedPullRequest[] | null;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type AirplaneSnapshot = {
|
|
38
|
+
capturedAt: string;
|
|
39
|
+
tabs: Record<string, Record<ConsoleTabName, AirplaneTabSnapshot>>;
|
|
40
|
+
items: Record<string, AirplaneItemSnapshot>;
|
|
41
|
+
failures: string[];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const AIRPLANE_CACHE_NAME = 'tdpm-airplane-v1';
|
|
45
|
+
const AIRPLANE_SNAPSHOT_REQUEST_URL = '/airplane-snapshot-data';
|
|
46
|
+
const AIRPLANE_MODE_STORAGE_KEY = 'tdpm_airplane_mode_on';
|
|
47
|
+
|
|
48
|
+
export const readAirplaneModeFlag = (): boolean => {
|
|
49
|
+
if (typeof localStorage === 'undefined') {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return localStorage.getItem(AIRPLANE_MODE_STORAGE_KEY) === '1';
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const writeAirplaneModeFlag = (on: boolean): void => {
|
|
56
|
+
if (typeof localStorage === 'undefined') {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (on) {
|
|
60
|
+
localStorage.setItem(AIRPLANE_MODE_STORAGE_KEY, '1');
|
|
61
|
+
} else {
|
|
62
|
+
localStorage.removeItem(AIRPLANE_MODE_STORAGE_KEY);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const storeAirplaneSnapshot = async (
|
|
67
|
+
snapshot: AirplaneSnapshot,
|
|
68
|
+
): Promise<void> => {
|
|
69
|
+
if (typeof caches === 'undefined') {
|
|
70
|
+
throw new Error('Cache API unavailable');
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const cache = await caches.open(AIRPLANE_CACHE_NAME);
|
|
74
|
+
await cache.put(
|
|
75
|
+
AIRPLANE_SNAPSHOT_REQUEST_URL,
|
|
76
|
+
new Response(JSON.stringify(snapshot), {
|
|
77
|
+
headers: { 'Content-Type': 'application/json' },
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
if (err instanceof Error && err.name === 'QuotaExceededError') {
|
|
82
|
+
throw new Error(
|
|
83
|
+
'Not enough browser storage to save the offline snapshot. Free up storage and retry.',
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const loadAirplaneSnapshot =
|
|
91
|
+
async (): Promise<AirplaneSnapshot | null> => {
|
|
92
|
+
if (typeof caches === 'undefined') {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const cache = await caches.open(AIRPLANE_CACHE_NAME);
|
|
96
|
+
const response = await cache.match(AIRPLANE_SNAPSHOT_REQUEST_URL);
|
|
97
|
+
if (response === undefined) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return response.json() as Promise<AirplaneSnapshot>;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const clearAirplaneSnapshot = async (): Promise<void> => {
|
|
104
|
+
if (typeof caches === 'undefined') {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
await caches.delete(AIRPLANE_CACHE_NAME);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
111
|
+
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
112
|
+
|
|
113
|
+
const getString = (value: unknown): string =>
|
|
114
|
+
typeof value === 'string' ? value : '';
|
|
115
|
+
|
|
116
|
+
const getNumber = (value: unknown): number =>
|
|
117
|
+
typeof value === 'number' ? value : 0;
|
|
118
|
+
|
|
119
|
+
const getBoolean = (value: unknown): boolean => value === true;
|
|
120
|
+
|
|
121
|
+
const parseStringArray = (value: unknown): string[] =>
|
|
122
|
+
Array.isArray(value)
|
|
123
|
+
? value.filter((entry): entry is string => typeof entry === 'string')
|
|
124
|
+
: [];
|
|
125
|
+
|
|
126
|
+
const parseMergeableStatus = (
|
|
127
|
+
value: unknown,
|
|
128
|
+
): 'MERGEABLE' | 'CONFLICTING' | 'UNKNOWN' => {
|
|
129
|
+
if (value === 'MERGEABLE') {
|
|
130
|
+
return 'MERGEABLE';
|
|
131
|
+
}
|
|
132
|
+
if (value === 'CONFLICTING') {
|
|
133
|
+
return 'CONFLICTING';
|
|
134
|
+
}
|
|
135
|
+
return 'UNKNOWN';
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const parseListItem = (item: unknown): ConsoleListItem | null => {
|
|
139
|
+
if (!isRecord(item)) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
number: getNumber(item.number),
|
|
144
|
+
title: getString(item.title),
|
|
145
|
+
url: getString(item.url),
|
|
146
|
+
repo: getString(item.repo),
|
|
147
|
+
nameWithOwner: getString(item.nameWithOwner),
|
|
148
|
+
projectItemId: getString(item.projectItemId),
|
|
149
|
+
itemId: getString(item.itemId),
|
|
150
|
+
isPr: getBoolean(item.isPr),
|
|
151
|
+
story: getString(item.story),
|
|
152
|
+
status: typeof item.status === 'string' ? item.status : null,
|
|
153
|
+
nextActionDate:
|
|
154
|
+
typeof item.nextActionDate === 'string' ? item.nextActionDate : null,
|
|
155
|
+
nextActionHour:
|
|
156
|
+
typeof item.nextActionHour === 'number' ? item.nextActionHour : null,
|
|
157
|
+
dependedIssueUrls: parseStringArray(item.dependedIssueUrls),
|
|
158
|
+
labels: parseStringArray(item.labels),
|
|
159
|
+
createdAt: getString(item.createdAt),
|
|
160
|
+
relatedOpenPullRequestUrls: parseStringArray(
|
|
161
|
+
item.relatedOpenPullRequestUrls,
|
|
162
|
+
),
|
|
163
|
+
agent: typeof item.agent === 'string' ? item.agent : null,
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const parseTabSnapshot = (payload: unknown): AirplaneTabSnapshot => {
|
|
168
|
+
if (!isRecord(payload)) {
|
|
169
|
+
return {
|
|
170
|
+
items: [],
|
|
171
|
+
generatedAt: '',
|
|
172
|
+
statusOptions: [],
|
|
173
|
+
storyOptions: [],
|
|
174
|
+
storyColors: {},
|
|
175
|
+
stories: [],
|
|
176
|
+
defaultNameWithOwner: null,
|
|
177
|
+
fromCache: false,
|
|
178
|
+
storyOrder: [],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
const items: ConsoleListItem[] = Array.isArray(payload.items)
|
|
182
|
+
? payload.items
|
|
183
|
+
.map(parseListItem)
|
|
184
|
+
.filter((item): item is ConsoleListItem => item !== null)
|
|
185
|
+
: [];
|
|
186
|
+
const statusOptions: ConsoleFieldOption[] = Array.isArray(
|
|
187
|
+
payload.statusOptions,
|
|
188
|
+
)
|
|
189
|
+
? (payload.statusOptions.filter(isRecord) as ConsoleFieldOption[])
|
|
190
|
+
: [];
|
|
191
|
+
const storyOptions: ConsoleFieldOption[] = Array.isArray(payload.storyOptions)
|
|
192
|
+
? (payload.storyOptions.filter(isRecord) as ConsoleFieldOption[])
|
|
193
|
+
: [];
|
|
194
|
+
const stories: ConsoleStoryEntry[] = Array.isArray(payload.stories)
|
|
195
|
+
? (payload.stories.filter(isRecord) as unknown as ConsoleStoryEntry[])
|
|
196
|
+
: [];
|
|
197
|
+
const storyOrder: string[] = Array.isArray(payload.storyOrder)
|
|
198
|
+
? payload.storyOrder.filter((s): s is string => typeof s === 'string')
|
|
199
|
+
: [];
|
|
200
|
+
return {
|
|
201
|
+
items,
|
|
202
|
+
generatedAt:
|
|
203
|
+
typeof payload.generatedAt === 'string' ? payload.generatedAt : '',
|
|
204
|
+
statusOptions,
|
|
205
|
+
storyOptions,
|
|
206
|
+
storyColors: isRecord(payload.storyColors)
|
|
207
|
+
? (payload.storyColors as ConsoleStoryColorSource)
|
|
208
|
+
: {},
|
|
209
|
+
stories,
|
|
210
|
+
defaultNameWithOwner:
|
|
211
|
+
typeof payload.defaultNameWithOwner === 'string'
|
|
212
|
+
? payload.defaultNameWithOwner
|
|
213
|
+
: null,
|
|
214
|
+
fromCache: false,
|
|
215
|
+
storyOrder,
|
|
216
|
+
};
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const parseComment = (item: unknown): ConsoleComment | null => {
|
|
220
|
+
if (!isRecord(item)) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
author: getString(item.author),
|
|
225
|
+
body: getString(item.body),
|
|
226
|
+
createdAt: getString(item.createdAt),
|
|
227
|
+
};
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const parseChangedFile = (item: unknown): ConsoleChangedFile | null => {
|
|
231
|
+
if (!isRecord(item)) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
path: getString(item.path),
|
|
236
|
+
additions: getNumber(item.additions),
|
|
237
|
+
deletions: getNumber(item.deletions),
|
|
238
|
+
status: getString(item.status),
|
|
239
|
+
patch: typeof item.patch === 'string' ? item.patch : null,
|
|
240
|
+
rawUrl: typeof item.rawUrl === 'string' ? item.rawUrl : null,
|
|
241
|
+
};
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const parseCommit = (item: unknown): ConsoleCommit | null => {
|
|
245
|
+
if (!isRecord(item)) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
sha: getString(item.sha),
|
|
250
|
+
message: getString(item.message),
|
|
251
|
+
author: getString(item.author),
|
|
252
|
+
authoredAt: getString(item.authoredAt),
|
|
253
|
+
};
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const parsePrStatus = (item: unknown): ConsolePullRequestStatus => {
|
|
257
|
+
if (!isRecord(item)) {
|
|
258
|
+
return {
|
|
259
|
+
found: false,
|
|
260
|
+
isConflicted: false,
|
|
261
|
+
mergeableStatus: 'UNKNOWN',
|
|
262
|
+
isPassedAllCiJob: false,
|
|
263
|
+
isCiStateSuccess: false,
|
|
264
|
+
isBranchOutOfDate: false,
|
|
265
|
+
missingRequiredCheckNames: [],
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
found: getBoolean(item.found),
|
|
270
|
+
isConflicted: getBoolean(item.isConflicted),
|
|
271
|
+
mergeableStatus: parseMergeableStatus(item.mergeableStatus),
|
|
272
|
+
isPassedAllCiJob: getBoolean(item.isPassedAllCiJob),
|
|
273
|
+
isCiStateSuccess: getBoolean(item.isCiStateSuccess),
|
|
274
|
+
isBranchOutOfDate: getBoolean(item.isBranchOutOfDate),
|
|
275
|
+
missingRequiredCheckNames: parseStringArray(item.missingRequiredCheckNames),
|
|
276
|
+
};
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const parseRelatedPr = (item: unknown): ConsoleRelatedPullRequest | null => {
|
|
280
|
+
if (!isRecord(item)) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
const summary = isRecord(item.summary)
|
|
284
|
+
? {
|
|
285
|
+
title: getString(item.summary.title),
|
|
286
|
+
body: getString(item.summary.body),
|
|
287
|
+
additions: getNumber(item.summary.additions),
|
|
288
|
+
deletions: getNumber(item.summary.deletions),
|
|
289
|
+
changedFiles: getNumber(item.summary.changedFiles),
|
|
290
|
+
}
|
|
291
|
+
: null;
|
|
292
|
+
return {
|
|
293
|
+
url: getString(item.url),
|
|
294
|
+
branchName: typeof item.branchName === 'string' ? item.branchName : null,
|
|
295
|
+
createdAt: getString(item.createdAt),
|
|
296
|
+
isDraft: getBoolean(item.isDraft),
|
|
297
|
+
isConflicted: getBoolean(item.isConflicted),
|
|
298
|
+
mergeableStatus: parseMergeableStatus(item.mergeableStatus),
|
|
299
|
+
isPassedAllCiJob: getBoolean(item.isPassedAllCiJob),
|
|
300
|
+
isCiStateSuccess: getBoolean(item.isCiStateSuccess),
|
|
301
|
+
isResolvedAllReviewComments: getBoolean(item.isResolvedAllReviewComments),
|
|
302
|
+
isBranchOutOfDate: getBoolean(item.isBranchOutOfDate),
|
|
303
|
+
missingRequiredCheckNames: parseStringArray(item.missingRequiredCheckNames),
|
|
304
|
+
summary,
|
|
305
|
+
};
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const parseItemSnapshot = (item: unknown): AirplaneItemSnapshot => {
|
|
309
|
+
if (!isRecord(item)) {
|
|
310
|
+
return {
|
|
311
|
+
body: '',
|
|
312
|
+
comments: [],
|
|
313
|
+
state: { state: 'open', merged: false, isPullRequest: false, title: '' },
|
|
314
|
+
files: null,
|
|
315
|
+
commits: null,
|
|
316
|
+
prStatus: null,
|
|
317
|
+
relatedPrs: null,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
const stateRaw = isRecord(item.state) ? item.state : {};
|
|
321
|
+
const state: ConsoleIssueState = {
|
|
322
|
+
state: getString(stateRaw.state) || 'open',
|
|
323
|
+
merged: getBoolean(stateRaw.merged),
|
|
324
|
+
isPullRequest: getBoolean(stateRaw.isPullRequest),
|
|
325
|
+
title: getString(stateRaw.title),
|
|
326
|
+
};
|
|
327
|
+
const files =
|
|
328
|
+
item.files === null
|
|
329
|
+
? null
|
|
330
|
+
: Array.isArray(item.files)
|
|
331
|
+
? item.files
|
|
332
|
+
.map(parseChangedFile)
|
|
333
|
+
.filter((f): f is ConsoleChangedFile => f !== null)
|
|
334
|
+
: null;
|
|
335
|
+
const commits =
|
|
336
|
+
item.commits === null
|
|
337
|
+
? null
|
|
338
|
+
: Array.isArray(item.commits)
|
|
339
|
+
? item.commits
|
|
340
|
+
.map(parseCommit)
|
|
341
|
+
.filter((c): c is ConsoleCommit => c !== null)
|
|
342
|
+
: null;
|
|
343
|
+
const prStatus = item.prStatus === null ? null : parsePrStatus(item.prStatus);
|
|
344
|
+
const relatedPrs =
|
|
345
|
+
item.relatedPrs === null
|
|
346
|
+
? null
|
|
347
|
+
: Array.isArray(item.relatedPrs)
|
|
348
|
+
? item.relatedPrs
|
|
349
|
+
.map(parseRelatedPr)
|
|
350
|
+
.filter((pr): pr is ConsoleRelatedPullRequest => pr !== null)
|
|
351
|
+
: null;
|
|
352
|
+
return {
|
|
353
|
+
body: getString(item.body),
|
|
354
|
+
comments: Array.isArray(item.comments)
|
|
355
|
+
? item.comments
|
|
356
|
+
.map(parseComment)
|
|
357
|
+
.filter((c): c is ConsoleComment => c !== null)
|
|
358
|
+
: [],
|
|
359
|
+
state,
|
|
360
|
+
files,
|
|
361
|
+
commits,
|
|
362
|
+
prStatus,
|
|
363
|
+
relatedPrs,
|
|
364
|
+
};
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
export const parseAirplaneSnapshot = (
|
|
368
|
+
raw: unknown,
|
|
369
|
+
): AirplaneSnapshot | null => {
|
|
370
|
+
if (!isRecord(raw)) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
const capturedAt = typeof raw.capturedAt === 'string' ? raw.capturedAt : '';
|
|
374
|
+
const failures = parseStringArray(raw.failures);
|
|
375
|
+
|
|
376
|
+
const tabs: Record<string, Record<ConsoleTabName, AirplaneTabSnapshot>> = {};
|
|
377
|
+
if (isRecord(raw.tabs)) {
|
|
378
|
+
for (const [pjcode, pjTabs] of Object.entries(raw.tabs)) {
|
|
379
|
+
if (!isRecord(pjTabs)) {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
const parsedPjTabs: Record<string, AirplaneTabSnapshot> = {};
|
|
383
|
+
for (const [tabName, tabPayload] of Object.entries(pjTabs)) {
|
|
384
|
+
parsedPjTabs[tabName] = parseTabSnapshot(tabPayload);
|
|
385
|
+
}
|
|
386
|
+
tabs[pjcode] = parsedPjTabs as Record<
|
|
387
|
+
ConsoleTabName,
|
|
388
|
+
AirplaneTabSnapshot
|
|
389
|
+
>;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const items: Record<string, AirplaneItemSnapshot> = {};
|
|
394
|
+
if (isRecord(raw.items)) {
|
|
395
|
+
for (const [url, itemData] of Object.entries(raw.items)) {
|
|
396
|
+
items[url] = parseItemSnapshot(itemData);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return { capturedAt, tabs, items, failures };
|
|
401
|
+
};
|
|
@@ -72,6 +72,28 @@ describe('actionToastMessage', () => {
|
|
|
72
72
|
);
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
+
it('labels the +1 hour snooze the same on every tab', () => {
|
|
76
|
+
const kind: ConsoleActionKind = {
|
|
77
|
+
type: 'next_action_date',
|
|
78
|
+
action: 'snooze_1hour',
|
|
79
|
+
};
|
|
80
|
+
expect(actionToastMessage(kind, 'prs')).toBe('Next Action Hour +1 hour');
|
|
81
|
+
expect(actionToastMessage(kind, 'todo-by-human')).toBe(
|
|
82
|
+
'Next Action Hour +1 hour',
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('labels the +3 hours snooze the same on every tab', () => {
|
|
87
|
+
const kind: ConsoleActionKind = {
|
|
88
|
+
type: 'next_action_date',
|
|
89
|
+
action: 'snooze_3hours',
|
|
90
|
+
};
|
|
91
|
+
expect(actionToastMessage(kind, 'prs')).toBe('Next Action Hour +3 hours');
|
|
92
|
+
expect(actionToastMessage(kind, 'todo-by-human')).toBe(
|
|
93
|
+
'Next Action Hour +3 hours',
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
75
97
|
it('labels close actions', () => {
|
|
76
98
|
expect(actionToastMessage({ type: 'close', action: 'close' }, 'prs')).toBe(
|
|
77
99
|
'Closed',
|
|
@@ -45,6 +45,12 @@ export const actionToastMessage = (
|
|
|
45
45
|
}
|
|
46
46
|
return 'Marked unnecessary';
|
|
47
47
|
case 'next_action_date':
|
|
48
|
+
if (kind.action === 'snooze_1hour') {
|
|
49
|
+
return 'Next Action Hour +1 hour';
|
|
50
|
+
}
|
|
51
|
+
if (kind.action === 'snooze_3hours') {
|
|
52
|
+
return 'Next Action Hour +3 hours';
|
|
53
|
+
}
|
|
48
54
|
if (kind.action === 'snooze_1day') {
|
|
49
55
|
return 'Next Action Date +1 day';
|
|
50
56
|
}
|
|
@@ -27,7 +27,11 @@ export type ConsoleReviewAction =
|
|
|
27
27
|
| 'unnecessary'
|
|
28
28
|
| 'totally_wrong';
|
|
29
29
|
|
|
30
|
-
export type ConsoleNextActionDateAction =
|
|
30
|
+
export type ConsoleNextActionDateAction =
|
|
31
|
+
| 'snooze_1hour'
|
|
32
|
+
| 'snooze_3hours'
|
|
33
|
+
| 'snooze_1day'
|
|
34
|
+
| 'snooze_1week';
|
|
31
35
|
|
|
32
36
|
export type ConsoleCloseAction = 'close' | 'close_not_planned';
|
|
33
37
|
|
|
@@ -30,6 +30,15 @@ const listPayload = (tab: string) => ({
|
|
|
30
30
|
statusOptions: [{ id: 's1', name: 'Awaiting Workspace', color: 'BLUE' }],
|
|
31
31
|
storyOptions: [{ id: 'st1', name: 'TDPM Console port', color: 'BLUE' }],
|
|
32
32
|
storyColors: { 'TDPM Console port': { color: 'BLUE' } },
|
|
33
|
+
stories: [
|
|
34
|
+
{
|
|
35
|
+
storyName: 'TDPM Console port',
|
|
36
|
+
storyOptionId: 'st1',
|
|
37
|
+
color: 'BLUE',
|
|
38
|
+
openItemCount: 1,
|
|
39
|
+
storyViewUrl: null,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
33
42
|
items:
|
|
34
43
|
tab === 'prs'
|
|
35
44
|
? [
|
|
@@ -516,6 +525,30 @@ describe('ConsolePage', () => {
|
|
|
516
525
|
jest.useRealTimers();
|
|
517
526
|
}
|
|
518
527
|
});
|
|
528
|
+
|
|
529
|
+
it('renders reorder buttons in the Stories tab', async () => {
|
|
530
|
+
window.history.replaceState({}, '', '/projects/acme/stories?k=token');
|
|
531
|
+
const { getAllByRole, getByText } = render(<ConsolePage />);
|
|
532
|
+
await waitFor(() => {
|
|
533
|
+
expect(
|
|
534
|
+
getByText('snapshot: 2026-06-19T00:00:00.000Z'),
|
|
535
|
+
).toBeInTheDocument();
|
|
536
|
+
});
|
|
537
|
+
expect(getAllByRole('button', { name: 'Move up' }).length).toBeGreaterThan(
|
|
538
|
+
0,
|
|
539
|
+
);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it('does not render reorder buttons in the Triage tab', async () => {
|
|
543
|
+
window.history.replaceState({}, '', '/projects/acme/triage?k=token');
|
|
544
|
+
const { queryAllByRole, getByText } = render(<ConsolePage />);
|
|
545
|
+
await waitFor(() => {
|
|
546
|
+
expect(
|
|
547
|
+
getByText('snapshot: 2026-06-19T00:00:00.000Z'),
|
|
548
|
+
).toBeInTheDocument();
|
|
549
|
+
});
|
|
550
|
+
expect(queryAllByRole('button', { name: 'Move up' })).toHaveLength(0);
|
|
551
|
+
});
|
|
519
552
|
});
|
|
520
553
|
|
|
521
554
|
const twoItemPrPayload = () => ({
|
|
@@ -1200,3 +1233,179 @@ describe('ConsolePage auto-advance tab', () => {
|
|
|
1200
1233
|
}
|
|
1201
1234
|
});
|
|
1202
1235
|
});
|
|
1236
|
+
|
|
1237
|
+
describe('ConsolePage airplane mode write guard', () => {
|
|
1238
|
+
const prItem = {
|
|
1239
|
+
number: 851,
|
|
1240
|
+
title: 'Add serveConsole subcommand',
|
|
1241
|
+
url: 'https://github.com/o/r/pull/851',
|
|
1242
|
+
repo: 'o/r',
|
|
1243
|
+
nameWithOwner: 'o/r',
|
|
1244
|
+
projectItemId: 'PVTI_1',
|
|
1245
|
+
itemId: 'PVTI_1',
|
|
1246
|
+
isPr: true,
|
|
1247
|
+
relatedOpenPullRequestUrls: [],
|
|
1248
|
+
story: 'TDPM Console port',
|
|
1249
|
+
status: 'Awaiting Quality Check',
|
|
1250
|
+
nextActionDate: null,
|
|
1251
|
+
nextActionHour: null,
|
|
1252
|
+
dependedIssueUrls: [],
|
|
1253
|
+
labels: [],
|
|
1254
|
+
createdAt: '2026-06-17T00:00:00.000Z',
|
|
1255
|
+
agent: null,
|
|
1256
|
+
};
|
|
1257
|
+
|
|
1258
|
+
const airplaneSnapshot = {
|
|
1259
|
+
capturedAt: '2026-06-19T00:00:00.000Z',
|
|
1260
|
+
tabs: {
|
|
1261
|
+
acme: {
|
|
1262
|
+
prs: {
|
|
1263
|
+
items: [prItem],
|
|
1264
|
+
generatedAt: '2026-06-19T00:00:00.000Z',
|
|
1265
|
+
statusOptions: [
|
|
1266
|
+
{ id: 's1', name: 'Awaiting Workspace', color: 'BLUE' },
|
|
1267
|
+
],
|
|
1268
|
+
storyOptions: [
|
|
1269
|
+
{ id: 'st1', name: 'TDPM Console port', color: 'BLUE' },
|
|
1270
|
+
],
|
|
1271
|
+
storyColors: { 'TDPM Console port': { color: 'BLUE' } },
|
|
1272
|
+
stories: [],
|
|
1273
|
+
defaultNameWithOwner: 'o/r',
|
|
1274
|
+
fromCache: false,
|
|
1275
|
+
storyOrder: [],
|
|
1276
|
+
},
|
|
1277
|
+
'workflow-blocker': {
|
|
1278
|
+
items: [],
|
|
1279
|
+
generatedAt: '',
|
|
1280
|
+
statusOptions: [],
|
|
1281
|
+
storyOptions: [],
|
|
1282
|
+
storyColors: {},
|
|
1283
|
+
stories: [],
|
|
1284
|
+
defaultNameWithOwner: null,
|
|
1285
|
+
fromCache: false,
|
|
1286
|
+
storyOrder: [],
|
|
1287
|
+
},
|
|
1288
|
+
'failed-preparation': {
|
|
1289
|
+
items: [],
|
|
1290
|
+
generatedAt: '',
|
|
1291
|
+
statusOptions: [],
|
|
1292
|
+
storyOptions: [],
|
|
1293
|
+
storyColors: {},
|
|
1294
|
+
stories: [],
|
|
1295
|
+
defaultNameWithOwner: null,
|
|
1296
|
+
fromCache: false,
|
|
1297
|
+
storyOrder: [],
|
|
1298
|
+
},
|
|
1299
|
+
'todo-by-human': {
|
|
1300
|
+
items: [],
|
|
1301
|
+
generatedAt: '',
|
|
1302
|
+
statusOptions: [],
|
|
1303
|
+
storyOptions: [],
|
|
1304
|
+
storyColors: {},
|
|
1305
|
+
stories: [],
|
|
1306
|
+
defaultNameWithOwner: null,
|
|
1307
|
+
fromCache: false,
|
|
1308
|
+
storyOrder: [],
|
|
1309
|
+
},
|
|
1310
|
+
'todo-by-agent': {
|
|
1311
|
+
items: [],
|
|
1312
|
+
generatedAt: '',
|
|
1313
|
+
statusOptions: [],
|
|
1314
|
+
storyOptions: [],
|
|
1315
|
+
storyColors: {},
|
|
1316
|
+
stories: [],
|
|
1317
|
+
defaultNameWithOwner: null,
|
|
1318
|
+
fromCache: false,
|
|
1319
|
+
storyOrder: [],
|
|
1320
|
+
},
|
|
1321
|
+
stories: {
|
|
1322
|
+
items: [],
|
|
1323
|
+
generatedAt: '',
|
|
1324
|
+
statusOptions: [],
|
|
1325
|
+
storyOptions: [],
|
|
1326
|
+
storyColors: {},
|
|
1327
|
+
stories: [],
|
|
1328
|
+
defaultNameWithOwner: null,
|
|
1329
|
+
fromCache: false,
|
|
1330
|
+
storyOrder: [],
|
|
1331
|
+
},
|
|
1332
|
+
},
|
|
1333
|
+
},
|
|
1334
|
+
items: {},
|
|
1335
|
+
failures: [],
|
|
1336
|
+
};
|
|
1337
|
+
|
|
1338
|
+
beforeEach(() => {
|
|
1339
|
+
localStorage.clear();
|
|
1340
|
+
window.history.replaceState({}, '', '/projects/acme/prs?k=token');
|
|
1341
|
+
|
|
1342
|
+
localStorage.setItem('tdpm_airplane_mode_on', '1');
|
|
1343
|
+
|
|
1344
|
+
const snapshotJson = JSON.stringify(airplaneSnapshot);
|
|
1345
|
+
const mockCache = {
|
|
1346
|
+
put: jest.fn(),
|
|
1347
|
+
match: jest.fn().mockResolvedValue(
|
|
1348
|
+
new Response(snapshotJson, {
|
|
1349
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1350
|
+
}),
|
|
1351
|
+
),
|
|
1352
|
+
delete: jest.fn(),
|
|
1353
|
+
};
|
|
1354
|
+
Object.defineProperty(global, 'caches', {
|
|
1355
|
+
writable: true,
|
|
1356
|
+
configurable: true,
|
|
1357
|
+
value: {
|
|
1358
|
+
open: jest.fn().mockResolvedValue(mockCache),
|
|
1359
|
+
delete: jest.fn().mockResolvedValue(true),
|
|
1360
|
+
},
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
global.fetch = jest.fn(async (url: string) => {
|
|
1364
|
+
if (url === '/api/features') {
|
|
1365
|
+
return {
|
|
1366
|
+
ok: true,
|
|
1367
|
+
status: 200,
|
|
1368
|
+
json: async () => ({ airplaneMode: true }),
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
if (url === '/api/projects') {
|
|
1372
|
+
return {
|
|
1373
|
+
ok: true,
|
|
1374
|
+
status: 200,
|
|
1375
|
+
json: async () => ({ pjcodes: ['acme'] }),
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
return { ok: true, status: 200, json: async () => ({ body: '# body' }) };
|
|
1379
|
+
}) as unknown as typeof fetch;
|
|
1380
|
+
});
|
|
1381
|
+
|
|
1382
|
+
afterEach(() => {
|
|
1383
|
+
Object.defineProperty(global, 'caches', {
|
|
1384
|
+
writable: true,
|
|
1385
|
+
configurable: true,
|
|
1386
|
+
value: undefined,
|
|
1387
|
+
});
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
it('shows an airplane mode error toast instead of enqueuing when airplane mode is on', async () => {
|
|
1391
|
+
const { getByText, findByText } = render(<ConsolePage />);
|
|
1392
|
+
|
|
1393
|
+
await waitFor(() => {
|
|
1394
|
+
expect(getByText('Add serveConsole subcommand')).toBeInTheDocument();
|
|
1395
|
+
});
|
|
1396
|
+
|
|
1397
|
+
await waitFor(() => {
|
|
1398
|
+
expect(getByText('Turn off')).toBeInTheDocument();
|
|
1399
|
+
});
|
|
1400
|
+
|
|
1401
|
+
fireEvent.click(getByText('Add serveConsole subcommand'));
|
|
1402
|
+
expect(await findByText('Approve & Merge')).toBeInTheDocument();
|
|
1403
|
+
fireEvent.click(getByText('Approve & Merge'));
|
|
1404
|
+
|
|
1405
|
+
await waitFor(() => {
|
|
1406
|
+
expect(getByText('Airplane mode')).toBeInTheDocument();
|
|
1407
|
+
});
|
|
1408
|
+
expect(getByText(/network connection/i)).toBeInTheDocument();
|
|
1409
|
+
expect(document.querySelector('.console-undo-toast')).toBeNull();
|
|
1410
|
+
});
|
|
1411
|
+
});
|