github-issue-tower-defence-management 1.182.0 → 1.183.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.
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>TDPM Console</title>
7
- <script type="module" crossorigin src="/assets/index-y4IbtMEJ.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-Cc2LSARI.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/assets/index-8Ni5iu-C.css">
9
9
  </head>
10
10
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-issue-tower-defence-management",
3
- "version": "1.182.0",
3
+ "version": "1.183.0",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "scripts": {
@@ -309,6 +309,80 @@ test('opens the comment input with the item detail, keeps it on screen while the
309
309
  expect(dockHeightAfterPosting).toBe(dockHeightBeforePosting);
310
310
  });
311
311
 
312
+ test('preserves the unsent comment draft when swiping between tasks', async ({
313
+ page,
314
+ }) => {
315
+ await page.goto(harness.appRootUrl);
316
+
317
+ await tabByLabel(page, 'Triage').click();
318
+
319
+ await itemRowByText(
320
+ page,
321
+ 'Add Sonnet to Opus weekly-limit fallback routing per token',
322
+ ).click();
323
+
324
+ await expect(page.locator('.console-composer-input')).toBeVisible();
325
+ await page.locator('.console-composer-input').fill('work in progress draft');
326
+
327
+ const swipeDetailScreen = async (
328
+ fromX: number,
329
+ toX: number,
330
+ ): Promise<void> => {
331
+ await page.evaluate(
332
+ ([from, to]: [number, number]) => {
333
+ const screen = document.querySelector('.console-detail-screen');
334
+ if (!screen) throw new Error('.console-detail-screen not found');
335
+ const mkTouch = (x: number): Touch =>
336
+ new Touch({
337
+ identifier: 1,
338
+ target: screen,
339
+ clientX: x,
340
+ clientY: 200,
341
+ pageX: x,
342
+ pageY: 200,
343
+ });
344
+ screen.dispatchEvent(
345
+ new TouchEvent('touchstart', {
346
+ bubbles: true,
347
+ cancelable: true,
348
+ touches: [mkTouch(from)],
349
+ }),
350
+ );
351
+ screen.dispatchEvent(
352
+ new TouchEvent('touchmove', {
353
+ bubbles: true,
354
+ cancelable: true,
355
+ touches: [mkTouch(to)],
356
+ }),
357
+ );
358
+ screen.dispatchEvent(
359
+ new TouchEvent('touchend', {
360
+ bubbles: true,
361
+ cancelable: true,
362
+ changedTouches: [mkTouch(to)],
363
+ touches: [],
364
+ }),
365
+ );
366
+ },
367
+ [fromX, toX] as [number, number],
368
+ );
369
+ };
370
+
371
+ await swipeDetailScreen(300, 100);
372
+ await expect(page).toHaveURL(/PVTI_lADOABCD1234zgTRI00692/, {
373
+ timeout: 3000,
374
+ });
375
+
376
+ await swipeDetailScreen(100, 300);
377
+ await expect(page).toHaveURL(/PVTI_lADOABCD1234zgTRI00778/, {
378
+ timeout: 3000,
379
+ });
380
+
381
+ await expect(page.locator('.console-composer-input')).toHaveValue(
382
+ 'work in progress draft',
383
+ );
384
+ });
385
+
312
386
  test('renders the stories tab with non-gray stories, their open item counts, and an add-task button', async ({
313
387
  page,
314
388
  }) => {
@@ -44,3 +44,10 @@ export const ComposerUploadFailure: Story = {
44
44
  },
45
45
  },
46
46
  };
47
+
48
+ export const ComposerWithSavedDraft: Story = {
49
+ args: {
50
+ initiallyOpen: true,
51
+ initialDraft: 'Previously typed text…',
52
+ },
53
+ };
@@ -26,6 +26,53 @@ describe('ConsoleCommentComposer', () => {
26
26
  expect(getByPlaceholderText('Leave a comment…')).toBeInTheDocument();
27
27
  });
28
28
 
