github-issue-tower-defence-management 1.184.0 → 1.185.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/README.md +4 -4
- package/bin/adapter/entry-points/cli/index.js +31 -29
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleOperationApi.js +26 -2
- package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleProjectResolver.js +5 -1
- package/bin/adapter/entry-points/console/consoleProjectResolver.js.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-DQT4HL0G.js +86 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/bin/adapter/entry-points/console/webServer.js +4 -0
- package/bin/adapter/entry-points/console/webServer.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +3 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/domain/entities/Project.js +17 -0
- package/bin/domain/entities/Project.js.map +1 -1
- package/bin/domain/usecases/CreateNewStoryByLabelUseCase.js +5 -16
- package/bin/domain/usecases/CreateNewStoryByLabelUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.ts +57 -52
- package/src/adapter/entry-points/console/consoleOperationApi.test.ts +242 -12
- package/src/adapter/entry-points/console/consoleOperationApi.ts +43 -7
- package/src/adapter/entry-points/console/consoleProjectResolver.test.ts +30 -10
- package/src/adapter/entry-points/console/consoleProjectResolver.ts +13 -2
- package/src/adapter/entry-points/console/ui/e2e/consoleScenario.spec.ts +37 -2
- package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +20 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.stories.tsx +4 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.test.tsx +127 -8
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.tsx +119 -71
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +26 -0
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +20 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +12 -0
- package/src/adapter/entry-points/console/ui-dist/assets/index-DQT4HL0G.js +86 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/src/adapter/entry-points/console/webServer.test.ts +74 -0
- package/src/adapter/entry-points/console/webServer.ts +8 -0
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +38 -1
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +3 -1
- package/src/domain/entities/Project.ts +23 -0
- package/src/domain/usecases/CreateNewStoryByLabelUseCase.ts +16 -34
- package/types/adapter/entry-points/cli/index.d.ts +1 -1
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts +8 -3
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleProjectResolver.d.ts +5 -1
- package/types/adapter/entry-points/console/consoleProjectResolver.d.ts.map +1 -1
- package/types/adapter/entry-points/console/webServer.d.ts +3 -1
- package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/domain/entities/Project.d.ts +4 -0
- package/types/domain/entities/Project.d.ts.map +1 -1
- package/types/domain/usecases/CreateNewStoryByLabelUseCase.d.ts +6 -8
- package/types/domain/usecases/CreateNewStoryByLabelUseCase.d.ts.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-CMrnEYO1.js +0 -86
- package/src/adapter/entry-points/console/ui-dist/assets/index-CMrnEYO1.js +0 -86
|
@@ -19,6 +19,11 @@ const storyEntries: ConsoleStoryEntry[] = [
|
|
|
19
19
|
},
|
|
20
20
|
];
|
|
21
21
|
|
|
22
|
+
const defaultProps = {
|
|
23
|
+
onCreateIssue: () => Promise.resolve(),
|
|
24
|
+
onAddStory: () => Promise.resolve(),
|
|
25
|
+
};
|
|
26
|
+
|
|
22
27
|
describe('ConsoleStoryList', () => {
|
|
23
28
|
it('renders each story name and its open item count', () => {
|
|
24
29
|
const { getByText } = render(
|
|
@@ -26,7 +31,7 @@ describe('ConsoleStoryList', () => {
|
|
|
26
31
|
stories={storyEntries}
|
|
27
32
|
isLoading={false}
|
|
28
33
|
error={null}
|
|
29
|
-
|
|
34
|
+
{...defaultProps}
|
|
30
35
|
/>,
|
|
31
36
|
);
|
|
32
37
|
expect(getByText('TDPM Console port')).toBeInTheDocument();
|
|
@@ -41,7 +46,7 @@ describe('ConsoleStoryList', () => {
|
|
|
41
46
|
stories={storyEntries}
|
|
42
47
|
isLoading={false}
|
|
43
48
|
error={null}
|
|
44
|
-
|
|
49
|
+
{...defaultProps}
|
|
45
50
|
/>,
|
|
46
51
|
);
|
|
47
52
|
const buttons = getAllByRole('button', { name: 'Add task' });
|
|
@@ -54,7 +59,7 @@ describe('ConsoleStoryList', () => {
|
|
|
54
59
|
stories={storyEntries}
|
|
55
60
|
isLoading={false}
|
|
56
61
|
error={null}
|
|
57
|
-
|
|
62
|
+
{...defaultProps}
|
|
58
63
|
/>,
|
|
59
64
|
);
|
|
60
65
|
const [firstButton] = getAllByRole('button', { name: 'Add task' });
|
|
@@ -68,7 +73,7 @@ describe('ConsoleStoryList', () => {
|
|
|
68
73
|
stories={storyEntries}
|
|
69
74
|
isLoading={false}
|
|
70
75
|
error={null}
|
|
71
|
-
|
|
76
|
+
{...defaultProps}
|
|
72
77
|
/>,
|
|
73
78
|
);
|
|
74
79
|
const [firstButton] = getAllByRole('button', { name: 'Add task' });
|
|
@@ -85,6 +90,7 @@ describe('ConsoleStoryList', () => {
|
|
|
85
90
|
isLoading={false}
|
|
86
91
|
error={null}
|
|
87
92
|
onCreateIssue={onCreateIssue}
|
|
93
|
+
onAddStory={() => Promise.resolve()}
|
|
88
94
|
/>,
|
|
89
95
|
);
|
|
90
96
|
fireEvent.click(getAllByRole('button', { name: 'Add task' })[0]);
|
|
@@ -106,7 +112,7 @@ describe('ConsoleStoryList', () => {
|
|
|
106
112
|
stories={storyEntries}
|
|
107
113
|
isLoading={false}
|
|
108
114
|
error={null}
|
|
109
|
-
|
|
115
|
+
{...defaultProps}
|
|
110
116
|
/>,
|
|
111
117
|
);
|
|
112
118
|
fireEvent.click(getAllByRole('button', { name: 'Add task' })[0]);
|
|
@@ -127,6 +133,7 @@ describe('ConsoleStoryList', () => {
|
|
|
127
133
|
isLoading={false}
|
|
128
134
|
error={null}
|
|
129
135
|
onCreateIssue={onCreateIssue}
|
|
136
|
+
onAddStory={() => Promise.resolve()}
|
|
130
137
|
/>,
|
|
131
138
|
);
|
|
132
139
|
fireEvent.click(getAllByRole('button', { name: 'Add task' })[0]);
|
|
@@ -149,6 +156,7 @@ describe('ConsoleStoryList', () => {
|
|
|
149
156
|
isLoading={false}
|
|
150
157
|
error={null}
|
|
151
158
|
onCreateIssue={onCreateIssue}
|
|
159
|
+
onAddStory={() => Promise.resolve()}
|
|
152
160
|
/>,
|
|
153
161
|
);
|
|
154
162
|
fireEvent.click(getAllByRole('button', { name: 'Add task' })[0]);
|
|
@@ -165,7 +173,7 @@ describe('ConsoleStoryList', () => {
|
|
|
165
173
|
stories={[]}
|
|
166
174
|
isLoading={true}
|
|
167
175
|
error={null}
|
|
168
|
-
|
|
176
|
+
{...defaultProps}
|
|
169
177
|
/>,
|
|
170
178
|
);
|
|
171
179
|
expect(getByText('Loading stories...')).toBeInTheDocument();
|
|
@@ -177,7 +185,7 @@ describe('ConsoleStoryList', () => {
|
|
|
177
185
|
stories={[]}
|
|
178
186
|
isLoading={false}
|
|
179
187
|
error={null}
|
|
180
|
-
|
|
188
|
+
{...defaultProps}
|
|
181
189
|
/>,
|
|
182
190
|
);
|
|
183
191
|
expect(getByText('No active stories')).toBeInTheDocument();
|
|
@@ -189,7 +197,7 @@ describe('ConsoleStoryList', () => {
|
|
|
189
197
|
stories={[]}
|
|
190
198
|
isLoading={false}
|
|
191
199
|
error="HTTP 503"
|
|
192
|
-
|
|
200
|
+
{...defaultProps}
|
|
193
201
|
/>,
|
|
194
202
|
);
|
|
195
203
|
expect(getByRole('alert')).toBeInTheDocument();
|
|
@@ -209,6 +217,7 @@ describe('ConsoleStoryList', () => {
|
|
|
209
217
|
isLoading={false}
|
|
210
218
|
error={null}
|
|
211
219
|
onCreateIssue={() => Promise.resolve()}
|
|
220
|
+
onAddStory={() => Promise.resolve()}
|
|
212
221
|
/>,
|
|
213
222
|
);
|
|
214
223
|
const nameEl = getByText('TDPM Console port');
|
|
@@ -222,6 +231,7 @@ describe('ConsoleStoryList', () => {
|
|
|
222
231
|
isLoading={false}
|
|
223
232
|
error={null}
|
|
224
233
|
onCreateIssue={() => Promise.resolve()}
|
|
234
|
+
onAddStory={() => Promise.resolve()}
|
|
225
235
|
/>,
|
|
226
236
|
);
|
|
227
237
|
const nameEl = getByText('TDPM Console port');
|
|
@@ -242,6 +252,7 @@ describe('ConsoleStoryList', () => {
|
|
|
242
252
|
isLoading={false}
|
|
243
253
|
error={null}
|
|
244
254
|
onCreateIssue={() => Promise.resolve()}
|
|
255
|
+
onAddStory={() => Promise.resolve()}
|
|
245
256
|
/>,
|
|
246
257
|
);
|
|
247
258
|
const nameEl = getByText('TDPM Console port');
|
|
@@ -253,4 +264,112 @@ describe('ConsoleStoryList', () => {
|
|
|
253
264
|
expect(nameEl).toHaveAttribute('target', '_blank');
|
|
254
265
|
expect(nameEl).toHaveAttribute('rel', 'noopener noreferrer');
|
|
255
266
|
});
|
|
267
|
+
|
|
268
|
+
it('renders an Add story button', () => {
|
|
269
|
+
const { getByRole } = render(
|
|
270
|
+
<ConsoleStoryList
|
|
271
|
+
stories={storyEntries}
|
|
272
|
+
isLoading={false}
|
|
273
|
+
error={null}
|
|
274
|
+
{...defaultProps}
|
|
275
|
+
/>,
|
|
276
|
+
);
|
|
277
|
+
expect(getByRole('button', { name: 'Add story' })).toBeInTheDocument();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('shows the Add story form when Add story button is clicked', () => {
|
|
281
|
+
const { getByRole, getByPlaceholderText } = render(
|
|
282
|
+
<ConsoleStoryList
|
|
283
|
+
stories={storyEntries}
|
|
284
|
+
isLoading={false}
|
|
285
|
+
error={null}
|
|
286
|
+
{...defaultProps}
|
|
287
|
+
/>,
|
|
288
|
+
);
|
|
289
|
+
fireEvent.click(getByRole('button', { name: 'Add story' }));
|
|
290
|
+
expect(getByPlaceholderText('Story name')).toBeInTheDocument();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('hides the Add story form when Add story button is clicked again', () => {
|
|
294
|
+
const { getByRole, queryByPlaceholderText } = render(
|
|
295
|
+
<ConsoleStoryList
|
|
296
|
+
stories={storyEntries}
|
|
297
|
+
isLoading={false}
|
|
298
|
+
error={null}
|
|
299
|
+
{...defaultProps}
|
|
300
|
+
/>,
|
|
301
|
+
);
|
|
302
|
+
fireEvent.click(getByRole('button', { name: 'Add story' }));
|
|
303
|
+
fireEvent.click(getByRole('button', { name: 'Add story' }));
|
|
304
|
+
expect(queryByPlaceholderText('Story name')).toBeNull();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('calls onAddStory with the story name and closes the form on success', async () => {
|
|
308
|
+
const onAddStory = jest.fn().mockResolvedValue(undefined);
|
|
309
|
+
const { getByRole, getByPlaceholderText, queryByPlaceholderText } = render(
|
|
310
|
+
<ConsoleStoryList
|
|
311
|
+
stories={storyEntries}
|
|
312
|
+
isLoading={false}
|
|
313
|
+
error={null}
|
|
314
|
+
onCreateIssue={() => Promise.resolve()}
|
|
315
|
+
onAddStory={onAddStory}
|
|
316
|
+
/>,
|
|
317
|
+
);
|
|
318
|
+
fireEvent.click(getByRole('button', { name: 'Add story' }));
|
|
319
|
+
fireEvent.change(getByPlaceholderText('Story name'), {
|
|
320
|
+
target: { value: 'My new story' },
|
|
321
|
+
});
|
|
322
|
+
fireEvent.click(getByRole('button', { name: 'Create' }));
|
|
323
|
+
await waitFor(() =>
|
|
324
|
+
expect(onAddStory).toHaveBeenCalledWith('My new story'),
|
|
325
|
+
);
|
|
326
|
+
await waitFor(() =>
|
|
327
|
+
expect(queryByPlaceholderText('Story name')).toBeNull(),
|
|
328
|
+
);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('shows a validation error when Add story Create is clicked with an empty name', () => {
|
|
332
|
+
const { getByRole, getByText } = render(
|
|
333
|
+
<ConsoleStoryList
|
|
334
|
+
stories={storyEntries}
|
|
335
|
+
isLoading={false}
|
|
336
|
+
error={null}
|
|
337
|
+
{...defaultProps}
|
|
338
|
+
/>,
|
|
339
|
+
);
|
|
340
|
+
fireEvent.click(getByRole('button', { name: 'Add story' }));
|
|
341
|
+
fireEvent.click(getByRole('button', { name: 'Create' }));
|
|
342
|
+
expect(getByText('Story name is required')).toBeInTheDocument();
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('shows an API error when onAddStory rejects', async () => {
|
|
346
|
+
const onAddStory = jest.fn().mockRejectedValue(new Error('API failure'));
|
|
347
|
+
const { getByRole, getByPlaceholderText, getByText } = render(
|
|
348
|
+
<ConsoleStoryList
|
|
349
|
+
stories={storyEntries}
|
|
350
|
+
isLoading={false}
|
|
351
|
+
error={null}
|
|
352
|
+
onCreateIssue={() => Promise.resolve()}
|
|
353
|
+
onAddStory={onAddStory}
|
|
354
|
+
/>,
|
|
355
|
+
);
|
|
356
|
+
fireEvent.click(getByRole('button', { name: 'Add story' }));
|
|
357
|
+
fireEvent.change(getByPlaceholderText('Story name'), {
|
|
358
|
+
target: { value: 'Bad story' },
|
|
359
|
+
});
|
|
360
|
+
fireEvent.click(getByRole('button', { name: 'Create' }));
|
|
361
|
+
await waitFor(() => expect(getByText('API failure')).toBeInTheDocument());
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('renders Add story button even when there are no stories', () => {
|
|
365
|
+
const { getByRole } = render(
|
|
366
|
+
<ConsoleStoryList
|
|
367
|
+
stories={[]}
|
|
368
|
+
isLoading={false}
|
|
369
|
+
error={null}
|
|
370
|
+
{...defaultProps}
|
|
371
|
+
/>,
|
|
372
|
+
);
|
|
373
|
+
expect(getByRole('button', { name: 'Add story' })).toBeInTheDocument();
|
|
374
|
+
});
|
|
256
375
|
});
|
|
@@ -2,18 +2,20 @@ import { useEffect, useRef, useState } from 'react';
|
|
|
2
2
|
import { colorFromEnum } from '../../logic/colors';
|
|
3
3
|
import type { ConsoleStoryEntry } from '../../logic/types';
|
|
4
4
|
|
|
5
|
-
type
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
type InlineInputFormProps = {
|
|
6
|
+
placeholder: string;
|
|
7
|
+
emptyValueError: string;
|
|
8
|
+
onSubmit: (value: string) => Promise<void>;
|
|
8
9
|
onCancel: () => void;
|
|
9
10
|
};
|
|
10
11
|
|
|
11
|
-
const
|
|
12
|
-
|
|
12
|
+
const InlineInputForm = ({
|
|
13
|
+
placeholder,
|
|
14
|
+
emptyValueError,
|
|
13
15
|
onSubmit,
|
|
14
16
|
onCancel,
|
|
15
|
-
}:
|
|
16
|
-
const [
|
|
17
|
+
}: InlineInputFormProps) => {
|
|
18
|
+
const [valueInput, setValueInput] = useState('');
|
|
17
19
|
const [submitting, setSubmitting] = useState(false);
|
|
18
20
|
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
19
21
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
@@ -23,15 +25,15 @@ const StoryCreateForm = ({
|
|
|
23
25
|
}, []);
|
|
24
26
|
|
|
25
27
|
const handleSubmit = async (): Promise<void> => {
|
|
26
|
-
const trimmed =
|
|
28
|
+
const trimmed = valueInput.trim();
|
|
27
29
|
if (trimmed.length === 0) {
|
|
28
|
-
setSubmitError(
|
|
30
|
+
setSubmitError(emptyValueError);
|
|
29
31
|
return;
|
|
30
32
|
}
|
|
31
33
|
setSubmitting(true);
|
|
32
34
|
setSubmitError(null);
|
|
33
35
|
try {
|
|
34
|
-
await onSubmit(
|
|
36
|
+
await onSubmit(trimmed);
|
|
35
37
|
} catch (err) {
|
|
36
38
|
setSubmitError(err instanceof Error ? err.message : String(err));
|
|
37
39
|
} finally {
|
|
@@ -41,7 +43,7 @@ const StoryCreateForm = ({
|
|
|
41
43
|
|
|
42
44
|
return (
|
|
43
45
|
<form
|
|
44
|
-
className="console-
|
|
46
|
+
className="console-inline-input-form"
|
|
45
47
|
onSubmit={(event) => {
|
|
46
48
|
event.preventDefault();
|
|
47
49
|
void handleSubmit();
|
|
@@ -50,10 +52,10 @@ const StoryCreateForm = ({
|
|
|
50
52
|
<input
|
|
51
53
|
ref={inputRef}
|
|
52
54
|
type="text"
|
|
53
|
-
className="console-
|
|
54
|
-
placeholder=
|
|
55
|
-
value={
|
|
56
|
-
onChange={(e) =>
|
|
55
|
+
className="console-inline-input-form-input"
|
|
56
|
+
placeholder={placeholder}
|
|
57
|
+
value={valueInput}
|
|
58
|
+
onChange={(e) => setValueInput(e.target.value)}
|
|
57
59
|
disabled={submitting}
|
|
58
60
|
/>
|
|
59
61
|
<button type="submit" className="console-op-button" disabled={submitting}>
|
|
@@ -76,11 +78,31 @@ const StoryCreateForm = ({
|
|
|
76
78
|
);
|
|
77
79
|
};
|
|
78
80
|
|
|
81
|
+
type StoryCreateFormProps = {
|
|
82
|
+
storyOptionId: string;
|
|
83
|
+
onSubmit: (storyOptionId: string, title: string) => Promise<void>;
|
|
84
|
+
onCancel: () => void;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const StoryCreateForm = ({
|
|
88
|
+
storyOptionId,
|
|
89
|
+
onSubmit,
|
|
90
|
+
onCancel,
|
|
91
|
+
}: StoryCreateFormProps) => (
|
|
92
|
+
<InlineInputForm
|
|
93
|
+
placeholder="Issue title"
|
|
94
|
+
emptyValueError="Title is required"
|
|
95
|
+
onSubmit={(title) => onSubmit(storyOptionId, title)}
|
|
96
|
+
onCancel={onCancel}
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
|
|
79
100
|
export type ConsoleStoryListProps = {
|
|
80
101
|
stories: ConsoleStoryEntry[];
|
|
81
102
|
isLoading: boolean;
|
|
82
103
|
error: string | null;
|
|
83
104
|
onCreateIssue: (storyOptionId: string, title: string) => Promise<void>;
|
|
105
|
+
onAddStory: (storyName: string) => Promise<void>;
|
|
84
106
|
};
|
|
85
107
|
|
|
86
108
|
export const ConsoleStoryList = ({
|
|
@@ -88,8 +110,10 @@ export const ConsoleStoryList = ({
|
|
|
88
110
|
isLoading,
|
|
89
111
|
error,
|
|
90
112
|
onCreateIssue,
|
|
113
|
+
onAddStory,
|
|
91
114
|
}: ConsoleStoryListProps) => {
|
|
92
115
|
const [expandedOptionId, setExpandedOptionId] = useState<string | null>(null);
|
|
116
|
+
const [addStoryExpanded, setAddStoryExpanded] = useState(false);
|
|
93
117
|
|
|
94
118
|
if (error !== null) {
|
|
95
119
|
return (
|
|
@@ -103,10 +127,6 @@ export const ConsoleStoryList = ({
|
|
|
103
127
|
return <p className="console-list-message">Loading stories...</p>;
|
|
104
128
|
}
|
|
105
129
|
|
|
106
|
-
if (stories.length === 0) {
|
|
107
|
-
return <p className="console-list-empty">No active stories</p>;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
130
|
const handleAddClick = (storyOptionId: string): void => {
|
|
111
131
|
setExpandedOptionId(
|
|
112
132
|
expandedOptionId === storyOptionId ? null : storyOptionId,
|
|
@@ -122,58 +142,86 @@ export const ConsoleStoryList = ({
|
|
|
122
142
|
};
|
|
123
143
|
|
|
124
144
|
return (
|
|
125
|
-
<
|
|
126
|
-
{stories.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
145
|
+
<div className="console-story-list-container">
|
|
146
|
+
{stories.length === 0 ? (
|
|
147
|
+
<p className="console-list-empty">No active stories</p>
|
|
148
|
+
) : (
|
|
149
|
+
<ul className="console-story-list">
|
|
150
|
+
{stories.map((entry) => {
|
|
151
|
+
const palette = colorFromEnum(entry.color);
|
|
152
|
+
const isExpanded = expandedOptionId === entry.storyOptionId;
|
|
153
|
+
return (
|
|
154
|
+
<li key={entry.storyOptionId} className="console-story-list-row">
|
|
155
|
+
<div className="console-story-list-row-main">
|
|
156
|
+
{entry.storyViewUrl ? (
|
|
157
|
+
<a
|
|
158
|
+
href={entry.storyViewUrl}
|
|
159
|
+
target="_blank"
|
|
160
|
+
rel="noopener noreferrer"
|
|
161
|
+
className="console-storytag"
|
|
162
|
+
style={{ color: palette.fg, borderColor: palette.border }}
|
|
163
|
+
>
|
|
164
|
+
<span
|
|
165
|
+
className="console-story-dot"
|
|
166
|
+
style={{ backgroundColor: palette.dot }}
|
|
167
|
+
/>
|
|
168
|
+
{entry.storyName}
|
|
169
|
+
</a>
|
|
170
|
+
) : (
|
|
171
|
+
<span
|
|
172
|
+
className="console-storytag"
|
|
173
|
+
style={{ color: palette.fg, borderColor: palette.border }}
|
|
174
|
+
>
|
|
175
|
+
<span
|
|
176
|
+
className="console-story-dot"
|
|
177
|
+
style={{ backgroundColor: palette.dot }}
|
|
178
|
+
/>
|
|
179
|
+
{entry.storyName}
|
|
180
|
+
</span>
|
|
181
|
+
)}
|
|
182
|
+
<span className="console-story-count">
|
|
183
|
+
{entry.openItemCount}
|
|
184
|
+
</span>
|
|
185
|
+
<button
|
|
186
|
+
type="button"
|
|
187
|
+
className="console-op-button"
|
|
188
|
+
onClick={() => handleAddClick(entry.storyOptionId)}
|
|
189
|
+
>
|
|
190
|
+
Add task
|
|
191
|
+
</button>
|
|
192
|
+
</div>
|
|
193
|
+
{isExpanded && (
|
|
194
|
+
<StoryCreateForm
|
|
195
|
+
storyOptionId={entry.storyOptionId}
|
|
196
|
+
onSubmit={handleSubmit}
|
|
197
|
+
onCancel={() => setExpandedOptionId(null)}
|
|
143
198
|
/>
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
onCancel={() => setExpandedOptionId(null)}
|
|
172
|
-
/>
|
|
173
|
-
)}
|
|
174
|
-
</li>
|
|
175
|
-
);
|
|
176
|
-
})}
|
|
177
|
-
</ul>
|
|
199
|
+
)}
|
|
200
|
+
</li>
|
|
201
|
+
);
|
|
202
|
+
})}
|
|
203
|
+
</ul>
|
|
204
|
+
)}
|
|
205
|
+
<div className="console-add-story-section">
|
|
206
|
+
<button
|
|
207
|
+
type="button"
|
|
208
|
+
className="console-op-button"
|
|
209
|
+
onClick={() => setAddStoryExpanded(!addStoryExpanded)}
|
|
210
|
+
>
|
|
211
|
+
Add story
|
|
212
|
+
</button>
|
|
213
|
+
{addStoryExpanded && (
|
|
214
|
+
<InlineInputForm
|
|
215
|
+
placeholder="Story name"
|
|
216
|
+
emptyValueError="Story name is required"
|
|
217
|
+
onSubmit={async (storyName) => {
|
|
218
|
+
await onAddStory(storyName);
|
|
219
|
+
setAddStoryExpanded(false);
|
|
220
|
+
}}
|
|
221
|
+
onCancel={() => setAddStoryExpanded(false)}
|
|
222
|
+
/>
|
|
223
|
+
)}
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
178
226
|
);
|
|
179
227
|
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
+
ADD_STORY_OPERATION_PATH,
|
|
2
3
|
createConsoleApiClient,
|
|
4
|
+
postConsoleAddStory,
|
|
3
5
|
postConsoleOperation,
|
|
4
6
|
postConsoleReviewComment,
|
|
5
7
|
} from './consoleApi';
|
|
@@ -418,3 +420,27 @@ describe('postConsoleReviewComment', () => {
|
|
|
418
420
|
).rejects.toThrow('line must be part of the diff');
|
|
419
421
|
});
|
|
420
422
|
});
|
|
423
|
+
|
|
424
|
+
describe('postConsoleAddStory', () => {
|
|
425
|
+
it('posts the story name to the addstory endpoint', async () => {
|
|
426
|
+
const fetchMock = mockFetchOnce({ ok: true });
|
|
427
|
+
await postConsoleAddStory({ pjcode: 'acme', storyName: 'My new story' });
|
|
428
|
+
const [url, init] = fetchMock.mock.calls[0];
|
|
429
|
+
expect(url).toBe(ADD_STORY_OPERATION_PATH);
|
|
430
|
+
expect(init).toMatchObject({ method: 'POST' });
|
|
431
|
+
expect(JSON.parse((init as { body: string }).body)).toEqual({
|
|
432
|
+
pjcode: 'acme',
|
|
433
|
+
storyName: 'My new story',
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('throws the error reason surfaced by the server', async () => {
|
|
438
|
+
mockFetchFailureOnce(
|
|
439
|
+
400,
|
|
440
|
+
JSON.stringify({ error: 'storyName is required' }),
|
|
441
|
+
);
|
|
442
|
+
await expect(
|
|
443
|
+
postConsoleAddStory({ pjcode: 'acme', storyName: '' }),
|
|
444
|
+
).rejects.toThrow('storyName is required');
|
|
445
|
+
});
|
|
446
|
+
});
|
|
@@ -374,6 +374,26 @@ export const postConsoleAttachment = async (
|
|
|
374
374
|
return payload.markdown;
|
|
375
375
|
};
|
|
376
376
|
|
|
377
|
+
export const ADD_STORY_OPERATION_PATH = '/api/addstory';
|
|
378
|
+
|
|
379
|
+
export type ConsoleAddStoryRequest = {
|
|
380
|
+
pjcode: string;
|
|
381
|
+
storyName: string;
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
export const postConsoleAddStory = async (
|
|
385
|
+
request: ConsoleAddStoryRequest,
|
|
386
|
+
): Promise<void> => {
|
|
387
|
+
const response = await fetch(ADD_STORY_OPERATION_PATH, {
|
|
388
|
+
method: 'POST',
|
|
389
|
+
headers: { 'Content-Type': 'application/json' },
|
|
390
|
+
body: JSON.stringify(request),
|
|
391
|
+
});
|
|
392
|
+
if (!response.ok) {
|
|
393
|
+
throw new Error(await readOperationErrorReason(response));
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
|
|
377
397
|
export const REVIEW_COMMENT_OPERATION_PATH = '/api/reviewcomment';
|
|
378
398
|
|
|
379
399
|
export const postConsoleReviewComment = async (
|
|
@@ -21,6 +21,7 @@ import { useConsoleSwipeNavigation } from '../hooks/useConsoleSwipeNavigation';
|
|
|
21
21
|
import { useConsoleTabData } from '../hooks/useConsoleTabData';
|
|
22
22
|
import { useConsoleTimerSettings } from '../hooks/useConsoleTimerSettings';
|
|
23
23
|
import {
|
|
24
|
+
postConsoleAddStory,
|
|
24
25
|
postConsoleCreateIssue,
|
|
25
26
|
postConsoleReorderStory,
|
|
26
27
|
} from '../lib/consoleApi';
|
|
@@ -375,6 +376,16 @@ export const ConsolePage = () => {
|
|
|
375
376
|
[pjcode, triageStoryOptions, activeSnapshot?.generatedAt],
|
|
376
377
|
);
|
|
377
378
|
|
|
379
|
+
const handleStoryAdd = useCallback(
|
|
380
|
+
async (storyName: string): Promise<void> => {
|
|
381
|
+
if (pjcode === null) {
|
|
382
|
+
throw new Error('No project specified in the URL path.');
|
|
383
|
+
}
|
|
384
|
+
await postConsoleAddStory({ pjcode, storyName });
|
|
385
|
+
},
|
|
386
|
+
[pjcode],
|
|
387
|
+
);
|
|
388
|
+
|
|
378
389
|
return (
|
|
379
390
|
<main className="console-app">
|
|
380
391
|
{actionQueue.pending !== null && (
|
|
@@ -421,6 +432,7 @@ export const ConsolePage = () => {
|
|
|
421
432
|
isLoading={isLoading}
|
|
422
433
|
error={error}
|
|
423
434
|
onCreateIssue={handleCreateIssue}
|
|
435
|
+
onAddStory={handleStoryAdd}
|
|
424
436
|
/>
|
|
425
437
|
) : selectedItem === null ? (
|
|
426
438
|
<>
|