github-issue-tower-defence-management 2.1.0 → 2.1.2

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 (32) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/bin/adapter/entry-points/console/consoleOperationApi.js +6 -4
  3. package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
  4. package/bin/adapter/entry-points/console/ui-dist/assets/{index-lT5OKx7e.js → index-BVyeR7xJ.js} +2 -2
  5. package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
  6. package/bin/adapter/entry-points/console/webServer.js +0 -4
  7. package/bin/adapter/entry-points/console/webServer.js.map +1 -1
  8. package/bin/domain/usecases/HandleScheduledEventUseCase.js +1 -0
  9. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  10. package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js +9 -0
  11. package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js.map +1 -1
  12. package/package.json +1 -1
  13. package/src/adapter/entry-points/console/consoleOperationApi.test.ts +60 -32
  14. package/src/adapter/entry-points/console/consoleOperationApi.ts +6 -7
  15. package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +12 -17
  16. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.stories.tsx +1 -0
  17. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.test.tsx +85 -0
  18. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.tsx +73 -1
  19. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +53 -16
  20. package/src/adapter/entry-points/console/ui-dist/assets/{index-lT5OKx7e.js → index-BVyeR7xJ.js} +2 -2
  21. package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
  22. package/src/adapter/entry-points/console/webServer.ts +0 -8
  23. package/src/domain/usecases/HandleScheduledEventUseCase.ts +1 -0
  24. package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +45 -0
  25. package/src/domain/usecases/RevertOrphanedPreparationUseCase.ts +19 -0
  26. package/types/adapter/entry-points/console/consoleOperationApi.d.ts +1 -2
  27. package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
  28. package/types/adapter/entry-points/console/webServer.d.ts +0 -2
  29. package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
  30. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  31. package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts +1 -0
  32. package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts.map +1 -1
@@ -38,6 +38,7 @@ const defaultProps = {
38
38
  onAddStory: () => Promise.resolve(),
39
39
  onSelectColor: () => undefined,
40
40
  onToggleGray: () => undefined,
41
+ onReorderStory: () => Promise.resolve(),
41
42
  optimisticColors: {} as Record<string, ConsoleColor>,
42
43
  colorChangeInFlight: null as string | null,
43
44
  colorErrors: {} as Record<string, string>,
@@ -466,4 +467,88 @@ describe('ConsoleStoryList', () => {
466
467
  expect(getByText('No active stories')).toBeInTheDocument();
467
468
  });
468
469
  });