29
+ it('initializes the textarea with initialDraft when provided', () => {
30
+ const { getByPlaceholderText } = render(
31
+ <ConsoleCommentComposer
32
+ initiallyOpen
33
+ initialDraft="Previously typed text"
34
+ onSubmit={stubSubmit}
35
+ />,
36
+ );
37
+ expect(getByPlaceholderText('Leave a comment…')).toHaveValue(
38
+ 'Previously typed text',
39
+ );
40
+ });
41
+
42
+ it('calls onDraftChange with every keystroke', () => {
43
+ const onDraftChange = jest.fn();
44
+ const { getByPlaceholderText } = render(
45
+ <ConsoleCommentComposer
46
+ initiallyOpen
47
+ onSubmit={stubSubmit}
48
+ onDraftChange={onDraftChange}
49
+ />,
50
+ );
51
+ fireEvent.change(getByPlaceholderText('Leave a comment…'), {
52
+ target: { value: 'hello' },
53
+ });
54
+ expect(onDraftChange).toHaveBeenCalledWith('hello');
55
+ });
56
+
57
+ it('calls onDraftChange with empty string after successful submission', async () => {
58
+ const onDraftChange = jest.fn();
59
+ const { getByPlaceholderText, getByText } = render(
60
+ <ConsoleCommentComposer
61
+ initiallyOpen
62
+ onSubmit={stubSubmit}
63
+ onDraftChange={onDraftChange}
64
+ />,
65
+ );
66
+ fireEvent.change(getByPlaceholderText('Leave a comment…'), {
67
+ target: { value: 'My comment' },
68
+ });
69
+ fireEvent.click(getByText('Comment'));
70
+ await waitFor(() => {
71
+ expect(getByPlaceholderText('Leave a comment…')).toHaveValue('');
72
+ });
73
+ expect(onDraftChange).toHaveBeenCalledWith('');
74
+ });
75
+
29
76
  it('offers the opening control and hides the form when it starts closed', () => {
30
77
  const { queryByPlaceholderText, getByText } = render(
31
78
  <ConsoleCommentComposer initiallyOpen={false} onSubmit={stubSubmit} />,
@@ -1,9 +1,11 @@
1
- import { useRef, useState } from 'react';
1
+ import { useEffect, useRef, useState } from 'react';
2
2
  import type { ConsoleComment } from '../../logic/types';
3
3
 
4
4
  export type ConsoleCommentComposerProps = {
5
5
  initiallyOpen: boolean;
6
+ initialDraft?: string;
6
7
  onSubmit: (body: string) => Promise<ConsoleComment>;
8
+ onDraftChange?: (draft: string) => void;
7
9
  onUploadFile?: (file: File) => Promise<string>;
8
10
  };
9
11
 
@@ -73,11 +75,16 @@ export const removePlaceholder = (draft: string, fileName: string): string => {
73
75
 
74
76
  export const ConsoleCommentComposer = ({
75
77
  initiallyOpen,
78
+ initialDraft,
76
79
  onSubmit,
80
+ onDraftChange,
77
81
  onUploadFile,
78
82
  }: ConsoleCommentComposerProps) => {
79
83
  const [open, setOpen] = useState<boolean>(initiallyOpen);
80
- const [draft, setDraft] = useState<string>('');
84
+ const [draft, setDraft] = useState<string>(initialDraft ?? '');
85
+ useEffect(() => {
86
+ onDraftChange?.(draft);
87
+ }, [draft, onDraftChange]);
81
88
  const [status, setStatus] = useState<ComposerStatus>({ kind: 'idle' });
82
89
  const [uploadStatus, setUploadStatus] = useState<UploadStatus>({
83
90
  kind: 'idle',
@@ -42,6 +42,8 @@ export type ConsoleItemDetailContainerProps = {
42
42
  storyName: string | null;
43
43
  overlayStatus: ConsoleOverlayStatus | null;
44
44
  now: number;
45
+ initialCommentDraft?: string;
46
+ onCommentDraftChange?: (draft: string) => void;
45
47
  onQueueAction: (input: ConsoleQueueActionInput) => void;
46
48
  };
47
49
 
@@ -56,6 +58,8 @@ export const ConsoleItemDetailContainer = ({
56
58
  storyName,
57
59
  overlayStatus,
58
60
  now,
61
+ initialCommentDraft,
62
+ onCommentDraftChange,
59
63
  onQueueAction,
60
64
  }: ConsoleItemDetailContainerProps) => {
61
65
  const detail = useConsoleItemDetailData(caches, item);
@@ -198,7 +202,9 @@ export const ConsoleItemDetailContainer = ({
198
202
  commentComposer={
199
203
  <ConsoleCommentComposer
200
204
  initiallyOpen
205
+ initialDraft={initialCommentDraft}
201
206
  onSubmit={addComment}
207
+ onDraftChange={onCommentDraftChange}
202
208
  onUploadFile={(file) => operations.uploadAttachment(item, file)}
203
209
  />
204
210
  }
@@ -837,6 +837,68 @@ describe('ConsolePage comment composer isolation', () => {
837
837
  });
838
838
  });
839
839
 
840
+ describe('ConsolePage draft preservation', () => {
841
+ beforeEach(() => {
842
+ localStorage.clear();
843
+ window.history.replaceState({}, '', '/projects/acme/prs?k=token');
844
+ const fetchMock = jest.fn(async (url: string, init?: RequestInit) => {
845
+ const listMatch = url.match(/\/projects\/[^/]+\/([^/]+)\/list\.json/);
846
+ if (listMatch !== null) {
847
+ return {
848
+ ok: true,
849
+ status: 200,
850
+ json: async () =>
851
+ listMatch[1] === 'prs'
852
+ ? twoItemPrPayload()
853
+ : { ...twoItemPrPayload(), items: [] },
854
+ };
855
+ }
856
+ void init;
857
+ return { ok: true, status: 200, json: async () => ({ body: '# body' }) };
858
+ });
859
+ global.fetch = fetchMock as unknown as typeof fetch;
860
+ });
861
+
862
+ it('restores the typed text when returning to a task after navigating away', async () => {
863
+ const { container, getByText, findByText, getByPlaceholderText } = render(
864
+ <ConsolePage />,
865
+ );
866
+ await waitFor(() => {
867
+ expect(getByText('Add serveConsole subcommand')).toBeInTheDocument();
868
+ });
869
+
870
+ fireEvent.click(getByText('Add serveConsole subcommand'));
871
+ expect(await findByText('Approve & Merge')).toBeInTheDocument();
872
+
873
+ fireEvent.change(getByPlaceholderText('Leave a comment…'), {
874
+ target: { value: 'work in progress' },
875
+ });
876
+
877
+ const detailScreen = container.querySelector('.console-detail-screen');
878
+ expect(detailScreen).not.toBeNull();
879
+
880
+ swipeDetailScreen(
881
+ detailScreen as HTMLElement,
882
+ { clientX: 240, clientY: 100 },
883
+ { clientX: 40, clientY: 110 },
884
+ );
885
+ await waitFor(() => {
886
+ expect(window.location.hash).toBe('#item/PVTI_2');
887
+ });
888
+
889
+ swipeDetailScreen(
890
+ detailScreen as HTMLElement,
891
+ { clientX: 40, clientY: 100 },
892
+ { clientX: 240, clientY: 110 },
893
+ );
894
+ await waitFor(() => {
895
+ expect(getByPlaceholderText('Leave a comment…')).toHaveValue(
896
+ 'work in progress',
897
+ );
898
+ });
899
+ });
900
+ });
901
+
840
902
  describe('ConsolePage auto-advance tab', () => {
841
903
  beforeEach(() => {
842
904
  localStorage.clear();
@@ -112,6 +112,19 @@ export const ConsolePage = () => {
112
112
  const { activeTab, selectedItemKey, openItem, closeItem, selectTab } =
113
113
  navigation;
114
114
 
115
+ const commentDrafts = useRef(new Map<string, string>());
116
+ const handleCommentDraftChange = useCallback(
117
+ (draft: string) => {
118
+ if (selectedItemKey === null) return;
119
+ if (draft) {
120
+ commentDrafts.current.set(selectedItemKey, draft);
121
+ } else {
122
+ commentDrafts.current.delete(selectedItemKey);
123
+ }
124
+ },
125
+ [selectedItemKey],
126
+ );
127
+
115
128
  const caches = useConsoleCaches();
116
129
  const operations = useConsoleOperations(
117
130
  pjcode,
@@ -441,6 +454,10 @@ export const ConsolePage = () => {
441
454
  storyName={storyNameForSelected}
442
455
  overlayStatus={overlayStatusForSelected}
443
456
  now={now}
457
+ initialCommentDraft={
458
+ commentDrafts.current.get(selectedItem.projectItemId) ?? ''
459
+ }
460
+ onCommentDraftChange={handleCommentDraftChange}
444
461
  onQueueAction={handleQueueAction}
445
462
  />
446
463
  </div>