github-issue-tower-defence-management 1.151.1 → 1.151.3
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 +15 -0
- package/bin/adapter/entry-points/console/consoleDoneStore.js +19 -1
- package/bin/adapter/entry-points/console/consoleDoneStore.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleOperationApi.js +17 -5
- package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/{index-Dihcph6j.css → index-Co7u5cAQ.css} +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-OB0UwU6U.js +83 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapter/entry-points/console/consoleDoneStore.test.ts +21 -0
- package/src/adapter/entry-points/console/consoleDoneStore.ts +25 -0
- package/src/adapter/entry-points/console/consoleOperationApi.test.ts +58 -7
- package/src/adapter/entry-points/console/consoleOperationApi.ts +43 -6
- package/src/adapter/entry-points/console/ui/e2e/consoleScenario.spec.ts +22 -0
- package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +10 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentComposer.stories.tsx +8 -8
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentComposer.test.tsx +29 -11
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentComposer.tsx +4 -4
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.test.tsx +10 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.tsx +4 -3
- package/src/adapter/entry-points/console/ui/src/features/console/logic/overlay.test.ts +79 -1
- package/src/adapter/entry-points/console/ui/src/features/console/logic/overlay.ts +20 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +1 -1
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.test.tsx +42 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +6 -2
- package/src/adapter/entry-points/console/ui/src/index.css +11 -1
- package/src/adapter/entry-points/console/ui-dist/assets/{index-Dihcph6j.css → index-Co7u5cAQ.css} +1 -1
- package/src/adapter/entry-points/console/ui-dist/assets/index-OB0UwU6U.js +83 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/types/adapter/entry-points/console/consoleDoneStore.d.ts +3 -0
- package/types/adapter/entry-points/console/consoleDoneStore.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-UQt5L756.js +0 -83
- package/src/adapter/entry-points/console/ui-dist/assets/index-UQt5L756.js +0 -83
|
@@ -4,8 +4,8 @@
|
|
|
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-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-OB0UwU6U.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Co7u5cAQ.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -3,6 +3,8 @@ import * as os from 'os';
|
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import {
|
|
5
5
|
CONSOLE_DONE_FILE_NAME,
|
|
6
|
+
CONSOLE_DONE_STATUS_SELECTED_TAB_NAMES,
|
|
7
|
+
CONSOLE_DONE_STORY_SELECTED_TAB_NAMES,
|
|
6
8
|
CONSOLE_DONE_TAB_NAMES,
|
|
7
9
|
doneFilePathForTab,
|
|
8
10
|
readDoneProjectItemIds,
|
|
@@ -145,4 +147,23 @@ describe('consoleDoneStore', () => {
|
|
|
145
147
|
}
|
|
146
148
|
});
|
|
147
149
|
});
|
|
150
|
+
|
|
151
|
+
describe('selector partitions of the console done tabs', () => {
|
|
152
|
+
it('covers every console done tab exactly once across the status and story partitions', () => {
|
|
153
|
+
expect(
|
|
154
|
+
[
|
|
155
|
+
...CONSOLE_DONE_STATUS_SELECTED_TAB_NAMES,
|
|
156
|
+
...CONSOLE_DONE_STORY_SELECTED_TAB_NAMES,
|
|
157
|
+
].sort(),
|
|
158
|
+
).toEqual([...CONSOLE_DONE_TAB_NAMES].sort());
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('places no console done tab in both the status and the story partition', () => {
|
|
162
|
+
expect(
|
|
163
|
+
CONSOLE_DONE_STATUS_SELECTED_TAB_NAMES.filter((tab) =>
|
|
164
|
+
CONSOLE_DONE_STORY_SELECTED_TAB_NAMES.includes(tab),
|
|
165
|
+
),
|
|
166
|
+
).toEqual([]);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
148
169
|
});
|
|
@@ -89,6 +89,31 @@ export const CONSOLE_DONE_TAB_NAMES: string[] = [
|
|
|
89
89
|
'in-tmux-by-human',
|
|
90
90
|
];
|
|
91
91
|
|
|
92
|
+
export const CONSOLE_DONE_STATUS_SELECTED_TAB_NAMES: string[] = [
|
|
93
|
+
'prs',
|
|
94
|
+
'unread',
|
|
95
|
+
'failed-preparation',
|
|
96
|
+
'todo-by-human',
|
|
97
|
+
'todo-by-agent',
|
|
98
|
+
'in-tmux-by-human',
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
export const CONSOLE_DONE_STORY_SELECTED_TAB_NAMES: string[] = [
|
|
102
|
+
'workflow-blocker',
|
|
103
|
+
'triage',
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
export const recordDoneProjectItemIdForTabs = (
|
|
107
|
+
consoleDataOutputDir: string,
|
|
108
|
+
pjcode: string,
|
|
109
|
+
projectItemId: string,
|
|
110
|
+
tabNames: string[],
|
|
111
|
+
): void => {
|
|
112
|
+
for (const tab of tabNames) {
|
|
113
|
+
recordDoneProjectItemId(consoleDataOutputDir, pjcode, tab, projectItemId);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
92
117
|
export const recordDoneProjectItemIdAcrossTabs = (
|
|
93
118
|
consoleDataOutputDir: string,
|
|
94
119
|
pjcode: string,
|
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
import {
|
|
19
19
|
CONSOLE_DONE_TAB_NAMES,
|
|
20
20
|
readDoneProjectItemIds,
|
|
21
|
+
CONSOLE_DONE_STATUS_SELECTED_TAB_NAMES,
|
|
22
|
+
CONSOLE_DONE_STORY_SELECTED_TAB_NAMES,
|
|
21
23
|
} from './consoleDoneStore';
|
|
22
24
|
|
|
23
25
|
describe('consoleOperationApi', () => {
|
|
@@ -104,6 +106,23 @@ describe('consoleOperationApi', () => {
|
|
|
104
106
|
}
|
|
105
107
|
};
|
|
106
108
|
|
|
109
|
+
const expectRecordedOnlyIn = (
|
|
110
|
+
projectItemId: string,
|
|
111
|
+
recordedTabs: string[],
|
|
112
|
+
): void => {
|
|
113
|
+
for (const tab of CONSOLE_DONE_TAB_NAMES) {
|
|
114
|
+
if (recordedTabs.includes(tab)) {
|
|
115
|
+
expect(readDoneProjectItemIds(baseDir, 'acme', tab)).toContain(
|
|
116
|
+
projectItemId,
|
|
117
|
+
);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
expect(readDoneProjectItemIds(baseDir, 'acme', tab)).not.toContain(
|
|
121
|
+
projectItemId,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
107
126
|
describe('handleReview', () => {
|
|
108
127
|
it('approves and sets Awaiting workspace then records done', async () => {
|
|
109
128
|
const response = await handleReview(context, {
|
|
@@ -125,7 +144,7 @@ describe('consoleOperationApi', () => {
|
|
|
125
144
|
'status_aw',
|
|
126
145
|
);
|
|
127
146
|
expect(issueRepository.get).not.toHaveBeenCalled();
|
|
128
|
-
|
|
147
|
+
expectRecordedOnlyIn('PVTI_a', CONSOLE_DONE_STATUS_SELECTED_TAB_NAMES);
|
|
129
148
|
});
|
|
130
149
|
|
|
131
150
|
it('resolves the issue repository from the url of the operated pull request', async () => {
|
|
@@ -173,7 +192,7 @@ describe('consoleOperationApi', () => {
|
|
|
173
192
|
side: 'RIGHT',
|
|
174
193
|
},
|
|
175
194
|
);
|
|
176
|
-
|
|
195
|
+
expectRecordedOnlyIn('PVTI_b', CONSOLE_DONE_STATUS_SELECTED_TAB_NAMES);
|
|
177
196
|
});
|
|
178
197
|
|
|
179
198
|
it('requests changes without a line anchor when line and side are missing', async () => {
|
|
@@ -194,7 +213,7 @@ describe('consoleOperationApi', () => {
|
|
|
194
213
|
'please fix',
|
|
195
214
|
null,
|
|
196
215
|
);
|
|
197
|
-
|
|
216
|
+
expectRecordedOnlyIn('PVTI_b', CONSOLE_DONE_STATUS_SELECTED_TAB_NAMES);
|
|
198
217
|
});
|
|
199
218
|
|
|
200
219
|
it('rejects request_changes without a comment body', async () => {
|
|
@@ -430,7 +449,37 @@ describe('consoleOperationApi', () => {
|
|
|
430
449
|
'status_todo',
|
|
431
450
|
);
|
|
432
451
|
expect(issueRepository.get).not.toHaveBeenCalled();
|
|
433
|
-
|
|
452
|
+
expectRecordedOnlyIn('PVTI_d', CONSOLE_DONE_STATUS_SELECTED_TAB_NAMES);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('keeps a status change out of the story-selected tabs', async () => {
|
|
456
|
+
const response = await handleTriage(context, {
|
|
457
|
+
pjcode: 'acme',
|
|
458
|
+
action: 'set_status',
|
|
459
|
+
issueUrl: 'https://github.com/o/r/issues/1',
|
|
460
|
+
projectItemId: 'PVTI_status_only',
|
|
461
|
+
statusName: 'Todo',
|
|
462
|
+
});
|
|
463
|
+
expect(response.statusCode).toBe(200);
|
|
464
|
+
expectRecordedOnlyIn(
|
|
465
|
+
'PVTI_status_only',
|
|
466
|
+
CONSOLE_DONE_STATUS_SELECTED_TAB_NAMES,
|
|
467
|
+
);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('keeps a story change out of the status-selected tabs', async () => {
|
|
471
|
+
const response = await handleTriage(context, {
|
|
472
|
+
pjcode: 'acme',
|
|
473
|
+
action: 'set_story',
|
|
474
|
+
issueUrl: 'https://github.com/o/r/issues/1',
|
|
475
|
+
projectItemId: 'PVTI_story_only',
|
|
476
|
+
storyOptionId: 'story_opt_1',
|
|
477
|
+
});
|
|
478
|
+
expect(response.statusCode).toBe(200);
|
|
479
|
+
expectRecordedOnlyIn(
|
|
480
|
+
'PVTI_story_only',
|
|
481
|
+
CONSOLE_DONE_STORY_SELECTED_TAB_NAMES,
|
|
482
|
+
);
|
|
434
483
|
});
|
|
435
484
|
|
|
436
485
|
it('rejects an unknown status name', async () => {
|
|
@@ -460,7 +509,7 @@ describe('consoleOperationApi', () => {
|
|
|
460
509
|
'story_opt_1',
|
|
461
510
|
);
|
|
462
511
|
expect(issueRepository.get).not.toHaveBeenCalled();
|
|
463
|
-
|
|
512
|
+
expectRecordedOnlyIn('PVTI_e', CONSOLE_DONE_STORY_SELECTED_TAB_NAMES);
|
|
464
513
|
});
|
|
465
514
|
|
|
466
515
|
it('snoozes for one day via updateNextActionDate', async () => {
|
|
@@ -794,7 +843,7 @@ describe('consoleOperationApi', () => {
|
|
|
794
843
|
'status_intmux',
|
|
795
844
|
);
|
|
796
845
|
expect(issueRepository.get).not.toHaveBeenCalled();
|
|
797
|
-
|
|
846
|
+
expectRecordedOnlyIn('PVTI_i', CONSOLE_DONE_STATUS_SELECTED_TAB_NAMES);
|
|
798
847
|
});
|
|
799
848
|
|
|
800
849
|
it('rejects an unknown intmux action', async () => {
|
|
@@ -907,10 +956,12 @@ describe('consoleOperationApi', () => {
|
|
|
907
956
|
expect.objectContaining({ itemId: 'PVTI_x' }),
|
|
908
957
|
'status_todo',
|
|
909
958
|
);
|
|
910
|
-
for (const tab of
|
|
959
|
+
for (const tab of CONSOLE_DONE_STATUS_SELECTED_TAB_NAMES) {
|
|
911
960
|
expect(readDoneProjectItemIds(baseDir, 'globex', tab)).toContain(
|
|
912
961
|
'PVTI_x',
|
|
913
962
|
);
|
|
963
|
+
}
|
|
964
|
+
for (const tab of CONSOLE_DONE_TAB_NAMES) {
|
|
914
965
|
expect(readDoneProjectItemIds(baseDir, 'acme', tab)).not.toContain(
|
|
915
966
|
'PVTI_x',
|
|
916
967
|
);
|
|
@@ -5,7 +5,12 @@ import {
|
|
|
5
5
|
import { IssueAttachmentRepository } from '../../../domain/usecases/adapter-interfaces/IssueAttachmentRepository';
|
|
6
6
|
import { Project } from '../../../domain/entities/Project';
|
|
7
7
|
import { Issue } from '../../../domain/entities/Issue';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
recordDoneProjectItemIdAcrossTabs,
|
|
10
|
+
recordDoneProjectItemIdForTabs,
|
|
11
|
+
CONSOLE_DONE_STATUS_SELECTED_TAB_NAMES,
|
|
12
|
+
CONSOLE_DONE_STORY_SELECTED_TAB_NAMES,
|
|
13
|
+
} from './consoleDoneStore';
|
|
9
14
|
import { findConsoleItemUrl } from './consoleItemUrlLookup';
|
|
10
15
|
|
|
11
16
|
export const AWAITING_WORKSPACE_STATUS_NAME = 'awaiting workspace';
|
|
@@ -135,6 +140,38 @@ const recordDone = (
|
|
|
135
140
|
);
|
|
136
141
|
};
|
|
137
142
|
|
|
143
|
+
const recordDoneForStatusChange = (
|
|
144
|
+
context: ConsoleOperationContext,
|
|
145
|
+
pjcode: string,
|
|
146
|
+
projectItemId: string,
|
|
147
|
+
): void => {
|
|
148
|
+
if (context.consoleDataOutputDir === null) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
recordDoneProjectItemIdForTabs(
|
|
152
|
+
context.consoleDataOutputDir,
|
|
153
|
+
pjcode,
|
|
154
|
+
projectItemId,
|
|
155
|
+
CONSOLE_DONE_STATUS_SELECTED_TAB_NAMES,
|
|
156
|
+
);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const recordDoneForStoryChange = (
|
|
160
|
+
context: ConsoleOperationContext,
|
|
161
|
+
pjcode: string,
|
|
162
|
+
projectItemId: string,
|
|
163
|
+
): void => {
|
|
164
|
+
if (context.consoleDataOutputDir === null) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
recordDoneProjectItemIdForTabs(
|
|
168
|
+
context.consoleDataOutputDir,
|
|
169
|
+
pjcode,
|
|
170
|
+
projectItemId,
|
|
171
|
+
CONSOLE_DONE_STORY_SELECTED_TAB_NAMES,
|
|
172
|
+
);
|
|
173
|
+
};
|
|
174
|
+
|
|
138
175
|
const resolveBinding = async (
|
|
139
176
|
context: ConsoleOperationContext,
|
|
140
177
|
body: Record<string, unknown>,
|
|
@@ -298,7 +335,7 @@ export const handleReview = async (
|
|
|
298
335
|
if (failure !== null) {
|
|
299
336
|
return failure;
|
|
300
337
|
}
|
|
301
|
-
|
|
338
|
+
recordDoneForStatusChange(context, pjcode, projectItemId);
|
|
302
339
|
return ok();
|
|
303
340
|
}
|
|
304
341
|
|
|
@@ -334,7 +371,7 @@ export const handleReview = async (
|
|
|
334
371
|
if (failure !== null) {
|
|
335
372
|
return failure;
|
|
336
373
|
}
|
|
337
|
-
|
|
374
|
+
recordDoneForStatusChange(context, pjcode, projectItemId);
|
|
338
375
|
return ok();
|
|
339
376
|
}
|
|
340
377
|
|
|
@@ -403,7 +440,7 @@ export const handleTriage = async (
|
|
|
403
440
|
if (failure !== null) {
|
|
404
441
|
return failure;
|
|
405
442
|
}
|
|
406
|
-
|
|
443
|
+
recordDoneForStatusChange(context, pjcode, projectItemId);
|
|
407
444
|
return ok();
|
|
408
445
|
}
|
|
409
446
|
|
|
@@ -422,7 +459,7 @@ export const handleTriage = async (
|
|
|
422
459
|
projectItemReference(issueUrl, projectItemId),
|
|
423
460
|
storyOptionId,
|
|
424
461
|
);
|
|
425
|
-
|
|
462
|
+
recordDoneForStoryChange(context, pjcode, projectItemId);
|
|
426
463
|
return ok();
|
|
427
464
|
}
|
|
428
465
|
|
|
@@ -590,6 +627,6 @@ export const handleIntmux = async (
|
|
|
590
627
|
if (failure !== null) {
|
|
591
628
|
return failure;
|
|
592
629
|
}
|
|
593
|
-
|
|
630
|
+
recordDoneForStatusChange(context, pjcode, projectItemId);
|
|
594
631
|
return ok();
|
|
595
632
|
};
|
|
@@ -274,3 +274,25 @@ test('lists a still-open item and keeps its tab visible when the browser overlay
|
|
|
274
274
|
),
|
|
275
275
|
).toBeVisible();
|
|
276
276
|
});
|
|
277
|
+
|
|
278
|
+
test('keeps the comment control on screen while the item body scrolls, and gives the height back when it is closed', async ({
|
|
279
|
+
page,
|
|
280
|
+
}) => {
|
|
281
|
+
await page.goto(harness.appRootUrl);
|
|
282
|
+
|
|
283
|
+
await itemRowByText(
|
|
284
|
+
page,
|
|
285
|
+
'Resolve the shared GitHub token rate-limit exhaustion blocker',
|
|
286
|
+
).click();
|
|
287
|
+
|
|
288
|
+
const composerToggle = page.locator('.console-composer-toggle');
|
|
289
|
+
await expect(composerToggle).toBeInViewport();
|
|
290
|
+
await expect(page.locator('.console-composer-input')).toHaveCount(0);
|
|
291
|
+
|
|
292
|
+
await composerToggle.click();
|
|
293
|
+
await expect(page.locator('.console-composer-input')).toBeInViewport();
|
|
294
|
+
|
|
295
|
+
await composerToggle.click();
|
|
296
|
+
await expect(page.locator('.console-composer-input')).toHaveCount(0);
|
|
297
|
+
await expect(composerToggle).toBeInViewport();
|
|
298
|
+
});
|
|
@@ -462,7 +462,16 @@ const createStubIssueRepository = (
|
|
|
462
462
|
addIssueToProject: () => notImplemented('addIssueToProject'),
|
|
463
463
|
setDependedIssueUrl: () => notImplemented('setDependedIssueUrl'),
|
|
464
464
|
getIssueOrPullRequestBody: async (): Promise<string> =>
|
|
465
|
-
|
|
465
|
+
[
|
|
466
|
+
'## Console E2E fixture',
|
|
467
|
+
'',
|
|
468
|
+
'This body is served by the isolated E2E stub.',
|
|
469
|
+
'',
|
|
470
|
+
...Array.from(
|
|
471
|
+
{ length: 80 },
|
|
472
|
+
(_, index) => `Description line ${index + 1} of the fixture body.`,
|
|
473
|
+
),
|
|
474
|
+
].join('\n'),
|
|
466
475
|
getIssueOrPullRequestComments: async (): Promise<IssueComment[]> => [],
|
|
467
476
|
getPullRequestDetail: async (
|
|
468
477
|
url: string,
|
|
@@ -21,25 +21,25 @@ export default meta;
|
|
|
21
21
|
|
|
22
22
|
type Story = StoryObj<typeof ConsoleCommentComposer>;
|
|
23
23
|
|
|
24
|
-
export const
|
|
25
|
-
args: {
|
|
24
|
+
export const ComposerOpen: Story = {
|
|
25
|
+
args: { initiallyOpen: true },
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
-
export const
|
|
29
|
-
args: {
|
|
28
|
+
export const ComposerCollapsed: Story = {
|
|
29
|
+
args: { initiallyOpen: false },
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
export const
|
|
32
|
+
export const ComposerWithFileUpload: Story = {
|
|
33
33
|
args: {
|
|
34
|
-
|
|
34
|
+
initiallyOpen: true,
|
|
35
35
|
onUploadFile: async (file: File) =>
|
|
36
36
|
``,
|
|
37
37
|
},
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
-
export const
|
|
40
|
+
export const ComposerUploadFailure: Story = {
|
|
41
41
|
args: {
|
|
42
|
-
|
|
42
|
+
initiallyOpen: true,
|
|
43
43
|
onUploadFile: async () => {
|
|
44
44
|
throw new Error('No GitHub web session is available');
|
|
45
45
|
},
|
|
@@ -12,10 +12,10 @@ jest.mock('../../lib/mermaidLoader', () => ({
|
|
|
12
12
|
const now = Date.parse('2026-06-19T12:00:00.000Z');
|
|
13
13
|
|
|
14
14
|
describe('ConsoleCommentComposer', () => {
|
|
15
|
-
it('
|
|
15
|
+
it('shows the form when it starts open', () => {
|
|
16
16
|
const { getByPlaceholderText } = render(
|
|
17
17
|
<ConsoleCommentComposer
|
|
18
|
-
|
|
18
|
+
initiallyOpen
|
|
19
19
|
now={now}
|
|
20
20
|
onSubmit={async (body) => ({
|
|
21
21
|
author: 'HiromiShikata',
|
|
@@ -27,10 +27,10 @@ describe('ConsoleCommentComposer', () => {
|
|
|
27
27
|
expect(getByPlaceholderText('Leave a comment…')).toBeInTheDocument();
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
it('
|
|
30
|
+
it('offers the opening control and hides the form when it starts closed', () => {
|
|
31
31
|
const { queryByPlaceholderText, getByText } = render(
|
|
32
32
|
<ConsoleCommentComposer
|
|
33
|
-
|
|
33
|
+
initiallyOpen={false}
|
|
34
34
|
now={now}
|
|
35
35
|
onSubmit={async (body) => ({
|
|
36
36
|
author: 'HiromiShikata',
|
|
@@ -43,6 +43,24 @@ describe('ConsoleCommentComposer', () => {
|
|
|
43
43
|
expect(getByText('💬 Add a comment')).toBeInTheDocument();
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
+
it('opens the form on the control and closes it again so the body regains the height', () => {
|
|
47
|
+
const { getByText, getByPlaceholderText, queryByPlaceholderText } = render(
|
|
48
|
+
<ConsoleCommentComposer
|
|
49
|
+
initiallyOpen={false}
|
|
50
|
+
now={now}
|
|
51
|
+
onSubmit={async (body) => ({
|
|
52
|
+
author: 'HiromiShikata',
|
|
53
|
+
body,
|
|
54
|
+
createdAt: '2026-06-19T11:58:00.000Z',
|
|
55
|
+
})}
|
|
56
|
+
/>,
|
|
57
|
+
);
|
|
58
|
+
fireEvent.click(getByText('💬 Add a comment'));
|
|
59
|
+
expect(getByPlaceholderText('Leave a comment…')).toBeInTheDocument();
|
|
60
|
+
fireEvent.click(getByText('✕ Close'));
|
|
61
|
+
expect(queryByPlaceholderText('Leave a comment…')).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
|
|
46
64
|
it('submits the comment and shows the posted comment', async () => {
|
|
47
65
|
const onSubmit = jest.fn(
|
|
48
66
|
async (body: string): Promise<ConsoleComment> => ({
|
|
@@ -52,7 +70,7 @@ describe('ConsoleCommentComposer', () => {
|
|
|
52
70
|
}),
|
|
53
71
|
);
|
|
54
72
|
const { getByPlaceholderText, getByText } = render(
|
|
55
|
-
<ConsoleCommentComposer
|
|
73
|
+
<ConsoleCommentComposer initiallyOpen now={now} onSubmit={onSubmit} />,
|
|
56
74
|
);
|
|
57
75
|
fireEvent.change(getByPlaceholderText('Leave a comment…'), {
|
|
58
76
|
target: { value: 'Looks good after the rebase.' },
|
|
@@ -67,7 +85,7 @@ describe('ConsoleCommentComposer', () => {
|
|
|
67
85
|
it('shows a failure message when the submission rejects', async () => {
|
|
68
86
|
const { getByPlaceholderText, getByText, findByRole } = render(
|
|
69
87
|
<ConsoleCommentComposer
|
|
70
|
-
|
|
88
|
+
initiallyOpen
|
|
71
89
|
now={now}
|
|
72
90
|
onSubmit={async () => {
|
|
73
91
|
throw new Error('HTTP 500');
|
|
@@ -85,7 +103,7 @@ describe('ConsoleCommentComposer', () => {
|
|
|
85
103
|
it('does not render the attach control when no upload handler is given', () => {
|
|
86
104
|
const { queryByText } = render(
|
|
87
105
|
<ConsoleCommentComposer
|
|
88
|
-
|
|
106
|
+
initiallyOpen
|
|
89
107
|
now={now}
|
|
90
108
|
onSubmit={async (body) => ({
|
|
91
109
|
author: 'HiromiShikata',
|
|
@@ -103,7 +121,7 @@ describe('ConsoleCommentComposer', () => {
|
|
|
103
121
|
);
|
|
104
122
|
const { getByPlaceholderText, getByLabelText } = render(
|
|
105
123
|
<ConsoleCommentComposer
|
|
106
|
-
|
|
124
|
+
initiallyOpen
|
|
107
125
|
now={now}
|
|
108
126
|
onSubmit={async (body) => ({
|
|
109
127
|
author: 'HiromiShikata',
|
|
@@ -135,7 +153,7 @@ describe('ConsoleCommentComposer', () => {
|
|
|
135
153
|
);
|
|
136
154
|
const { getByPlaceholderText } = render(
|
|
137
155
|
<ConsoleCommentComposer
|
|
138
|
-
|
|
156
|
+
initiallyOpen
|
|
139
157
|
now={now}
|
|
140
158
|
onSubmit={async (body) => ({
|
|
141
159
|
author: 'HiromiShikata',
|
|
@@ -164,7 +182,7 @@ describe('ConsoleCommentComposer', () => {
|
|
|
164
182
|
);
|
|
165
183
|
const { getByPlaceholderText } = render(
|
|
166
184
|
<ConsoleCommentComposer
|
|
167
|
-
|
|
185
|
+
initiallyOpen
|
|
168
186
|
now={now}
|
|
169
187
|
onSubmit={async (body) => ({
|
|
170
188
|
author: 'HiromiShikata',
|
|
@@ -190,7 +208,7 @@ describe('ConsoleCommentComposer', () => {
|
|
|
190
208
|
it('shows the upload failure reason when the upload rejects', async () => {
|
|
191
209
|
const { getByLabelText, findByRole } = render(
|
|
192
210
|
<ConsoleCommentComposer
|
|
193
|
-
|
|
211
|
+
initiallyOpen
|
|
194
212
|
now={now}
|
|
195
213
|
onSubmit={async (body) => ({
|
|
196
214
|
author: 'HiromiShikata',
|
|
@@ -7,7 +7,7 @@ import type { ConsoleComment } from '../../logic/types';
|
|
|
7
7
|
import { ConsoleMarkdownContent } from '../content/ConsoleMarkdownContent';
|
|
8
8
|
|
|
9
9
|
export type ConsoleCommentComposerProps = {
|
|
10
|
-
|
|
10
|
+
initiallyOpen: boolean;
|
|
11
11
|
now: number;
|
|
12
12
|
onSubmit: (body: string) => Promise<ConsoleComment>;
|
|
13
13
|
onUploadFile?: (file: File) => Promise<string>;
|
|
@@ -37,12 +37,12 @@ export const appendAttachmentMarkdown = (
|
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
export const ConsoleCommentComposer = ({
|
|
40
|
-
|
|
40
|
+
initiallyOpen,
|
|
41
41
|
now,
|
|
42
42
|
onSubmit,
|
|
43
43
|
onUploadFile,
|
|
44
44
|
}: ConsoleCommentComposerProps) => {
|
|
45
|
-
const [open, setOpen] = useState<boolean>(
|
|
45
|
+
const [open, setOpen] = useState<boolean>(initiallyOpen);
|
|
46
46
|
const [draft, setDraft] = useState<string>('');
|
|
47
47
|
const [status, setStatus] = useState<ComposerStatus>({ kind: 'idle' });
|
|
48
48
|
const [uploadStatus, setUploadStatus] = useState<UploadStatus>({
|
|
@@ -99,7 +99,7 @@ export const ConsoleCommentComposer = ({
|
|
|
99
99
|
aria-expanded={open}
|
|
100
100
|
onClick={() => setOpen((value) => !value)}
|
|
101
101
|
>
|
|
102
|
-
{open ? '✕
|
|
102
|
+
{open ? '✕ Close' : '💬 Add a comment'}
|
|
103
103
|
</button>
|
|
104
104
|
{posted.length > 0 && (
|
|
105
105
|
<div className="console-composer-posted">
|
|
@@ -44,6 +44,16 @@ const baseProps = {
|
|
|
44
44
|
};
|
|
45
45
|
|
|
46
46
|
describe('ConsoleItemDetail', () => {
|
|
47
|
+
it('docks the comment composer with the operation bar so it stays reachable while the body scrolls', () => {
|
|
48
|
+
const { container, getByText } = render(
|
|
49
|
+
<ConsoleItemDetail item={issueItem} {...baseProps} />,
|
|
50
|
+
);
|
|
51
|
+
const dock = container.querySelector('.console-detail-dock');
|
|
52
|
+
expect(dock).not.toBeNull();
|
|
53
|
+
expect(dock).toContainElement(getByText('comment-composer'));
|
|
54
|
+
expect(dock).toContainElement(getByText('operation-bar'));
|
|
55
|
+
});
|
|
56
|
+
|
|
47
57
|
it('reports a failed related pull request read on an issue item', () => {
|
|
48
58
|
const { getByRole } = render(
|
|
49
59
|
<ConsoleItemDetail
|
|
@@ -327,9 +327,10 @@ export const ConsoleItemDetail = ({
|
|
|
327
327
|
/>
|
|
328
328
|
))}
|
|
329
329
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
330
|
+
<div className="console-detail-dock">
|
|
331
|
+
<div className="console-detail-dock-composer">{commentComposer}</div>
|
|
332
|
+
<div className="console-actionbar">{operationBar}</div>
|
|
333
|
+
</div>
|
|
333
334
|
</article>
|
|
334
335
|
);
|
|
335
336
|
};
|
|
@@ -4,10 +4,15 @@ import {
|
|
|
4
4
|
isOverlayEntryActed,
|
|
5
5
|
overlayEntriesActedSinceSnapshot,
|
|
6
6
|
overlayKeyForItem,
|
|
7
|
+
overlayStatusSinceSnapshot,
|
|
7
8
|
overlayStorageKey,
|
|
8
9
|
writeOverlayEntry,
|
|
9
10
|
} from './overlay';
|
|
10
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
ConsoleListItem,
|
|
13
|
+
ConsoleOverlay,
|
|
14
|
+
ConsoleOverlayStatus,
|
|
15
|
+
} from './types';
|
|
11
16
|
|
|
12
17
|
const item = (number: number): ConsoleListItem => ({
|
|
13
18
|
number,
|
|
@@ -230,3 +235,76 @@ describe('writeOverlayEntry', () => {
|
|
|
230
235
|
});
|
|
231
236
|
});
|
|
232
237
|
});
|
|
238
|
+
|
|
239
|
+
describe('overlayStatusSinceSnapshot', () => {
|
|
240
|
+
const snapshotGeneratedAt = '2026-08-13T21:52:01.000Z';
|
|
241
|
+
const snapshotEpochMs = Date.parse(snapshotGeneratedAt);
|
|
242
|
+
const inTmuxByHuman: ConsoleOverlayStatus = {
|
|
243
|
+
name: 'In Tmux by human',
|
|
244
|
+
color: 'RED',
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const overlayWithStatusAt = (epochMs: number): ConsoleOverlay => ({
|
|
248
|
+
[overlayKeyForItem(item(1))]: {
|
|
249
|
+
status: inTmuxByHuman,
|
|
250
|
+
ts: epochMs,
|
|
251
|
+
mode: 'todo-by-human',
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('shows a status the owner set after the snapshot was generated', () => {
|
|
256
|
+
expect(
|
|
257
|
+
overlayStatusSinceSnapshot(
|
|
258
|
+
overlayWithStatusAt(snapshotEpochMs + 1000),
|
|
259
|
+
item(1),
|
|
260
|
+
snapshotGeneratedAt,
|
|
261
|
+
),
|
|
262
|
+
).toEqual(inTmuxByHuman);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('drops a status the snapshot already superseded', () => {
|
|
266
|
+
expect(
|
|
267
|
+
overlayStatusSinceSnapshot(
|
|
268
|
+
overlayWithStatusAt(snapshotEpochMs - 1000),
|
|
269
|
+
item(1),
|
|
270
|
+
snapshotGeneratedAt,
|
|
271
|
+
),
|
|
272
|
+
).toBeNull();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('drops every status when the snapshot time is unusable', () => {
|
|
276
|
+
expect(
|
|
277
|
+
overlayStatusSinceSnapshot(
|
|
278
|
+
overlayWithStatusAt(snapshotEpochMs + 1000),
|
|
279
|
+
item(1),
|
|
280
|
+
null,
|
|
281
|
+
),
|
|
282
|
+
).toBeNull();
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('shows a status set at the exact moment the snapshot was generated', () => {
|
|
286
|
+
expect(
|
|
287
|
+
overlayStatusSinceSnapshot(
|
|
288
|
+
overlayWithStatusAt(snapshotEpochMs),
|
|
289
|
+
item(1),
|
|
290
|
+
snapshotGeneratedAt,
|
|
291
|
+
),
|
|
292
|
+
).toEqual(inTmuxByHuman);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('drops every status when the snapshot time cannot be parsed', () => {
|
|
296
|
+
expect(
|
|
297
|
+
overlayStatusSinceSnapshot(
|
|
298
|
+
overlayWithStatusAt(snapshotEpochMs + 1000),
|
|
299
|
+
item(1),
|
|
300
|
+
'not a timestamp',
|
|
301
|
+
),
|
|
302
|
+
).toBeNull();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('returns null when the item has no overlay entry', () => {
|
|
306
|
+
expect(
|
|
307
|
+
overlayStatusSinceSnapshot({}, item(1), snapshotGeneratedAt),
|
|
308
|
+
).toBeNull();
|
|
309
|
+
});
|
|
310
|
+
});
|