github-issue-tower-defence-management 1.142.3 → 1.143.1

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 (25) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/bin/adapter/entry-points/console/ui-dist/assets/index-BWy8G--c.js +81 -0
  3. package/bin/adapter/entry-points/console/ui-dist/assets/{index-BaGbcbYI.css → index-Bp-p-XOs.css} +1 -1
  4. package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/src/adapter/entry-points/console/ui/e2e/consoleScenario.spec.ts +36 -0
  7. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleCopyCodeButton.stories.tsx +28 -0
  8. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleCopyCodeButton.test.tsx +65 -0
  9. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleCopyCodeButton.tsx +19 -0
  10. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMarkdownContent.test.tsx +103 -1
  11. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMarkdownContent.tsx +41 -5
  12. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCopyUrlButton.tsx +10 -42
  13. package/src/adapter/entry-points/console/ui/src/features/console/components/shared/ConsoleClipboardCopyButton.stories.tsx +32 -0
  14. package/src/adapter/entry-points/console/ui/src/features/console/components/shared/ConsoleClipboardCopyButton.test.tsx +67 -0
  15. package/src/adapter/entry-points/console/ui/src/features/console/components/shared/ConsoleClipboardCopyButton.tsx +57 -0
  16. package/src/adapter/entry-points/console/ui/src/features/console/logic/overlay.test.ts +55 -0
  17. package/src/adapter/entry-points/console/ui/src/features/console/logic/overlay.ts +17 -0
  18. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.test.tsx +68 -1
  19. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +12 -2
  20. package/src/adapter/entry-points/console/ui/src/index.css +31 -0
  21. package/src/adapter/entry-points/console/ui-dist/assets/index-BWy8G--c.js +81 -0
  22. package/src/adapter/entry-points/console/ui-dist/assets/{index-BaGbcbYI.css → index-Bp-p-XOs.css} +1 -1
  23. package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
  24. package/bin/adapter/entry-points/console/ui-dist/assets/index-2c5E3tdu.js +0 -81
  25. package/src/adapter/entry-points/console/ui-dist/assets/index-2c5E3tdu.js +0 -81
@@ -0,0 +1,57 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { Button } from '@/components/ui/button';
3
+
4
+ export type ConsoleClipboardCopyButtonProps = {
5
+ value: string;
6
+ idleText: string;
7
+ idleAriaLabel: string;
8
+ copiedAriaLabel: string;
9
+ className: string;
10
+ };
11
+
12
+ const COPIED_FEEDBACK_MS = 1500;
13
+
14
+ export const ConsoleClipboardCopyButton = ({
15
+ value,
16
+ idleText,
17
+ idleAriaLabel,
18
+ copiedAriaLabel,
19
+ className,
20
+ }: ConsoleClipboardCopyButtonProps) => {
21
+ const [copied, setCopied] = useState(false);
22
+ const resetTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
23
+
24
+ useEffect(
25
+ () => () => {
26
+ if (resetTimerRef.current !== null) {
27
+ clearTimeout(resetTimerRef.current);
28
+ }
29
+ },
30
+ [],
31
+ );
32
+
33
+ const handleCopy = async () => {
34
+ await navigator.clipboard.writeText(value);
35
+ setCopied(true);
36
+ if (resetTimerRef.current !== null) {
37
+ clearTimeout(resetTimerRef.current);
38
+ }
39
+ resetTimerRef.current = setTimeout(() => {
40
+ setCopied(false);
41
+ resetTimerRef.current = null;
42
+ }, COPIED_FEEDBACK_MS);
43
+ };
44
+
45
+ return (
46
+ <Button
47
+ type="button"
48
+ variant="ghost"
49
+ size="sm"
50
+ className={className}
51
+ aria-label={copied ? copiedAriaLabel : idleAriaLabel}
52
+ onClick={handleCopy}
53
+ >
54
+ {copied ? 'Copied' : idleText}
55
+ </Button>
56
+ );
57
+ };
@@ -2,6 +2,7 @@ import {
2
2
  countPendingItems,
3
3
  filterPendingItems,
4
4
  isOverlayEntryActed,
5
+ overlayEntriesActedSinceSnapshot,
5
6
  overlayKeyForItem,
6
7
  overlayStorageKey,
7
8
  writeOverlayEntry,
@@ -149,6 +150,60 @@ describe('workflow-blocker items are filtered by the done overlay like every oth
149
150
  });
150
151
  });
151
152
 
