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.
- package/CHANGELOG.md +14 -0
- package/bin/adapter/entry-points/console/ui-dist/assets/index-DT98Xk6Y.js +86 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentComposer.test.tsx +225 -59
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentComposer.tsx +46 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.stories.tsx +33 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.test.tsx +43 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.tsx +18 -5
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +1 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.test.tsx +7 -3
- package/src/adapter/entry-points/console/ui-dist/assets/index-DT98Xk6Y.js +86 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-DmsQyXek.js +0 -83
- package/src/adapter/entry-points/console/ui-dist/assets/index-DmsQyXek.js +0 -83
|
@@ -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-
|
|
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
|
@@ -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('
|
|
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
|
-
|
|
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={
|
|
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\n',
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
resolveUpload('');
|
|
123
|
+
await waitFor(() => {
|
|
124
|
+
expect(textarea.value).toBe(
|
|
125
|
+
'See the screen:\n\n\n\n\n',
|
|
139
126
|
);
|
|
140
127
|
});
|
|
141
128
|
expect(onUploadFile).toHaveBeenCalledWith(file);
|
|
142
129
|
});
|
|
143
130
|
|
|
144
|
-
it('
|
|
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('');
|
|
196
|
+
await waitFor(() => {
|
|
197
|
+
expect(textarea.value).toBe(
|
|
198
|
+
'The fix works:\n\n\n\n\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 () => '',
|
|
147
206
|
);
|
|
148
207
|
const { getByPlaceholderText } = render(
|
|
149
208
|
<ConsoleCommentComposer
|
|
150
209
|
initiallyOpen
|
|
151
|
-
onSubmit={
|
|
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
|
-
'\n',
|
|
221
|
+
'\n\n\n\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 () => '',
|
|
175
230
|
);
|
|
176
231
|
const { getByPlaceholderText } = render(
|
|
177
232
|
<ConsoleCommentComposer
|
|
178
233
|
initiallyOpen
|
|
179
|
-
onSubmit={
|
|
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
|
-
'\n',
|
|
245
|
+
'\n\n\n\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={
|
|
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
|
+
'',
|
|
337
|
+
),
|
|
338
|
+
).toBe(
|
|
339
|
+
'See the screen:\n\n\n\n\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', ''),
|
|
348
|
+
).toBe('text\n\n\n\n\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', ''),
|
|
354
|
+
).toBe('text\n\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('', '')).toBe('\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) =>
|
|
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
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
|
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(
|
|
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(
|
|
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 () => {
|