github-issue-tower-defence-management 2.7.0 → 2.8.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/bin/adapter/entry-points/cli/index.js +3 -1
  3. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  4. package/bin/adapter/entry-points/console/consoleAirplaneSnapshotApi.js +347 -0
  5. package/bin/adapter/entry-points/console/consoleAirplaneSnapshotApi.js.map +1 -0
  6. package/bin/adapter/entry-points/console/ui-dist/assets/index-DkMqSpkk.js +88 -0
  7. package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
  8. package/bin/adapter/entry-points/console/webServer.js +28 -0
  9. package/bin/adapter/entry-points/console/webServer.js.map +1 -1
  10. package/jest.config.js +1 -0
  11. package/package.json +1 -1
  12. package/src/adapter/entry-points/cli/index.ts +4 -1
  13. package/src/adapter/entry-points/console/consoleAirplaneSnapshotApi.test.ts +658 -0
  14. package/src/adapter/entry-points/console/consoleAirplaneSnapshotApi.ts +482 -0
  15. package/src/adapter/entry-points/console/ui/jest-globals.cjs +26 -0
  16. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleFileDiff.test.tsx +10 -0
  17. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleFileDiff.tsx +23 -16
  18. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleAirplaneModeButton.stories.tsx +56 -0
  19. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleAirplaneModeButton.test.tsx +111 -0
  20. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleAirplaneModeButton.tsx +92 -0
  21. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.stories.tsx +15 -0
  22. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.test.tsx +19 -0
  23. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.tsx +30 -1
  24. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleErrorToast.stories.tsx +10 -0
  25. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleUndoToast.test.tsx +1 -3
  26. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleUndoToast.tsx +8 -7
  27. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useAirplaneMode.test.ts +304 -0
  28. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useAirplaneMode.ts +167 -0
  29. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleActionQueue.test.ts +15 -0
  30. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleActionQueue.ts +6 -1
  31. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleCaches.test.ts +77 -0
  32. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleCaches.ts +48 -3
  33. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleFeaturesConfig.test.ts +51 -0
  34. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleFeaturesConfig.ts +39 -0
  35. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.test.ts +215 -0
  36. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.ts +20 -1
  37. package/src/adapter/entry-points/console/ui/src/features/console/lib/airplaneSnapshot.test.ts +361 -0
  38. package/src/adapter/entry-points/console/ui/src/features/console/lib/airplaneSnapshot.ts +401 -0
  39. package/src/adapter/entry-points/console/ui/src/features/console/lib/resourceCache.ts +4 -0
  40. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.test.tsx +209 -0
  41. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +27 -2
  42. package/src/adapter/entry-points/console/ui-dist/assets/index-DkMqSpkk.js +88 -0
  43. package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
  44. package/src/adapter/entry-points/console/webServer.test.ts +189 -0
  45. package/src/adapter/entry-points/console/webServer.ts +38 -0
  46. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  47. package/types/adapter/entry-points/console/consoleAirplaneSnapshotApi.d.ts +90 -0
  48. package/types/adapter/entry-points/console/consoleAirplaneSnapshotApi.d.ts.map +1 -0
  49. package/types/adapter/entry-points/console/webServer.d.ts +1 -0
  50. package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
  51. package/bin/adapter/entry-points/console/ui-dist/assets/index-CaxYBHiT.js +0 -86
  52. package/src/adapter/entry-points/console/ui-dist/assets/index-CaxYBHiT.js +0 -86
@@ -1,4 +1,5 @@
1
1
  import { useEffect, useState } from 'react';
