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.
Files changed (55) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +4 -4
  3. package/bin/adapter/entry-points/cli/index.js +31 -29
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/console/consoleOperationApi.js +26 -2
  6. package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
  7. package/bin/adapter/entry-points/console/consoleProjectResolver.js +5 -1
  8. package/bin/adapter/entry-points/console/consoleProjectResolver.js.map +1 -1
  9. package/bin/adapter/entry-points/console/ui-dist/assets/index-DQT4HL0G.js +86 -0
  10. package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
  11. package/bin/adapter/entry-points/console/webServer.js +4 -0
  12. package/bin/adapter/entry-points/console/webServer.js.map +1 -1
  13. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +3 -1
  14. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  15. package/bin/domain/entities/Project.js +17 -0
  16. package/bin/domain/entities/Project.js.map +1 -1
  17. package/bin/domain/usecases/CreateNewStoryByLabelUseCase.js +5 -16
  18. package/bin/domain/usecases/CreateNewStoryByLabelUseCase.js.map +1 -1
  19. package/package.json +1 -1
  20. package/src/adapter/entry-points/cli/index.ts +57 -52
  21. package/src/adapter/entry-points/console/consoleOperationApi.test.ts +242 -12
  22. package/src/adapter/entry-points/console/consoleOperationApi.ts +43 -7
  23. package/src/adapter/entry-points/console/consoleProjectResolver.test.ts +30 -10
  24. package/src/adapter/entry-points/console/consoleProjectResolver.ts +13 -2
  25. package/src/adapter/entry-points/console/ui/e2e/consoleScenario.spec.ts +37 -2
  26. package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +20 -0
  27. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.stories.tsx +4 -1
  28. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.test.tsx +127 -8
  29. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.tsx +119 -71
  30. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +26 -0
  31. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +20 -0
  32. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +12 -0
  33. package/src/adapter/entry-points/console/ui-dist/assets/index-DQT4HL0G.js +86 -0
  34. package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
  35. package/src/adapter/entry-points/console/webServer.test.ts +74 -0
  36. package/src/adapter/entry-points/console/webServer.ts +8 -0
  37. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +38 -1
  38. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +3 -1
  39. package/src/domain/entities/Project.ts +23 -0
  40. package/src/domain/usecases/CreateNewStoryByLabelUseCase.ts +16 -34
  41. package/types/adapter/entry-points/cli/index.d.ts +1 -1
  42. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  43. package/types/adapter/entry-points/console/consoleOperationApi.d.ts +8 -3
  44. package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
  45. package/types/adapter/entry-points/console/consoleProjectResolver.d.ts +5 -1
  46. package/types/adapter/entry-points/console/consoleProjectResolver.d.ts.map +1 -1
  47. package/types/adapter/entry-points/console/webServer.d.ts +3 -1
  48. package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
  49. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  50. package/types/domain/entities/Project.d.ts +4 -0
  51. package/types/domain/entities/Project.d.ts.map +1 -1
  52. package/types/domain/usecases/CreateNewStoryByLabelUseCase.d.ts +6 -8
  53. package/types/domain/usecases/CreateNewStoryByLabelUseCase.d.ts.map +1 -1
  54. package/bin/adapter/entry-points/console/ui-dist/assets/index-CMrnEYO1.js +0 -86
  55. 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
- onCreateIssue={() => Promise.resolve()}
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
- onCreateIssue={() => Promise.resolve()}
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
- onCreateIssue={() => Promise.resolve()}
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
- onCreateIssue={() => Promise.resolve()}
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
- onCreateIssue={() => Promise.resolve()}
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
- onCreateIssue={() => Promise.resolve()}
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
- onCreateIssue={() => Promise.resolve()}
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
- onCreateIssue={() => Promise.resolve()}
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 StoryCreateFormProps = {
6
- storyOptionId: string;
7
- onSubmit: (storyOptionId: string, title: string) => Promise<void>;
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 StoryCreateForm = ({
12
- storyOptionId,
12
+ const InlineInputForm = ({
13
+ placeholder,
14
+ emptyValueError,
13
15
  onSubmit,
14
16
  onCancel,
15
- }: StoryCreateFormProps) => {
16
- const [titleInput, setTitleInput] = useState('');
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 = titleInput.trim();
28
+ const trimmed = valueInput.trim();
27
29
  if (trimmed.length === 0) {
28
- setSubmitError('Title is required');
30
+ setSubmitError(emptyValueError);
29
31
  return;
30
32
  }
31
33
  setSubmitting(true);
32
34
  setSubmitError(null);
33
35
  try {
34
- await onSubmit(storyOptionId, trimmed);
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-story-create-form"
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-story-create-input"
54
- placeholder="Issue title"
55
- value={titleInput}
56
- onChange={(e) => setTitleInput(e.target.value)}
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
- <ul className="console-story-list">
126
- {stories.map((entry) => {
127
- const palette = colorFromEnum(entry.color);
128
- const isExpanded = expandedOptionId === entry.storyOptionId;
129
- return (
130
- <li key={entry.storyOptionId} className="console-story-list-row">
131
- <div className="console-story-list-row-main">
132
- {entry.storyViewUrl ? (
133
- <a
134
- href={entry.storyViewUrl}
135
- target="_blank"
136
- rel="noopener noreferrer"
137
- className="console-storytag"
138
- style={{ color: palette.fg, borderColor: palette.border }}
139
- >
140
- <span
141
- className="console-story-dot"
142
- style={{ backgroundColor: palette.dot }}
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
- {entry.storyName}
145
- </a>
146
- ) : (
147
- <span
148
- className="console-storytag"
149
- style={{ color: palette.fg, borderColor: palette.border }}
150
- >
151
- <span
152
- className="console-story-dot"
153
- style={{ backgroundColor: palette.dot }}
154
- />
155
- {entry.storyName}
156
- </span>
157
- )}
158
- <span className="console-story-count">{entry.openItemCount}</span>
159
- <button
160
- type="button"
161
- className="console-op-button"
162
- onClick={() => handleAddClick(entry.storyOptionId)}
163
- >
164
- Add task
165
- </button>
166
- </div>
167
- {isExpanded && (
168
- <StoryCreateForm
169
- storyOptionId={entry.storyOptionId}
170
- onSubmit={handleSubmit}
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
  <>