153
+ describe('overlayEntriesActedSinceSnapshot', () => {
154
+ it('drops an entry written before the snapshot was generated so the item is pending again', () => {
155
+ const overlay: ConsoleOverlay = {
156
+ PVTI_1: {
157
+ ts: Date.parse('2026-08-04T00:20:00.000Z'),
158
+ mode: 'todo-by-human',
159
+ done: true,
160
+ },
161
+ };
162
+ const acted = overlayEntriesActedSinceSnapshot(
163
+ overlay,
164
+ '2026-08-04T00:26:06Z',
165
+ );
166
+ expect(acted).toEqual({});
167
+ expect(countPendingItems([item(1)], acted)).toBe(1);
168
+ expect(filterPendingItems([item(1)], acted)).toEqual([item(1)]);
169
+ });
170
+
171
+ it('keeps an entry written after the snapshot was generated so the item stays hidden', () => {
172
+ const overlay: ConsoleOverlay = {
173
+ PVTI_1: {
174
+ ts: Date.parse('2026-08-04T00:30:00.000Z'),
175
+ mode: 'todo-by-human',
176
+ done: true,
177
+ },
178
+ };
179
+ const acted = overlayEntriesActedSinceSnapshot(
180
+ overlay,
181
+ '2026-08-04T00:26:06Z',
182
+ );
183
+ expect(countPendingItems([item(1)], acted)).toBe(0);
184
+ });
185
+
186
+ it('keeps an entry written at the exact snapshot generation time', () => {
187
+ const overlay: ConsoleOverlay = {
188
+ PVTI_1: {
189
+ ts: Date.parse('2026-08-04T00:26:06Z'),
190
+ mode: 'todo-by-human',
191
+ done: true,
192
+ },
193
+ };
194
+ expect(
195
+ overlayEntriesActedSinceSnapshot(overlay, '2026-08-04T00:26:06Z'),
196
+ ).toEqual(overlay);
197
+ });
198
+
199
+ it('suppresses nothing when the snapshot carries no parsable generation time', () => {
200
+ const overlay: ConsoleOverlay = {
201
+ PVTI_1: { ts: 500, mode: 'todo-by-human', done: true },
202
+ };
203
+ expect(overlayEntriesActedSinceSnapshot(overlay, '')).toEqual({});
204
+ });
205
+ });
206
+
152
207
  describe('writeOverlayEntry', () => {
153
208
  it('stamps the timestamp and mode on write', () => {
154
209
  const next = writeOverlayEntry({}, 'PVTI_1', { done: true }, 'prs', 1234);
@@ -30,6 +30,23 @@ export const filterPendingItems = (
30
30
  (item) => !isOverlayEntryActed(overlay[overlayKeyForItem(item)]),
31
31
  );
32
32
 
33
+ export const overlayEntriesActedSinceSnapshot = (
34
+ overlay: ConsoleOverlay,
35
+ snapshotGeneratedAt: string,
36
+ ): ConsoleOverlay => {
37
+ const snapshotGeneratedAtMs = Date.parse(snapshotGeneratedAt);
38
+ if (Number.isNaN(snapshotGeneratedAtMs)) {
39
+ return {};
40
+ }
41
+ const acted: ConsoleOverlay = {};
42
+ for (const [key, entry] of Object.entries(overlay)) {
43
+ if (entry.ts >= snapshotGeneratedAtMs) {
44
+ acted[key] = entry;
45
+ }
46
+ }
47
+ return acted;
48
+ };
49
+
33
50
  export const writeOverlayEntry = (
34
51
  overlay: ConsoleOverlay,
35
52
  key: string,
@@ -310,7 +310,11 @@ describe('ConsolePage', () => {
310
310
  localStorage.setItem(
311
311
  'pv_overlay_umino',
312
312
  JSON.stringify({
313
- PVTI_B1: { ts: 1, mode: 'workflow-blocker', done: true },
313
+ PVTI_B1: {
314
+ ts: Date.parse('2026-06-19T00:05:00.000Z'),
315
+ mode: 'workflow-blocker',
316
+ done: true,
317
+ },
314
318
  }),
315
319
  );
316
320
  window.history.replaceState(
@@ -333,6 +337,69 @@ describe('ConsolePage', () => {
333
337
  );
334
338
  });
335
339
 
340
+ it('lists an item again when the overlay marked it done before the served snapshot was generated', async () => {
341
+ const blockerItems = [
342
+ {
343
+ number: 701,
344
+ title: 'Blocked deployment task',
345
+ url: 'https://github.com/o/r/issues/701',
346
+ repo: 'o/r',
347
+ nameWithOwner: 'o/r',
348
+ projectItemId: 'PVTI_B1',
349
+ itemId: 'PVTI_B1',
350
+ isPr: false,
351
+ story: 'TDPM Console port',
352
+ status: 'In Progress',
353
+ nextActionDate: null,
354
+ nextActionHour: null,
355
+ dependedIssueUrls: [],
356
+ labels: [],
357
+ createdAt: '2026-06-17T00:00:00.000Z',
358
+ },
359
+ ];
360
+ const fetchMock = jest.fn(async (url: string) => {
361
+ const listMatch = url.match(/\/projects\/[^/]+\/([^/]+)\/list\.json/);
362
+ if (listMatch !== null) {
363
+ return {
364
+ ok: true,
365
+ status: 200,
366
+ json: async () =>
367
+ listMatch[1] === 'workflow-blocker'
368
+ ? { ...listPayload('workflow-blocker'), items: blockerItems }
369
+ : listPayload(listMatch[1]),
370
+ };
371
+ }
372
+ return { ok: true, status: 200, json: async () => ({ body: '# body' }) };
373
+ });
374
+ global.fetch = fetchMock as unknown as typeof fetch;
375
+ localStorage.setItem(
376
+ 'pv_overlay_umino',
377
+ JSON.stringify({
378
+ PVTI_B1: {
379
+ ts: Date.parse('2026-06-18T23:00:00.000Z'),
380
+ mode: 'workflow-blocker',
381
+ done: true,
382
+ },
383
+ }),
384
+ );
385
+ window.history.replaceState(
386
+ {},
387
+ '',
388
+ '/projects/umino/workflow-blocker?k=token',
389
+ );
390
+
391
+ const { getByText } = render(<ConsolePage />);
392
+ await waitFor(() => {
393
+ expect(getByText('Blocked deployment task')).toBeInTheDocument();
394
+ });
395
+ const blockerTab = within(tabBar())
396
+ .getByText('Workflow Blocker')
397
+ .closest('a');
398
+ expect(blockerTab?.querySelector('.console-tab-badge')?.textContent).toBe(
399
+ '1',
400
+ );
401
+ });
402
+
336
403
  it('renders the failure toast in English without any Japanese characters', async () => {
337
404
  const fetchMock = jest.fn(
338
405
  async (url: string, init?: { method?: string }) => {
@@ -28,6 +28,7 @@ import {
28
28
  import {
29
29
  countPendingItems,
30
30
  filterPendingItems,
31
+ overlayEntriesActedSinceSnapshot,
31
32
  overlayKeyForItem,
32
33
  } from '../logic/overlay';
33
34
  import type { ConsoleSwipeDirection } from '../logic/swipe';
@@ -67,7 +68,10 @@ export const ConsolePage = () => {
67
68
  }
68
69
  result[tab.name] = countPendingItems(
69
70
  snapshot.items,
70
- overlayState.overlay,
71
+ overlayEntriesActedSinceSnapshot(
72
+ overlayState.overlay,
73
+ snapshot.generatedAt,
74
+ ),
71
75
  );
72
76
  }
73
77
  return result;
@@ -92,7 +96,13 @@ export const ConsolePage = () => {
92
96
  if (activeSnapshot === null) {
93
97
  return [];
94
98
  }
95
- return filterPendingItems(activeSnapshot.items, overlayState.overlay);
99
+ return filterPendingItems(
100
+ activeSnapshot.items,
101
+ overlayEntriesActedSinceSnapshot(
102
+ overlayState.overlay,
103
+ activeSnapshot.generatedAt,
104
+ ),
105
+ );
96
106
  }, [activeSnapshot, overlayState.overlay]);
97
107
 
98
108
  const orderedPendingKeys = useMemo(
@@ -511,6 +511,37 @@ body {
511
511
  font-size: inherit;
512
512
  }
513
513
 
514
+ .console-markdown-code-block {
515
+ display: flex;
516
+ flex-direction: column;
517
+ margin: 0.5em 0;
518
+ }
519
+
520
+ .console-markdown-code-block pre {
521
+ margin: 0;
522
+ }
523
+
524
+ .console-markdown-code-copy-host {
525
+ display: flex;
526
+ justify-content: flex-end;
527
+ margin-bottom: 4px;
528
+ }
529
+
530
+ .console-copy-code-button {
531
+ height: 24px;
532
+ padding: 0 8px;
533
+ border: 1px solid #30363d;
534
+ border-radius: 6px;
535
+ background-color: #161b22;
536
+ color: #c9d1d9;
537
+ font-size: 0.75rem;
538
+ }
539
+
540
+ .console-copy-code-button:hover {
541
+ background-color: #21262d;
542
+ color: #e6edf3;
543
+ }
544
+
514
545
  .console-markdown blockquote {
515
546
  border-left: 3px solid #30363d;
516
547
  padding: 0 1em;