470
+
471
+ describe('reorder buttons', () => {
472
+ it('renders Move up and Move down buttons for each visible story', () => {
473
+ const { getAllByRole } = render(<ConsoleStoryList {...defaultProps} />);
474
+ const upButtons = getAllByRole('button', { name: 'Move up' });
475
+ const downButtons = getAllByRole('button', { name: 'Move down' });
476
+ expect(upButtons).toHaveLength(storyEntries.length);
477
+ expect(downButtons).toHaveLength(storyEntries.length);
478
+ });
479
+
480
+ it('disables the Move up button for the first story', () => {
481
+ const { getAllByRole } = render(<ConsoleStoryList {...defaultProps} />);
482
+ const upButtons = getAllByRole('button', { name: 'Move up' });
483
+ expect(upButtons[0]).toBeDisabled();
484
+ expect(upButtons[1]).not.toBeDisabled();
485
+ });
486
+
487
+ it('disables the Move down button for the last story', () => {
488
+ const { getAllByRole } = render(<ConsoleStoryList {...defaultProps} />);
489
+ const downButtons = getAllByRole('button', { name: 'Move down' });
490
+ expect(downButtons[0]).not.toBeDisabled();
491
+ expect(downButtons[1]).toBeDisabled();
492
+ });
493
+
494
+ it('calls onReorderStory with storyOptionId and up when Move up is clicked', async () => {
495
+ const onReorderStory = jest.fn().mockResolvedValue(undefined);
496
+ const { getAllByRole } = render(
497
+ <ConsoleStoryList {...defaultProps} onReorderStory={onReorderStory} />,
498
+ );
499
+ const upButtons = getAllByRole('button', { name: 'Move up' });
500
+ await act(async () => {
501
+ fireEvent.click(upButtons[1]);
502
+ });
503
+ expect(onReorderStory).toHaveBeenCalledWith('564803ee', 'up');
504
+ });
505
+
506
+ it('calls onReorderStory with storyOptionId and down when Move down is clicked', async () => {
507
+ const onReorderStory = jest.fn().mockResolvedValue(undefined);
508
+ const { getAllByRole } = render(
509
+ <ConsoleStoryList {...defaultProps} onReorderStory={onReorderStory} />,
510
+ );
511
+ const downButtons = getAllByRole('button', { name: 'Move down' });
512
+ await act(async () => {
513
+ fireEvent.click(downButtons[0]);
514
+ });
515
+ expect(onReorderStory).toHaveBeenCalledWith('1491051e', 'down');
516
+ });
517
+
518
+ it('disables Move up and Move down buttons while reorder is in progress', async () => {
519
+ let resolveReorder: () => void;
520
+ const reorderPromise = new Promise<void>((resolve) => {
521
+ resolveReorder = resolve;
522
+ });
523
+ const onReorderStory = jest.fn().mockReturnValue(reorderPromise);
524
+ const { getAllByRole } = render(
525
+ <ConsoleStoryList {...defaultProps} onReorderStory={onReorderStory} />,
526
+ );
527
+ const downButtons = getAllByRole('button', { name: 'Move down' });
528
+ fireEvent.click(downButtons[0]);
529
+ await waitFor(() => {
530
+ const upButtons = getAllByRole('button', { name: 'Move up' });
531
+ expect(upButtons[0]).toBeDisabled();
532
+ expect(downButtons[0]).toBeDisabled();
533
+ });
534
+ await act(async () => {
535
+ resolveReorder!();
536
+ });
537
+ });
538
+
539
+ it('shows an error message when onReorderStory throws', async () => {
540
+ const onReorderStory = jest
541
+ .fn()
542
+ .mockRejectedValue(new Error('Reorder failed'));
543
+ const { getAllByRole, findByRole } = render(
544
+ <ConsoleStoryList {...defaultProps} onReorderStory={onReorderStory} />,
545
+ );
546
+ const downButtons = getAllByRole('button', { name: 'Move down' });
547
+ await act(async () => {
548
+ fireEvent.click(downButtons[0]);
549
+ });
550
+ const alert = await findByRole('alert');
551
+ expect(alert).toHaveTextContent('Reorder failed');
552
+ });
553
+ });
469
554
  });
@@ -3,6 +3,11 @@ import { CONSOLE_COLOR_PALETTE, colorFromEnum } from '../../logic/colors';
3
3
  import type { ConsoleColor, ConsoleStoryEntry } from '../../logic/types';
4
4
  import { ConsoleCopyStoryNameButton } from './ConsoleCopyStoryNameButton';
5
5
 
6
+ type RowReorderState = {
7
+ inProgress: boolean;
8
+ error: string | null;
9
+ };
10
+
6
11
  const ALL_COLORS = Object.keys(CONSOLE_COLOR_PALETTE) as ConsoleColor[];
7
12
 
