github-issue-tower-defence-management 1.182.0 → 1.184.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 +20 -0
- package/README.md +4 -4
- package/bin/adapter/entry-points/cli/index.js +1 -2
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/cli/projectConfig.js +3 -3
- package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleGithubTokenResolver.js +20 -6
- package/bin/adapter/entry-points/console/consoleGithubTokenResolver.js.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-CMrnEYO1.js +86 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +1 -0
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/consoleListsWriter.js +1 -0
- package/bin/adapter/entry-points/handlers/consoleListsWriter.js.map +1 -1
- package/bin/domain/usecases/console/GenerateConsoleListsUseCase.js +5 -1
- package/bin/domain/usecases/console/GenerateConsoleListsUseCase.js.map +1 -1
- package/eslint.config.mjs +9 -0
- package/package.json +1 -1
- package/src/adapter/ci/workflowRunCancellation.test.ts +0 -46
- package/src/adapter/entry-points/cli/index.ts +2 -3
- package/src/adapter/entry-points/cli/projectConfig.test.ts +58 -0
- package/src/adapter/entry-points/cli/projectConfig.ts +6 -6
- package/src/adapter/entry-points/console/consoleGithubTokenResolver.test.ts +77 -28
- package/src/adapter/entry-points/console/consoleGithubTokenResolver.ts +25 -6
- package/src/adapter/entry-points/console/ui/e2e/consoleScenario.spec.ts +83 -0
- package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +3 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentComposer.stories.tsx +7 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentComposer.test.tsx +47 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentComposer.tsx +9 -2
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.stories.tsx +23 -2
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.test.tsx +60 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.tsx +25 -9
- package/src/adapter/entry-points/console/ui/src/features/console/logic/types.ts +1 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +6 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.test.tsx +62 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +17 -0
- package/src/adapter/entry-points/console/ui-dist/assets/index-CMrnEYO1.js +86 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +1 -0
- package/src/adapter/entry-points/handlers/consoleListsWriter.ts +2 -0
- package/src/domain/usecases/StartPreparationUseCase.test.ts +2 -2
- package/src/domain/usecases/console/GenerateConsoleListsUseCase.test.ts +53 -0
- package/src/domain/usecases/console/GenerateConsoleListsUseCase.ts +7 -0
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/cli/projectConfig.d.ts +1 -1
- package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleGithubTokenResolver.d.ts +1 -1
- package/types/adapter/entry-points/console/consoleGithubTokenResolver.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/consoleListsWriter.d.ts +1 -0
- package/types/adapter/entry-points/handlers/consoleListsWriter.d.ts.map +1 -1
- package/types/domain/usecases/console/GenerateConsoleListsUseCase.d.ts +2 -0
- package/types/domain/usecases/console/GenerateConsoleListsUseCase.d.ts.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-y4IbtMEJ.js +0 -86
- package/src/adapter/entry-points/console/ui-dist/assets/index-y4IbtMEJ.js +0 -86
|
@@ -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
|
}) => {
|
|
@@ -325,10 +399,19 @@ test('renders the stories tab with non-gray stories, their open item counts, and
|
|
|
325
399
|
tdpmRow.locator('.console-op-button', { hasText: 'Add task' }),
|
|
326
400
|
).toBeVisible();
|
|
327
401
|
|
|
402
|
+
const tdpmLink = tdpmRow.locator('a.console-storytag');
|
|
403
|
+
await expect(tdpmLink).toBeVisible();
|
|
404
|
+
await expect(tdpmLink).toHaveAttribute(
|
|
405
|
+
'href',
|
|
406
|
+
'https://github.com/orgs/HiromiShikata/projects/6/views/1?sliceBy%5Bvalue%5D=TDPM%20Console%20port',
|
|
407
|
+
);
|
|
408
|
+
|
|
328
409
|
const publishRow = page.locator('.console-story-list-row', {
|
|
329
410
|
hasText: 'Publish product documentation site',
|
|
330
411
|
});
|
|
331
412
|
await expect(publishRow.locator('.console-story-count')).toHaveText('1');
|
|
413
|
+
await expect(publishRow.locator('a.console-storytag')).toHaveCount(0);
|
|
414
|
+
await expect(publishRow.locator('span.console-storytag')).toBeVisible();
|
|
332
415
|
|
|
333
416
|
await expect(
|
|
334
417
|
page.locator('.console-story-list-row', {
|
|
@@ -282,12 +282,15 @@ const CONSOLE_E2E_STORIES_SNAPSHOT = {
|
|
|
282
282
|
storyOptionId: '1491051e',
|
|
283
283
|
color: 'BLUE',
|
|
284
284
|
openItemCount: 4,
|
|
285
|
+
storyViewUrl:
|
|
286
|
+
'https://github.com/orgs/HiromiShikata/projects/6/views/1?sliceBy%5Bvalue%5D=TDPM%20Console%20port',
|
|
285
287
|
},
|
|
286
288
|
{
|
|
287
289
|
storyName: 'Publish product documentation site',
|
|
288
290
|
storyOptionId: 'f7cd5cbc',
|
|
289
291
|
color: 'GREEN',
|
|
290
292
|
openItemCount: 1,
|
|
293
|
+
storyViewUrl: null,
|
|
291
294
|
},
|
|
292
295
|
{
|
|
293
296
|
storyName: 'regular / WORKFLOW BLOCKER',
|
|
@@ -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',
|
|
@@ -1,39 +1,52 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
2
|
import { ConsoleStoryList } from './ConsoleStoryList';
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const storiesWithoutUrl = [
|
|
5
5
|
{
|
|
6
6
|
storyName: 'TDPM Console port',
|
|
7
7
|
storyOptionId: '1491051e',
|
|
8
8
|
color: 'BLUE' as const,
|
|
9
9
|
openItemCount: 12,
|
|
10
|
+
storyViewUrl: null,
|
|
10
11
|
},
|
|
11
12
|
{
|
|
12
13
|
storyName: 'Publish product documentation site',
|
|
13
14
|
storyOptionId: 'f7cd5cbc',
|
|
14
15
|
color: 'GREEN' as const,
|
|
15
16
|
openItemCount: 3,
|
|
17
|
+
storyViewUrl: null,
|
|
16
18
|
},
|
|
17
19
|
{
|
|
18
20
|
storyName: 'Move to Okinawa',
|
|
19
21
|
storyOptionId: '564803ee',
|
|
20
22
|
color: 'PURPLE' as const,
|
|
21
23
|
openItemCount: 0,
|
|
24
|
+
storyViewUrl: null,
|
|
22
25
|
},
|
|
23
26
|
{
|
|
24
27
|
storyName: 'regular / regular payment invoice tax',
|
|
25
28
|
storyOptionId: 'd7cdcb61',
|
|
26
29
|
color: 'YELLOW' as const,
|
|
27
30
|
openItemCount: 7,
|
|
31
|
+
storyViewUrl: null,
|
|
28
32
|
},
|
|
29
33
|
{
|
|
30
34
|
storyName: 'regular / WORKFLOW BLOCKER',
|
|
31
35
|
storyOptionId: 'e35b3da2',
|
|
32
36
|
color: 'RED' as const,
|
|
33
37
|
openItemCount: 2,
|
|
38
|
+
storyViewUrl: null,
|
|
34
39
|
},
|
|
35
40
|
];
|
|
36
41
|
|
|
42
|
+
const BASE_VIEW_URL =
|
|
43
|
+
'https://github.com/orgs/HiromiShikata/projects/6/views/1';
|
|
44
|
+
|
|
45
|
+
const storiesWithUrl = storiesWithoutUrl.map((s) => ({
|
|
46
|
+
...s,
|
|
47
|
+
storyViewUrl: `${BASE_VIEW_URL}?sliceBy%5Bvalue%5D=${encodeURI(s.storyName).replace(/#/g, '%23').replace(/&/g, '%26')}`,
|
|
48
|
+
}));
|
|
49
|
+
|
|
37
50
|
const meta: Meta<typeof ConsoleStoryList> = {
|
|
38
51
|
title: 'Console/ConsoleStoryList',
|
|
39
52
|
component: ConsoleStoryList,
|
|
@@ -46,7 +59,15 @@ type Story = StoryObj<typeof ConsoleStoryList>;
|
|
|
46
59
|
|
|
47
60
|
export const WithStories: Story = {
|
|
48
61
|
args: {
|
|
49
|
-
stories,
|
|
62
|
+
stories: storiesWithoutUrl,
|
|
63
|
+
isLoading: false,
|
|
64
|
+
error: null,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const WithStoriesLinked: Story = {
|
|
69
|
+
args: {
|
|
70
|
+
stories: storiesWithUrl,
|
|
50
71
|
isLoading: false,
|
|
51
72
|
error: null,
|
|
52
73
|
},
|
|
@@ -8,12 +8,14 @@ const storyEntries: ConsoleStoryEntry[] = [
|
|
|
8
8
|
storyOptionId: '1491051e',
|
|
9
9
|
color: 'BLUE',
|
|
10
10
|
openItemCount: 12,
|
|
11
|
+
storyViewUrl: null,
|
|
11
12
|
},
|
|
12
13
|
{
|
|
13
14
|
storyName: 'Move to Okinawa',
|
|
14
15
|
storyOptionId: '564803ee',
|
|
15
16
|
color: 'PURPLE',
|
|
16
17
|
openItemCount: 0,
|
|
18
|
+
storyViewUrl: null,
|
|
17
19
|
},
|
|
18
20
|
];
|
|
19
21
|
|
|
@@ -193,4 +195,62 @@ describe('ConsoleStoryList', () => {
|
|
|
193
195
|
expect(getByRole('alert')).toBeInTheDocument();
|
|
194
196
|
expect(getByText(/HTTP 503/)).toBeInTheDocument();
|
|
195
197
|
});
|
|
198
|
+
|
|
199
|
+
it('renders story name as a span when storyViewUrl is absent (old JSON format)', () => {
|
|
200
|
+
const oldFormatEntry = {
|
|
201
|
+
storyName: 'TDPM Console port',
|
|
202
|
+
storyOptionId: '1491051e',
|
|
203
|
+
color: 'BLUE',
|
|
204
|
+
openItemCount: 4,
|
|
205
|
+
} as unknown as ConsoleStoryEntry;
|
|
206
|
+
const { getByText } = render(
|
|
207
|
+
<ConsoleStoryList
|
|
208
|
+
stories={[oldFormatEntry]}
|
|
209
|
+
isLoading={false}
|
|
210
|
+
error={null}
|
|
211
|
+
onCreateIssue={() => Promise.resolve()}
|
|
212
|
+
/>,
|
|
213
|
+
);
|
|
214
|
+
const nameEl = getByText('TDPM Console port');
|
|
215
|
+
expect(nameEl.tagName).toBe('SPAN');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('renders story name as a span when storyViewUrl is null', () => {
|
|
219
|
+
const { getByText } = render(
|
|
220
|
+
<ConsoleStoryList
|
|
221
|
+
stories={storyEntries}
|
|
222
|
+
isLoading={false}
|
|
223
|
+
error={null}
|
|
224
|
+
onCreateIssue={() => Promise.resolve()}
|
|
225
|
+
/>,
|
|
226
|
+
);
|
|
227
|
+
const nameEl = getByText('TDPM Console port');
|
|
228
|
+
expect(nameEl.tagName).toBe('SPAN');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('renders story name as an anchor when storyViewUrl is non-null', () => {
|
|
232
|
+
const entriesWithUrl: ConsoleStoryEntry[] = [
|
|
233
|
+
{
|
|
234
|
+
...storyEntries[0],
|
|
235
|
+
storyViewUrl:
|
|
236
|
+
'https://github.com/orgs/demo/projects/1/views/1?sliceBy%5Bvalue%5D=TDPM%20Console%20port',
|
|
237
|
+
},
|
|
238
|
+
];
|
|
239
|
+
const { getByText } = render(
|
|
240
|
+
<ConsoleStoryList
|
|
241
|
+
stories={entriesWithUrl}
|
|
242
|
+
isLoading={false}
|
|
243
|
+
error={null}
|
|
244
|
+
onCreateIssue={() => Promise.resolve()}
|
|
245
|
+
/>,
|
|
246
|
+
);
|
|
247
|
+
const nameEl = getByText('TDPM Console port');
|
|
248
|
+
expect(nameEl.tagName).toBe('A');
|
|
249
|
+
expect(nameEl).toHaveAttribute(
|
|
250
|
+
'href',
|
|
251
|
+
'https://github.com/orgs/demo/projects/1/views/1?sliceBy%5Bvalue%5D=TDPM%20Console%20port',
|
|
252
|
+
);
|
|
253
|
+
expect(nameEl).toHaveAttribute('target', '_blank');
|
|
254
|
+
expect(nameEl).toHaveAttribute('rel', 'noopener noreferrer');
|
|
255
|
+
});
|
|
196
256
|
});
|
|
@@ -129,16 +129,32 @@ export const ConsoleStoryList = ({
|
|
|
129
129
|
return (
|
|
130
130
|
<li key={entry.storyOptionId} className="console-story-list-row">
|
|
131
131
|
<div className="console-story-list-row-main">
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
{entry.storyViewUrl ? (
|
|
133
|
+
<a
|
|
134
|
+
href={entry.storyViewUrl}
|
|
135
|
+
target="_blank"
|
|
136
|
+
rel="noopener noreferrer"
|
|
137
|
+
className="console-storytag"
|
|
138
|
+
style={{ color: palette.fg, borderColor: palette.border }}
|
|
139
|
+
>
|
|
140
|
+
<span
|
|
141
|
+
className="console-story-dot"
|
|
142
|
+
style={{ backgroundColor: palette.dot }}
|
|
143
|
+
/>
|
|
144
|
+
{entry.storyName}
|
|
145
|
+
</a>
|
|
146
|
+
) : (
|
|
136
147
|
<span
|
|
137
|
-
className="console-
|
|
138
|
-
style={{
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
148
|
+
className="console-storytag"
|
|
149
|
+
style={{ color: palette.fg, borderColor: palette.border }}
|
|
150
|
+
>
|
|
151
|
+
<span
|
|
152
|
+
className="console-story-dot"
|
|
153
|
+
style={{ backgroundColor: palette.dot }}
|
|
154
|
+
/>
|
|
155
|
+
{entry.storyName}
|
|
156
|
+
</span>
|
|
157
|
+
)}
|
|
142
158
|
<span className="console-story-count">{entry.openItemCount}</span>
|
|
143
159
|
<button
|
|
144
160
|
type="button"
|
|
@@ -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>
|