github-issue-tower-defence-management 1.158.0 → 1.160.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-DmsQyXek.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-DT98Xk6Y.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/assets/index-BbUBeonj.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.158.0",
3
+ "version": "1.160.0",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "scripts": {
@@ -3,37 +3,32 @@ import type { ConsoleComment } from '../../logic/types';
3
3
  import {
4
4
  appendAttachmentMarkdown,
5
5
  ConsoleCommentComposer,
6
+ insertUploadPlaceholder,
7
+ removePlaceholder,
8
+ replacePlaceholderWithMarkdown,
6
9
  } from './ConsoleCommentComposer';
7
10
 
8
11
  jest.mock('../../lib/mermaidLoader', () => ({
9
12
  renderMermaidToSvg: jest.fn(async () => '<svg></svg>'),
10
13
  }));
11
14
 
15
+ const stubSubmit = async (body: string): Promise<ConsoleComment> => ({
16
+ author: 'HiromiShikata',
17
+ body,
18
+ createdAt: '2026-06-19T11:58:00.000Z',
19
+ });
20
+
12
21
  describe('ConsoleCommentComposer', () => {
13
22
  it('shows the form when it starts open', () => {
14
23
  const { getByPlaceholderText } = render(
15
- <ConsoleCommentComposer
16
- initiallyOpen
17
- onSubmit={async (body) => ({
18
- author: 'HiromiShikata',
19
- body,
20
- createdAt: '2026-06-19T11:58:00.000Z',
21
- })}
22
- />,
24
+ <ConsoleCommentComposer initiallyOpen onSubmit={stubSubmit} />,
23
25
  );
24
26
  expect(getByPlaceholderText('Leave a comment…')).toBeInTheDocument();
25
27
  });
26
28
 
27
29
  it('offers the opening control and hides the form when it starts closed', () => {
28
30
  const { queryByPlaceholderText, getByText } = render(
29
- <ConsoleCommentComposer
30
- initiallyOpen={false}
31
- onSubmit={async (body) => ({
32
- author: 'HiromiShikata',
33
- body,
34
- createdAt: '2026-06-19T11:58:00.000Z',
35
- })}
36
- />,
31
+ <ConsoleCommentComposer initiallyOpen={false} onSubmit={stubSubmit} />,
37
32
  );
38
33
  expect(queryByPlaceholderText('Leave a comment…')).toBeNull();
39
34
  expect(getByText('💬 Add a comment')).toBeInTheDocument();
@@ -41,14 +36,7 @@ describe('ConsoleCommentComposer', () => {
41
36
 
42
37
  it('opens the form on the control and closes it again so the body regains the height', () => {
43
38
  const { getByText, getByPlaceholderText, queryByPlaceholderText } = render(
44
- <ConsoleCommentComposer
45
- initiallyOpen={false}
46
- onSubmit={async (body) => ({
47
- author: 'HiromiShikata',
48
- body,
49
- createdAt: '2026-06-19T11:58:00.000Z',
50
- })}
51
- />,
39
+ <ConsoleCommentComposer initiallyOpen={false} onSubmit={stubSubmit} />,
52
40
  );
53
41
  fireEvent.click(getByText('💬 Add a comment'));
54
42
  expect(getByPlaceholderText('Leave a comment…')).toBeInTheDocument();
@@ -98,30 +86,23 @@ describe('ConsoleCommentComposer', () => {
98
86
 
99
87
  it('does not render the attach control when no upload handler is given', () => {
100
88
  const { queryByText } = render(
101
- <ConsoleCommentComposer
102
- initiallyOpen
103
- onSubmit={async (body) => ({
104
- author: 'HiromiShikata',
105
- body,
106
- createdAt: '2026-06-19T11:58:00.000Z',
107
- })}
108
- />,
89
+ <ConsoleCommentComposer initiallyOpen onSubmit={stubSubmit} />,
109
90
  );
110
91
  expect(queryByText('📎 Attach files')).toBeNull();
111
92
  });
112
93
 
113
- it('uploads a selected file and inserts the returned markdown into the draft', async () => {
94
+ it('inserts a placeholder when a file upload starts and replaces it on success', async () => {
95
+ let resolveUpload!: (markdown: string) => void;
114
96
  const onUploadFile = jest.fn(
115
- async () => '![shot](https://github.com/user-attachments/assets/abc)',
97
+ () =>
98
+ new Promise<string>((resolve) => {
99
+ resolveUpload = resolve;
100
+ }),
116
101
  );
117
102
  const { getByPlaceholderText, getByLabelText } = render(
118
103
  <ConsoleCommentComposer
119
104
  initiallyOpen
120
- onSubmit={async (body) => ({
121
- author: 'HiromiShikata',
122
- body,
123
- createdAt: '2026-06-19T11:58:00.000Z',
124
- })}
105
+ onSubmit={stubSubmit}
125
106
  onUploadFile={onUploadFile}
126
107
  />,
127
108
  );
@@ -135,24 +116,98 @@ describe('ConsoleCommentComposer', () => {
135
116
  });
136
117
  await waitFor(() => {
137
118
  expect(textarea.value).toBe(
138
- 'See the screen:\n![shot](https://github.com/user-attachments/assets/abc)\n',
119
+ 'See the screen:\n\n\n\n![uploading shot.png]()\n',
120
+ );
121
+ });
122
+ resolveUpload('![shot](https://github.com/user-attachments/assets/abc)');
123
+ await waitFor(() => {
124
+ expect(textarea.value).toBe(
125
+ 'See the screen:\n\n\n\n![shot](https://github.com/user-attachments/assets/abc)\n',
139
126
  );
140
127
  });
141
128
  expect(onUploadFile).toHaveBeenCalledWith(file);
142
129
  });
143
130
 
144
- it('uploads a pasted file', async () => {
131
+ it('removes the placeholder and the three empty lines when an upload fails', async () => {
132
+ let rejectUpload!: (error: Error) => void;
133
+ const onUploadFile = jest.fn(
134
+ () =>
135
+ new Promise<string>((_, reject) => {
136
+ rejectUpload = reject;
137
+ }),
138
+ );
139
+ const { getByPlaceholderText, getByLabelText, findByRole } = render(
140
+ <ConsoleCommentComposer
141
+ initiallyOpen
142
+ onSubmit={stubSubmit}
143
+ onUploadFile={onUploadFile}
144
+ />,
145
+ );
146
+ const textarea = getByPlaceholderText(
147
+ 'Leave a comment…',
148
+ ) as HTMLTextAreaElement;
149
+ fireEvent.change(textarea, { target: { value: 'See the screen:' } });
150
+ const file = new File(['binary'], 'shot.png', { type: 'image/png' });
151
+ fireEvent.change(getByLabelText('Attach files'), {
152
+ target: { files: [file] },
153
+ });
154
+ await waitFor(() => {
155
+ expect(textarea.value).toContain('![uploading shot.png]()');
156
+ });
157
+ rejectUpload(new Error('No GitHub web session is available'));
158
+ await waitFor(() => {
159
+ expect(textarea.value).toBe('See the screen:\n');
160
+ });
161
+ const alert = await findByRole('alert');
162
+ expect(alert.textContent).toContain('No GitHub web session is available');
163
+ });
164
+
165
+ it('preserves text typed in the draft while an upload is in flight', async () => {
166
+ let resolveUpload!: (markdown: string) => void;
167
+ const onUploadFile = jest.fn(
168
+ () =>
169
+ new Promise<string>((resolve) => {
170
+ resolveUpload = resolve;
171
+ }),
172
+ );
173
+ const { getByPlaceholderText, getByLabelText } = render(
174
+ <ConsoleCommentComposer
175
+ initiallyOpen
176
+ onSubmit={stubSubmit}
177
+ onUploadFile={onUploadFile}
178
+ />,
179
+ );
180
+ const textarea = getByPlaceholderText(
181
+ 'Leave a comment…',
182
+ ) as HTMLTextAreaElement;
183
+ const file = new File(['binary'], 'shot.png', { type: 'image/png' });
184
+ fireEvent.change(getByLabelText('Attach files'), {
185
+ target: { files: [file] },
186
+ });
187
+ await waitFor(() => {
188
+ expect(textarea.value).toContain('![uploading shot.png]()');
189
+ });
190
+ fireEvent.change(textarea, {
191
+ target: {
192
+ value: 'The fix works:\n\n\n\n![uploading shot.png]()\n',
193
+ },
194
+ });
195
+ resolveUpload('![shot](https://github.com/user-attachments/assets/abc)');
196
+ await waitFor(() => {
197
+ expect(textarea.value).toBe(
198
+ 'The fix works:\n\n\n\n![shot](https://github.com/user-attachments/assets/abc)\n',
199
+ );
200
+ });
201
+ });
202
+
203
+ it('uploads a pasted file and reserves position with a placeholder', async () => {
145
204
  const onUploadFile = jest.fn(
146
205
  async () => '![pasted](https://github.com/user-attachments/assets/def)',
147
206
  );
148
207
  const { getByPlaceholderText } = render(
149
208
  <ConsoleCommentComposer
150
209
  initiallyOpen
151
- onSubmit={async (body) => ({
152
- author: 'HiromiShikata',
153
- body,
154
- createdAt: '2026-06-19T11:58:00.000Z',
155
- })}
210
+ onSubmit={stubSubmit}
156
211
  onUploadFile={onUploadFile}
157
212
  />,
158
213
  );
@@ -163,24 +218,20 @@ describe('ConsoleCommentComposer', () => {
163
218
  fireEvent.paste(textarea, { clipboardData: { files: [file] } });
164
219
  await waitFor(() => {
165
220
  expect(textarea.value).toBe(
166
- '![pasted](https://github.com/user-attachments/assets/def)\n',
221
+ '\n\n\n![pasted](https://github.com/user-attachments/assets/def)\n',
167
222
  );
168
223
  });
169
224
  expect(onUploadFile).toHaveBeenCalledWith(file);
170
225
  });
171
226
 
172
- it('uploads a dropped file', async () => {
227
+ it('uploads a dropped file and reserves position with a placeholder', async () => {
173
228
  const onUploadFile = jest.fn(
174
229
  async () => '![dropped](https://github.com/user-attachments/assets/ghi)',
175
230
  );
176
231
  const { getByPlaceholderText } = render(
177
232
  <ConsoleCommentComposer
178
233
  initiallyOpen
179
- onSubmit={async (body) => ({
180
- author: 'HiromiShikata',
181
- body,
182
- createdAt: '2026-06-19T11:58:00.000Z',
183
- })}
234
+ onSubmit={stubSubmit}
184
235
  onUploadFile={onUploadFile}
185
236
  />,
186
237
  );
@@ -191,21 +242,51 @@ describe('ConsoleCommentComposer', () => {
191
242
  fireEvent.drop(textarea, { dataTransfer: { files: [file] } });
192
243
  await waitFor(() => {
193
244
  expect(textarea.value).toBe(
194
- '![dropped](https://github.com/user-attachments/assets/ghi)\n',
245
+ '\n\n\n![dropped](https://github.com/user-attachments/assets/ghi)\n',
195
246
  );
196
247
  });
197
248
  expect(onUploadFile).toHaveBeenCalledWith(file);
198
249
  });
199
250
 
251
+ it('removes the placeholder when text was typed into a reserved empty line and the upload fails', async () => {
252
+ let rejectUpload!: (error: Error) => void;
253
+ const onUploadFile = jest.fn(
254
+ () =>
255
+ new Promise<string>((_, reject) => {
256
+ rejectUpload = reject;
257
+ }),
258
+ );
259
+ const { getByPlaceholderText, getByLabelText } = render(
260
+ <ConsoleCommentComposer
261
+ initiallyOpen
262
+ onSubmit={stubSubmit}
263
+ onUploadFile={onUploadFile}
264
+ />,
265
+ );
266
+ const textarea = getByPlaceholderText(
267
+ 'Leave a comment…',
268
+ ) as HTMLTextAreaElement;
269
+ const file = new File(['binary'], 'shot.png', { type: 'image/png' });
270
+ fireEvent.change(getByLabelText('Attach files'), {
271
+ target: { files: [file] },
272
+ });
273
+ await waitFor(() => {
274
+ expect(textarea.value).toContain('![uploading shot.png]()');
275
+ });
276
+ fireEvent.change(textarea, {
277
+ target: { value: '\n見てください\n\n![uploading shot.png]()\n' },
278
+ });
279
+ rejectUpload(new Error('network error'));
280
+ await waitFor(() => {
281
+ expect(textarea.value).not.toContain('![uploading shot.png]()');
282
+ });
283
+ });
284
+
200
285
  it('shows the upload failure reason when the upload rejects', async () => {
201
286
  const { getByLabelText, findByRole } = render(
202
287
  <ConsoleCommentComposer
203
288
  initiallyOpen
204
- onSubmit={async (body) => ({
205
- author: 'HiromiShikata',
206
- body,
207
- createdAt: '2026-06-19T11:58:00.000Z',
208
- })}
289
+ onSubmit={stubSubmit}
209
290
  onUploadFile={async () => {
210
291
  throw new Error('No GitHub web session is available');
211
292
  }}
@@ -219,6 +300,91 @@ describe('ConsoleCommentComposer', () => {
219
300
  });
220
301
  });
221
302
 
303
+ describe('insertUploadPlaceholder', () => {
304
+ it('writes the placeholder with three empty lines when the draft is empty', () => {
305
+ expect(insertUploadPlaceholder('', 'shot.png')).toBe(
306
+ '\n\n\n![uploading shot.png]()\n',
307
+ );
308
+ });
309
+
310
+ it('normalizes the draft to end with one newline then adds three empty lines before the placeholder', () => {
311
+ expect(insertUploadPlaceholder('See the screen:', 'shot.png')).toBe(
312
+ 'See the screen:\n\n\n\n![uploading shot.png]()\n',
313
+ );
314
+ });
315
+
316
+ it('does not double the trailing newline when the draft already ends with one', () => {
317
+ expect(insertUploadPlaceholder('See the screen:\n', 'shot.png')).toBe(
318
+ 'See the screen:\n\n\n\n![uploading shot.png]()\n',
319
+ );
320
+ });
321
+
322
+ it('strips multiple trailing newlines before normalizing', () => {
323
+ expect(insertUploadPlaceholder('See the screen:\n\n', 'shot.png')).toBe(
324
+ 'See the screen:\n\n\n\n![uploading shot.png]()\n',
325
+ );
326
+ });
327
+ });
328
+
329
+ describe('replacePlaceholderWithMarkdown', () => {
330
+ it('replaces the placeholder line with the returned markdown', () => {
331
+ const draft = 'See the screen:\n\n\n\n![uploading shot.png]()\n';
332
+ expect(
333
+ replacePlaceholderWithMarkdown(
334
+ draft,
335
+ 'shot.png',
336
+ '![shot](https://github.com/user-attachments/assets/abc)',
337
+ ),
338
+ ).toBe(
339
+ 'See the screen:\n\n\n\n![shot](https://github.com/user-attachments/assets/abc)\n',
340
+ );
341
+ });
342
+
343
+ it('replaces only the first occurrence when two uploads share the same file name', () => {
344
+ const draft =
345
+ 'text\n\n\n\n![uploading file.png]()\n\n\n\n![uploading file.png]()\n';
346
+ expect(
347
+ replacePlaceholderWithMarkdown(draft, 'file.png', '![img](url)'),
348
+ ).toBe('text\n\n\n\n![img](url)\n\n\n\n![uploading file.png]()\n');
349
+ });
350
+
351
+ it('falls back to appending when the placeholder is absent', () => {
352
+ expect(
353
+ replacePlaceholderWithMarkdown('text\n', 'shot.png', '![shot](url)'),
354
+ ).toBe('text\n![shot](url)\n');
355
+ });
356
+ });
357
+
358
+ describe('removePlaceholder', () => {
359
+ it('removes the placeholder and the three empty lines added with it', () => {
360
+ const draft = 'See the screen:\n\n\n\n![uploading shot.png]()\n';
361
+ expect(removePlaceholder(draft, 'shot.png')).toBe('See the screen:\n');
362
+ });
363
+
364
+ it('leaves the draft empty when the placeholder was inserted into an empty draft', () => {
365
+ const draft = '\n\n\n![uploading shot.png]()\n';
366
+ expect(removePlaceholder(draft, 'shot.png')).toBe('');
367
+ });
368
+
369
+ it('returns the draft unchanged when the placeholder is absent', () => {
370
+ expect(removePlaceholder('text\n', 'shot.png')).toBe('text\n');
371
+ });
372
+
373
+ it('removes the placeholder when text was typed into the second reserved empty line', () => {
374
+ const draft = '\n見てください\n\n![uploading shot.png]()\n';
375
+ expect(removePlaceholder(draft, 'shot.png')).not.toContain(
376
+ '![uploading shot.png]()',
377
+ );
378
+ });
379
+
380
+ it('removes the placeholder when text was typed into the third reserved empty line', () => {
381
+ const draft = '\n\n見てください\n![uploading shot.png]()\n';
382
+ expect(removePlaceholder(draft, 'shot.png')).not.toContain(
383
+ '![uploading shot.png]()',
384
+ );
385
+ });
386
+ });
387
+
222
388
  describe('appendAttachmentMarkdown', () => {
223
389
  it('returns the markdown alone when the draft is empty', () => {
224
390
  expect(appendAttachmentMarkdown('', '![a](b)')).toBe('![a](b)\n');
@@ -30,6 +30,47 @@ export const appendAttachmentMarkdown = (
30
30
  return `${draft}\n${markdown}\n`;
31
31
  };
32
32
 
33
+ export const insertUploadPlaceholder = (
34
+ draft: string,
35
+ fileName: string,
36
+ ): string => {
37
+ const placeholder = `![uploading ${fileName}]()`;
38
+ const prefix = draft.length === 0 ? '' : draft.replace(/\n*$/, '\n');
39
+ return `${prefix}\n\n\n${placeholder}\n`;
40
+ };
41
+
42
+ export const replacePlaceholderWithMarkdown = (
43
+ draft: string,
44
+ fileName: string,
45
+ markdown: string,
46
+ ): string => {
47
+ const placeholder = `![uploading ${fileName}]()`;
48
+ const index = draft.indexOf(placeholder);
49
+ if (index === -1) {
50
+ return appendAttachmentMarkdown(draft, markdown);
51
+ }
52
+ return (
53
+ draft.slice(0, index) + markdown + draft.slice(index + placeholder.length)
54
+ );
55
+ };
56
+
57
+ export const removePlaceholder = (draft: string, fileName: string): string => {
58
+ const placeholder = `![uploading ${fileName}]()`;
59
+ const index = draft.indexOf(placeholder);
60
+ if (index === -1) {
61
+ return draft;
62
+ }
63
+ let start = index;
64
+ let newlinesRemoved = 0;
65
+ while (newlinesRemoved < 3 && start > 0 && draft[start - 1] === '\n') {
66
+ start--;
67
+ newlinesRemoved++;
68
+ }
69
+ const trailingNewline = draft[index + placeholder.length] === '\n' ? 1 : 0;
70
+ const end = index + placeholder.length + trailingNewline;
71
+ return draft.slice(0, start) + draft.slice(end);
72
+ };
73
+
33
74
  export const ConsoleCommentComposer = ({
34
75
  initiallyOpen,
35
76
  onSubmit,
@@ -66,12 +107,16 @@ export const ConsoleCommentComposer = ({
66
107
  return;
67
108
  }
68
109
  for (const file of files) {
110
+ setDraft((previous) => insertUploadPlaceholder(previous, file.name));
69
111
  setUploadStatus({ kind: 'uploading', fileName: file.name });
70
112
  try {
71
113
  const markdown = await onUploadFile(file);
72
- setDraft((previous) => appendAttachmentMarkdown(previous, markdown));
114
+ setDraft((previous) =>
115
+ replacePlaceholderWithMarkdown(previous, file.name, markdown),
116
+ );
73
117
  setUploadStatus({ kind: 'idle' });
74
118
  } catch (error) {
119
+ setDraft((previous) => removePlaceholder(previous, file.name));
75
120
  setUploadStatus({
76
121
  kind: 'error',
77
122
  message:
@@ -53,6 +53,7 @@ const meta: Meta<typeof ConsoleItemDetail> = {
53
53
  component: ConsoleItemDetail,
54
54
  args: {
55
55
  now: Date.parse('2026-06-19T12:00:00.000Z'),
56
+ statusOptions: consoleStatusOptionsFixture,
56
57
  commentComposer: null,
57
58
  operationBar: null,
58
59
  },
@@ -365,3 +366,35 @@ export const IssueWithEveryReadFailedByOneCause: Story = {
365
366
  relatedPullRequests: [],
366
367
  },
367
368
  };
369
+
370
+ export const IssueWithSnapshotStatusNoOverlay: Story = {
371
+ args: {
372
+ item: consoleListItemsFixture[2],
373
+ storyName: 'TDPM Console port',
374
+ storyColorEnum: 'BLUE',
375
+ overlayStatus: null,
376
+ state: {
377
+ state: 'open',
378
+ merged: false,
379
+ isPullRequest: false,
380
+ title: 'Scaffold React console UI under entry-points with build bundling',
381
+ },
382
+ body: '## Issue body\n\nThis item has a status set in the snapshot.',
383
+ bodyIsLoading: false,
384
+ bodyError: null,
385
+ stateError: null,
386
+ pullRequestStatusError: null,
387
+ relatedPullRequestsError: null,
388
+ comments: [],
389
+ commentsAreLoading: false,
390
+ commentsError: null,
391
+ files: [],
392
+ filesAreLoading: false,
393
+ filesError: null,
394
+ commits: [],
395
+ commitsAreLoading: false,
396
+ commitsError: null,
397
+ pullRequestStatus: null,
398
+ relatedPullRequests: [],
399
+ },
400
+ };
@@ -5,6 +5,7 @@ import {
5
5
  consoleCommitsFixture,
6
6
  consoleListItemsFixture,
7
7
  consoleRelatedPullRequestsFixture,
8
+ consoleStatusOptionsFixture,
8
9
  } from '../../testing/fixtures';
9
10
  import { ConsoleItemDetail } from './ConsoleItemDetail';
10
11
 
@@ -20,6 +21,7 @@ const baseProps = {
20
21
  storyName: 'TDPM Console port',
21
22
  storyColorEnum: 'BLUE' as const,
22
23
  overlayStatus: null,
24
+ statusOptions: consoleStatusOptionsFixture,
23
25
  state: { state: 'open', merged: false, isPullRequest: true, title: '' },
24
26
  body: '## Body heading',
25
27
  bodyIsLoading: false,
@@ -480,4 +482,45 @@ describe('ConsoleItemDetail', () => {
480
482
  within(topline as HTMLElement).getByText('CI failing'),
481
483
  ).toBeInTheDocument();
482
484
  });
485
+
486
+ it('shows the snapshot status chip in the header when the item has a status and no overlay entry applies', () => {
487
+ const { getByText, container } = render(
488
+ <ConsoleItemDetail
489
+ item={issueItem}
490
+ {...baseProps}
491
+ overlayStatus={null}
492
+ />,
493
+ );
494
+ const title = container.querySelector('.console-detail-title');
495
+ expect(title).not.toBeNull();
496
+ expect(title?.contains(getByText(issueItem.status as string))).toBe(true);
497
+ });
498
+
499
+ it('shows the overlay status in the header when the overlay entry is newer than the snapshot', () => {
500
+ const { getByText, queryByText, container } = render(
501
+ <ConsoleItemDetail
502
+ item={issueItem}
503
+ {...baseProps}
504
+ overlayStatus={{ name: 'In Progress', color: 'GREEN' }}
505
+ />,
506
+ );
507
+ const title = container.querySelector('.console-detail-title');
508
+ expect(title).not.toBeNull();
509
+ expect(title?.contains(getByText('In Progress'))).toBe(true);
510
+ expect(queryByText(issueItem.status as string)).toBeNull();
511
+ });
512
+
513
+ it('renders no status chip when the item has no status and no overlay applies', () => {
514
+ const noStatusItem = consoleListItemsFixture[3];
515
+ expect(noStatusItem.status).toBeNull();
516
+ const { container } = render(
517
+ <ConsoleItemDetail
518
+ item={noStatusItem}
519
+ {...baseProps}
520
+ overlayStatus={null}
521
+ />,
522
+ );
523
+ const chip = container.querySelector('.console-detail-status-chip');
524
+ expect(chip).toBeNull();
525
+ });
483
526
  });
@@ -14,6 +14,7 @@ import type {
14
14
  ConsoleColor,
15
15
  ConsoleComment,
16
16
  ConsoleCommit,
17
+ ConsoleFieldOption,
17
18
  ConsoleIssueState,
18
19
  ConsoleListItem,
19
20
  ConsoleMergeableStatus,
@@ -51,6 +52,7 @@ export type ConsoleItemDetailProps = {
51
52
  storyName: string | null;
52
53
  storyColorEnum: ConsoleColor | null;
53
54
  overlayStatus: ConsoleOverlayStatus | null;
55
+ statusOptions: ConsoleFieldOption[];
54
56
  state: ConsoleIssueState | null;
55
57
  body: string;
56
58
  bodyIsLoading: boolean;
@@ -82,6 +84,7 @@ export const ConsoleItemDetail = ({
82
84
  storyName,
83
85
  storyColorEnum,
84
86
  overlayStatus,
87
+ statusOptions,
85
88
  state,
86
89
  body,
87
90
  bodyIsLoading,
@@ -113,9 +116,19 @@ export const ConsoleItemDetail = ({
113
116
  !item.isPr && resolvedState === 'closed' ? 'Closed' : null;
114
117
  const repoContext = parseNameWithOwner(item.nameWithOwner) ?? undefined;
115
118
  const storyPalette = colorFromEnum(storyColorEnum);
116
- const statusPalette = overlayStatus
117
- ? colorFromEnum(overlayStatus.color)
118
- : null;
119
+ const displayStatus: ConsoleOverlayStatus | null =
120
+ overlayStatus !== null
121
+ ? overlayStatus
122
+ : item.status !== null
123
+ ? {
124
+ name: item.status,
125
+ color:
126
+ statusOptions.find((o) => o.name === item.status)?.color ??
127
+ 'GRAY',
128
+ }
129
+ : null;
130
+ const statusPalette =
131
+ displayStatus !== null ? colorFromEnum(displayStatus.color) : null;
119
132
  const filesCount =
120
133
  filesAreLoading || filesError !== null ? null : files.length;
121
134
  const commentsCount =
@@ -218,7 +231,7 @@ export const ConsoleItemDetail = ({
218
231
  </div>
219
232
 
220
233
  <h2 className="console-detail-title">
221
- {overlayStatus !== null && statusPalette !== null && (
234
+ {displayStatus !== null && statusPalette !== null && (
222
235
  <span
223
236
  className="console-detail-status-chip"
224
237
  style={{
@@ -227,7 +240,7 @@ export const ConsoleItemDetail = ({
227
240
  backgroundColor: statusPalette.bg,
228
241
  }}
229
242
  >
230
- {overlayStatus.name}
243
+ {displayStatus.name}
231
244
  </span>
232
245
  )}
233
246
  <ConsoleItemIcon
@@ -165,6 +165,7 @@ export const ConsoleItemDetailContainer = ({
165
165
  storyName={resolvedStoryName}
166
166
  storyColorEnum={storyColorEnum}
167
167
  overlayStatus={overlayStatus}
168
+ statusOptions={statusOptions}
168
169
  state={detail.state}
169
170
  stateError={detail.stateError}
170
171
  body={detail.body}
@@ -106,7 +106,7 @@ describe('ConsolePage', () => {
106
106
  ).toContain('TDPM Console port');
107
107
  });
108
108
 
109
- it('keeps a status the snapshot already superseded out of the detail header', async () => {
109
+ it('keeps a stale overlay status out of the detail header and shows the snapshot status instead', async () => {
110
110
  localStorage.setItem(
111
111
  'pv_overlay_acme',
112
112
  JSON.stringify({
@@ -117,13 +117,17 @@ describe('ConsolePage', () => {
117
117
  },
118
118
  }),
119
119
  );
120
- const { getByText, findByText, container } = render(<ConsolePage />);
120
+ const { getByText, findByText, queryByText, container } = render(
121
+ <ConsolePage />,
122
+ );
121
123
  await waitFor(() => {
122
124
  expect(getByText('Add serveConsole subcommand')).toBeInTheDocument();
123
125
  });
124
126
  fireEvent.click(getByText('Add serveConsole subcommand'));
125
127
  expect(await findByText('Approve')).toBeInTheDocument();
126
- expect(container.querySelector('.console-detail-status-chip')).toBeNull();
128
+ expect(queryByText('In Tmux by human')).toBeNull();
129
+ const chip = container.querySelector('.console-detail-status-chip');
130
+ expect(chip?.textContent).toBe('Awaiting Quality Check');
127
131
  });
128
132
 
129
133
  it('shows a status the owner set after the snapshot was generated in the detail header', async () => {