2
+ import type { AirplaneSnapshot } from '../lib/airplaneSnapshot';
2
3
  import type {
3
4
  ConsoleFieldOption,
4
5
  ConsoleListItem,
@@ -205,6 +206,7 @@ const fetchSnapshots = async (
205
206
 
206
207
  export const useConsoleTabData = (
207
208
  pjcode: string | null,
209
+ airplaneSnapshot: AirplaneSnapshot | null = null,
208
210
  ): ConsoleTabDataState => {
209
211
  const [snapshots, setSnapshots] =
210
212
  useState<Record<ConsoleTabName, ConsoleTabSnapshot | null>>(emptySnapshots);
@@ -212,6 +214,23 @@ export const useConsoleTabData = (
212
214
  const [error, setError] = useState<string | null>(null);
213
215
 
214
216
  useEffect(() => {
217
+ if (airplaneSnapshot !== null && pjcode !== null) {
218
+ const pjTabs = airplaneSnapshot.tabs[pjcode];
219
+ if (pjTabs !== undefined) {
220
+ const next = emptySnapshots();
221
+ for (const tab of CONSOLE_TABS) {
222
+ const stored = pjTabs[tab.name];
223
+ if (stored !== undefined) {
224
+ next[tab.name] = stored;
225
+ }
226
+ }
227
+ setSnapshots(next);
228
+ setIsLoading(false);
229
+ setError(null);
230
+ return;
231
+ }
232
+ }
233
+
215
234
  let cancelled = false;
216
235
  setIsLoading(true);
217
236
  setError(null);
@@ -251,7 +270,7 @@ export const useConsoleTabData = (
251
270
  cancelled = true;
252
271
  clearInterval(timer);
253
272
  };
254
- }, [pjcode]);
273
+ }, [pjcode, airplaneSnapshot]);
255
274
 
256
275
  return { snapshots, isLoading, error };
257
276
  };
@@ -0,0 +1,361 @@
1
+ import {
2
+ parseAirplaneSnapshot,
3
+ readAirplaneModeFlag,
4
+ storeAirplaneSnapshot,
5
+ writeAirplaneModeFlag,
6
+ } from './airplaneSnapshot';
7
+
8
+ const minimalSnapshot = () => ({
9
+ capturedAt: '2026-01-01T00:00:00Z',
10
+ tabs: {},
11
+ items: {},
12
+ failures: [],
13
+ });
14
+
15
+ describe('airplaneSnapshot', () => {
16
+ describe('parseAirplaneSnapshot', () => {
17
+ it('returns null for non-object input', () => {
18
+ expect(parseAirplaneSnapshot(null)).toBeNull();
19
+ expect(parseAirplaneSnapshot('string')).toBeNull();
20
+ expect(parseAirplaneSnapshot(42)).toBeNull();
21
+ });
22
+
23
+ it('parses a minimal valid snapshot', () => {
24
+ const raw = {
25
+ capturedAt: '2026-01-01T00:00:00Z',
26
+ tabs: {},
27
+ items: {},
28
+ failures: [],
29
+ };
30
+ const result = parseAirplaneSnapshot(raw);
31
+ expect(result).not.toBeNull();
32
+ expect(result?.capturedAt).toBe('2026-01-01T00:00:00Z');
33
+ expect(result?.failures).toEqual([]);
34
+ });
35
+
36
+ it('parses tab snapshots with items', () => {
37
+ const raw = {
38
+ capturedAt: '2026-01-01T00:00:00Z',
39
+ tabs: {
40
+ acme: {
41
+ prs: {
42
+ generatedAt: '2026-01-01T00:00:00Z',
43
+ statusOptions: [{ id: 'x', name: 'Open', color: 'GREEN' }],
44
+ storyOptions: [],
45
+ storyColors: {},
46
+ items: [
47
+ {
48
+ number: 1,
49
+ title: 'Fix bug',
50
+ url: 'https://github.com/o/r/pull/1',
51
+ repo: 'o/r',
52
+ nameWithOwner: 'o/r',
53
+ projectItemId: 'PVTI_1',
54
+ itemId: 'PVTI_1',
55
+ isPr: true,
56
+ story: 'regular',
57
+ status: 'In Progress',
58
+ nextActionDate: null,
59
+ nextActionHour: null,
60
+ dependedIssueUrls: [],
61
+ labels: ['bug'],
62
+ createdAt: '2026-01-01T00:00:00Z',
63
+ relatedOpenPullRequestUrls: [],
64
+ },
65
+ ],
66
+ },
67
+ },
68
+ },
69
+ items: {},
70
+ failures: [],
71
+ };
72
+
73
+ const result = parseAirplaneSnapshot(raw);
74
+ expect(result?.tabs.acme?.prs.items).toHaveLength(1);
75
+ expect(result?.tabs.acme?.prs.items[0].title).toBe('Fix bug');
76
+ expect(result?.tabs.acme?.prs.statusOptions).toHaveLength(1);
77
+ expect(result?.tabs.acme?.prs.fromCache).toBe(false);
78
+ expect(result?.tabs.acme?.prs.storyOrder).toEqual([]);
79
+ });
80
+
81
+ it('parses storyOrder and fromCache from a tab snapshot', () => {
82
+ const raw = {
83
+ capturedAt: '2026-01-01T00:00:00Z',
84
+ tabs: {
85
+ acme: {
86
+ prs: {
87
+ generatedAt: '2026-01-01T00:00:00Z',
88
+ statusOptions: [],
89
+ storyOptions: [],
90
+ storyColors: {},
91
+ items: [],
92
+ stories: [],
93
+ defaultNameWithOwner: null,
94
+ storyOrder: ['regular', 'chore'],
95
+ },
96
+ },
97
+ },
98
+ items: {},
99
+ failures: [],
100
+ };
101
+ const result = parseAirplaneSnapshot(raw);
102
+ expect(result?.tabs.acme?.prs.storyOrder).toEqual(['regular', 'chore']);
103
+ expect(result?.tabs.acme?.prs.fromCache).toBe(false);
104
+ });
105
+
106
+ it('parses item snapshots with PR fields', () => {
107
+ const url = 'https://github.com/o/r/pull/1';
108
+ const raw = {
109
+ capturedAt: '2026-01-01T00:00:00Z',
110
+ tabs: {},
111
+ failures: [],
112
+ items: {
113
+ [url]: {
114
+ body: 'PR body text',
115
+ comments: [
116
+ {
117
+ author: 'alice',
118
+ body: 'LGTM',
119
+ createdAt: '2026-01-01T01:00:00Z',
120
+ },
121
+ ],
122
+ state: {
123
+ state: 'open',
124
+ merged: false,
125
+ isPullRequest: true,
126
+ title: 'PR',
127
+ },
128
+ files: [
129
+ {
130
+ path: 'src/foo.ts',
131
+ additions: 5,
132
+ deletions: 2,
133
+ status: 'modified',
134
+ patch: '@@ diff @@',
135
+ rawUrl: null,
136
+ },
137
+ ],
138
+ commits: [
139
+ {
140
+ sha: 'abc',
141
+ message: 'fix',
142
+ author: 'alice',
143
+ authoredAt: '2026-01-01T00:00:00Z',
144
+ },
145
+ ],
146
+ prStatus: {
147
+ found: true,
148
+ isConflicted: false,
149
+ mergeableStatus: 'MERGEABLE',
150
+ isPassedAllCiJob: true,
151
+ isCiStateSuccess: true,
152
+ isBranchOutOfDate: false,
153
+ missingRequiredCheckNames: [],
154
+ },
155
+ relatedPrs: null,
156
+ },
157
+ },
158
+ };
159
+
160
+ const result = parseAirplaneSnapshot(raw);
161
+ const item = result?.items[url];
162
+ expect(item?.body).toBe('PR body text');
163
+ expect(item?.comments).toHaveLength(1);
164
+ expect(item?.comments[0].author).toBe('alice');
165
+ expect(item?.state.isPullRequest).toBe(true);
166
+ expect(item?.files).toHaveLength(1);
167
+ expect(item?.files?.[0].path).toBe('src/foo.ts');
168
+ expect(item?.commits).toHaveLength(1);
169
+ expect(item?.prStatus?.isPassedAllCiJob).toBe(true);
170
+ expect(item?.prStatus?.mergeableStatus).toBe('MERGEABLE');
171
+ expect(item?.relatedPrs).toBeNull();
172
+ });
173
+
174
+ it('parses item snapshots with issue fields', () => {
175
+ const url = 'https://github.com/o/r/issues/5';
176
+ const raw = {
177
+ capturedAt: '2026-01-01T00:00:00Z',
178
+ tabs: {},
179
+ failures: [],
180
+ items: {
181
+ [url]: {
182
+ body: 'Issue body',
183
+ comments: [],
184
+ state: {
185
+ state: 'open',
186
+ merged: false,
187
+ isPullRequest: false,
188
+ title: 'Issue',
189
+ },
190
+ files: null,
191
+ commits: null,
192
+ prStatus: null,
193
+ relatedPrs: [
194
+ {
195
+ url: 'https://github.com/o/r/pull/10',
196
+ branchName: 'fix-branch',
197
+ createdAt: '2026-01-01T00:00:00Z',
198
+ isDraft: false,
199
+ isConflicted: false,
200
+ mergeableStatus: 'MERGEABLE',
201
+ isPassedAllCiJob: true,
202
+ isCiStateSuccess: true,
203
+ isResolvedAllReviewComments: true,
204
+ isBranchOutOfDate: false,
205
+ missingRequiredCheckNames: [],
206
+ summary: null,
207
+ },
208
+ ],
209
+ },
210
+ },
211
+ };
212
+
213
+ const result = parseAirplaneSnapshot(raw);
214
+ const item = result?.items[url];
215
+ expect(item?.files).toBeNull();
216
+ expect(item?.commits).toBeNull();
217
+ expect(item?.prStatus).toBeNull();
218
+ expect(item?.relatedPrs).toHaveLength(1);
219
+ expect(item?.relatedPrs?.[0].url).toBe('https://github.com/o/r/pull/10');
220
+ });
221
+
222
+ it('handles unknown mergeableStatus gracefully', () => {
223
+ const url = 'https://github.com/o/r/pull/2';
224
+ const raw = {
225
+ capturedAt: '',
226
+ tabs: {},
227
+ failures: [],
228
+ items: {
229
+ [url]: {
230
+ body: '',
231
+ comments: [],
232
+ state: {
233
+ state: 'open',
234
+ merged: false,
235
+ isPullRequest: true,
236
+ title: '',
237
+ },
238
+ files: [],
239
+ commits: [],
240
+ prStatus: {
241
+ found: true,
242
+ isConflicted: false,
243
+ mergeableStatus: 'UNKNOWN',
244
+ isPassedAllCiJob: false,
245
+ isCiStateSuccess: false,
246
+ isBranchOutOfDate: false,
247
+ missingRequiredCheckNames: [],
248
+ },
249
+ relatedPrs: null,
250
+ },
251
+ },
252
+ };
253
+ const result = parseAirplaneSnapshot(raw);
254
+ expect(result?.items[url]?.prStatus?.mergeableStatus).toBe('UNKNOWN');
255
+ });
256
+
257
+ it('records failures from the raw payload', () => {
258
+ const raw = {
259
+ capturedAt: '',
260
+ tabs: {},
261
+ items: {},
262
+ failures: ['https://github.com/o/r/issues/99'],
263
+ };
264
+ const result = parseAirplaneSnapshot(raw);
265
+ expect(result?.failures).toContain('https://github.com/o/r/issues/99');
266
+ });
267
+
268
+ it('parses stories and defaultNameWithOwner from a tab snapshot', () => {
269
+ const raw = {
270
+ capturedAt: '2026-01-01T00:00:00Z',
271
+ tabs: {
272
+ acme: {
273
+ stories: {
274
+ generatedAt: '2026-01-01T00:00:00Z',
275
+ statusOptions: [],
276
+ storyOptions: [],
277
+ storyColors: {},
278
+ items: [],
279
+ stories: [
280
+ {
281
+ storyName: 'regular',
282
+ storyOptionId: 'opt-1',
283
+ color: 'GREEN',
284
+ openItemCount: 3,
285
+ },
286
+ ],
287
+ defaultNameWithOwner: 'owner/repo',
288
+ },
289
+ },
290
+ },
291
+ items: {},
292
+ failures: [],
293
+ };
294
+ const result = parseAirplaneSnapshot(raw);
295
+ const storiesTab = result?.tabs.acme?.stories;
296
+ expect(storiesTab?.stories).toHaveLength(1);
297
+ expect(storiesTab?.stories[0].storyName).toBe('regular');
298
+ expect(storiesTab?.defaultNameWithOwner).toBe('owner/repo');
299
+ });
300
+ });
301
+
302
+ describe('readAirplaneModeFlag / writeAirplaneModeFlag', () => {
303
+ beforeEach(() => {
304
+ localStorage.clear();
305
+ });
306
+
307
+ it('returns false when the flag has never been written', () => {
308
+ expect(readAirplaneModeFlag()).toBe(false);
309
+ });
310
+
311
+ it('returns true after writeAirplaneModeFlag(true)', () => {
312
+ writeAirplaneModeFlag(true);
313
+ expect(readAirplaneModeFlag()).toBe(true);
314
+ });
315
+
316
+ it('returns false after writeAirplaneModeFlag(false)', () => {
317
+ writeAirplaneModeFlag(true);
318
+ writeAirplaneModeFlag(false);
319
+ expect(readAirplaneModeFlag()).toBe(false);
320
+ });
321
+ });
322
+
323
+ describe('storeAirplaneSnapshot', () => {
324
+ it('rejects when Cache API is unavailable', async () => {
325
+ const originalCaches = globalThis.caches;
326
+ Object.defineProperty(globalThis, 'caches', {
327
+ configurable: true,
328
+ writable: true,
329
+ value: undefined,
330
+ });
331
+ await expect(storeAirplaneSnapshot(minimalSnapshot())).rejects.toThrow(
332
+ 'Cache API unavailable',
333
+ );
334
+ Object.defineProperty(globalThis, 'caches', {
335
+ configurable: true,
336
+ writable: true,
337
+ value: originalCaches,
338
+ });
339
+ });
340
+
341
+ it('rejects with a user-readable message when cache.put throws QuotaExceededError', async () => {
342
+ const quotaError = new DOMException(
343
+ 'QuotaExceededError',
344
+ 'QuotaExceededError',
345
+ );
346
+ const mockCache = {
347
+ put: jest.fn().mockRejectedValue(quotaError),
348
+ };
349
+ Object.defineProperty(globalThis, 'caches', {
350
+ configurable: true,
351
+ writable: true,
352
+ value: {
353
+ open: jest.fn().mockResolvedValue(mockCache),
354
+ },
355
+ });
356
+ await expect(storeAirplaneSnapshot(minimalSnapshot())).rejects.toThrow(
357
+ 'browser storage',
358
+ );
359
+ });
360
+ });
361
+ });