8
13
  type InlineInputFormProps = {
@@ -137,6 +142,10 @@ export type ConsoleStoryListProps = {
137
142
  onAddStory: (storyName: string) => Promise<void>;
138
143
  onSelectColor: (storyOptionId: string, newColor: ConsoleColor) => void;
139
144
  onToggleGray: () => void;
145
+ onReorderStory: (
146
+ storyOptionId: string,
147
+ direction: 'up' | 'down',
148
+ ) => Promise<void>;
140
149
  optimisticColors: Record<string, ConsoleColor>;
141
150
  colorChangeInFlight: string | null;
142
151
  colorErrors: Record<string, string>;
@@ -151,6 +160,7 @@ export const ConsoleStoryList = ({
151
160
  onAddStory,
152
161
  onSelectColor,
153
162
  onToggleGray,
163
+ onReorderStory,
154
164
  optimisticColors,
155
165
  colorChangeInFlight,
156
166
  colorErrors,
@@ -160,6 +170,37 @@ export const ConsoleStoryList = ({
160
170
  const [colorPickerOptionId, setColorPickerOptionId] = useState<string | null>(
161
171
  null,
162
172
  );
173
+ const [rowReorderStates, setRowReorderStates] = useState<
174
+ Record<string, RowReorderState>
175
+ >({});
176
+
177
+ const getRowReorderState = (id: string): RowReorderState =>
178
+ rowReorderStates[id] ?? { inProgress: false, error: null };
179
+
180
+ const handleReorder = async (
181
+ storyOptionId: string,
182
+ direction: 'up' | 'down',
183
+ ): Promise<void> => {
184
+ setRowReorderStates((prev) => ({
185
+ ...prev,
186
+ [storyOptionId]: { inProgress: true, error: null },
187
+ }));
188
+ try {
189
+ await onReorderStory(storyOptionId, direction);
190
+ setRowReorderStates((prev) => ({
191
+ ...prev,
192
+ [storyOptionId]: { inProgress: false, error: null },
193
+ }));
194
+ } catch (err) {
195
+ setRowReorderStates((prev) => ({
196
+ ...prev,
197
+ [storyOptionId]: {
198
+ inProgress: false,
199
+ error: err instanceof Error ? err.message : String(err),
200
+ },
201
+ }));
202
+ }
203
+ };
163
204
 
164
205
  if (error !== null) {
165
206
  return (
@@ -212,7 +253,7 @@ export const ConsoleStoryList = ({
212
253
  <p className="console-list-empty">No active stories</p>
213
254
  ) : (
214
255
  <ul className="console-story-list">
215
- {visibleStories.map((entry) => {
256
+ {visibleStories.map((entry, index) => {
216
257
  const displayColor: ConsoleColor =
217
258
  optimisticColors[entry.storyOptionId] ?? entry.color;
218
259
  const palette = colorFromEnum(displayColor);
@@ -220,6 +261,10 @@ export const ConsoleStoryList = ({
220
261
  const isPickerOpen = colorPickerOptionId === entry.storyOptionId;
221
262
  const isInFlight = colorChangeInFlight === entry.storyOptionId;
222
263
  const colorError = colorErrors[entry.storyOptionId] ?? null;
264
+ const { inProgress: reorderInProgress, error: reorderError } =
265
+ getRowReorderState(entry.storyOptionId);
266
+ const isFirst = index === 0;
267
+ const isLast = index === visibleStories.length - 1;
223
268
  return (
224
269
  <li key={entry.storyOptionId} className="console-story-list-row">
225
270
  <div className="console-story-list-row-main">
@@ -268,7 +313,34 @@ export const ConsoleStoryList = ({
268
313
  >
269
314
  Change color
270
315
  </button>
316
+ <button
317
+ type="button"
318
+ className="console-op-button"
319
+ aria-label="Move up"
320
+ disabled={isFirst || reorderInProgress}
321
+ onClick={() =>
322
+ void handleReorder(entry.storyOptionId, 'up')
323
+ }
324
+ >
325
+ ↑
326
+ </button>
327
+ <button
328
+ type="button"
329
+ className="console-op-button"
330
+ aria-label="Move down"
331
+ disabled={isLast || reorderInProgress}
332
+ onClick={() =>
333
+ void handleReorder(entry.storyOptionId, 'down')
334
+ }
335
+ >
336
+ ↓
337
+ </button>
271
338
  </div>
339
+ {reorderError !== null && (
340
+ <p role="alert" className="console-list-error">
341
+ {reorderError}
342
+ </p>
343
+ )}
272
344
  {isPickerOpen && (
273
345
  <ColorPalette
274
346
  onSelectColor={(newColor) =>
@@ -52,6 +52,7 @@ import type {
52
52
  ConsoleColor,
53
53
  ConsoleListItem,
54
54
  ConsoleOverlayStatus,
55
+ ConsoleStoryEntry,
55
56
  ConsoleTabName,
56
57
  } from '../logic/types';
57
58
  import { CONSOLE_TABS } from '../logic/types';
@@ -185,6 +186,11 @@ export const ConsolePage = () => {
185
186
  stories: typeof storyOptions;
186
187
  } | null>(null);
187
188
 
189
+ const [localStoryEntriesOverride, setLocalStoryEntriesOverride] = useState<{
190
+ generatedAt: string | undefined;
191
+ stories: ConsoleStoryEntry[];
192
+ } | null>(null);
193
+
188
194
  const triageStoryOptions =
189
195
  localStoryOptionsOverride !== null &&
190
196
  localStoryOptionsOverride.generatedAt === activeSnapshot?.generatedAt
@@ -347,7 +353,12 @@ export const ConsolePage = () => {
347
353
  }, []);
348
354
 
349
355
  const storiesSnapshot = snapshots.stories;
350
- const storyEntries = storiesSnapshot?.stories ?? [];
356
+ const rawStoryEntries = storiesSnapshot?.stories ?? [];
357
+ const storyEntries =
358
+ localStoryEntriesOverride !== null &&
359
+ localStoryEntriesOverride.generatedAt === storiesSnapshot?.generatedAt
360
+ ? localStoryEntriesOverride.stories
361
+ : rawStoryEntries;
351
362
  const defaultNameWithOwner = storiesSnapshot?.defaultNameWithOwner ?? null;
352
363
 
353
364
  const handleCreateIssue = useCallback(
@@ -374,24 +385,49 @@ export const ConsolePage = () => {
374
385
  throw new Error('No project specified in the URL path.');
375
386
  }
376
387
  await postConsoleReorderStory({ pjcode, storyOptionId, direction });
377
- const index = triageStoryOptions.findIndex((o) => o.id === storyOptionId);
378
- if (index === -1) {
379
- return;
388
+ const triageIndex = triageStoryOptions.findIndex(
389
+ (o) => o.id === storyOptionId,
390
+ );
391
+ if (triageIndex !== -1) {
392
+ const triageSwapIndex = triageIndex + (direction === 'up' ? -1 : 1);
393
+ if (
394
+ triageSwapIndex >= 0 &&
395
+ triageSwapIndex < triageStoryOptions.length
396
+ ) {
397
+ const nextTriage = [...triageStoryOptions];
398
+ const triageTemp = nextTriage[triageIndex];
399
+ nextTriage[triageIndex] = nextTriage[triageSwapIndex];
400
+ nextTriage[triageSwapIndex] = triageTemp;
401
+ setLocalStoryOptionsOverride({
402
+ generatedAt: activeSnapshot?.generatedAt,
403
+ stories: nextTriage,
404
+ });
405
+ }
380
406
  }
381
- const swapIndex = index + (direction === 'up' ? -1 : 1);
382
- if (swapIndex < 0 || swapIndex >= triageStoryOptions.length) {
383
- return;
407
+ const entryIndex = storyEntries.findIndex(
408
+ (e) => e.storyOptionId === storyOptionId,
409
+ );
410
+ if (entryIndex !== -1) {
411
+ const entrySwapIndex = entryIndex + (direction === 'up' ? -1 : 1);
412
+ if (entrySwapIndex >= 0 && entrySwapIndex < storyEntries.length) {
413
+ const nextEntries = [...storyEntries];
414
+ const entryTemp = nextEntries[entryIndex];
415
+ nextEntries[entryIndex] = nextEntries[entrySwapIndex];
416
+ nextEntries[entrySwapIndex] = entryTemp;
417
+ setLocalStoryEntriesOverride({
418
+ generatedAt: storiesSnapshot?.generatedAt,
419
+ stories: nextEntries,
420
+ });
421
+ }
384
422
  }
385
- const next = [...triageStoryOptions];
386
- const temp = next[index];
387
- next[index] = next[swapIndex];
388
- next[swapIndex] = temp;
389
- setLocalStoryOptionsOverride({
390
- generatedAt: activeSnapshot?.generatedAt,
391
- stories: next,
392
- });
393
423
  },
394
- [pjcode, triageStoryOptions, activeSnapshot?.generatedAt],
424
+ [
425
+ pjcode,
426
+ triageStoryOptions,
427
+ storyEntries,
428
+ activeSnapshot?.generatedAt,
429
+ storiesSnapshot?.generatedAt,
430
+ ],
395
431
  );
396
432
 
397
433
  const handleStoryAdd = useCallback(
@@ -523,6 +559,7 @@ export const ConsolePage = () => {
523
559
  onAddStory={handleStoryAdd}
524
560
  onSelectColor={handleSelectColor}
525
561
  onToggleGray={handleToggleGray}
562
+ onReorderStory={handleReorderStory}
526
563
  optimisticColors={storyOptimisticColors}
527
564
  colorChangeInFlight={storyColorChangeInFlight}
528
565
  colorErrors={storyColorErrors}