|
18 | 18 | import ItemPicker from '../../pickers/ItemPicker.svelte'; |
19 | 19 | import { backlogStore, workspaceDataStore } from '../../stores/index.js'; |
20 | 20 | import { useWorkItemPoller } from '../../composables/useWorkItemPoller.svelte.js'; |
21 | | - import { successToast, warningToast } from '../../stores/toasts.svelte.js'; |
| 21 | + import { errorToast, successToast, warningToast } from '../../stores/toasts.svelte.js'; |
22 | 22 | import { getIncompleteIterationItems } from './iterationCompletion.js'; |
23 | 23 | import CompleteIterationDialog from '../../dialogs/CompleteIterationDialog.svelte'; |
24 | 24 |
|
|
48 | 48 | let addedGlobalIds = $state(new Set()); |
49 | 49 | let collapsedSections = $state(new Set()); |
50 | 50 | let sectionDropHighlight = $state(new Map()); // iterationId|'unassigned' -> boolean |
| 51 | + let pendingActionItemIds = $state(new Set()); |
51 | 52 |
|
52 | 53 | // --- Complete Iteration dialog state --- |
53 | 54 | let completeIterationShow = $state(false); |
|
82 | 83 | // Derived iteration groupings |
83 | 84 | let localIterations = $derived(allIterations.filter(i => !i.is_global)); |
84 | 85 | let addedGlobalIterations = $derived(allIterations.filter(i => i.is_global && addedGlobalIds.has(i.id))); |
| 86 | + let assignableIterations = $derived( |
| 87 | + allIterations.filter(i => i.status === 'planned' || i.status === 'active') |
| 88 | + ); |
85 | 89 |
|
86 | 90 | // Sort order: active first, then planned, then completed/cancelled |
87 | 91 | const statusOrder = { active: 0, planned: 1, completed: 2, cancelled: 3 }; |
|
287 | 291 | persistGlobalIds(); |
288 | 292 | } |
289 | 293 |
|
| 294 | + function setItemActionPending(itemId, pending) { |
| 295 | + const next = new Set(pendingActionItemIds); |
| 296 | + if (pending) next.add(itemId); |
| 297 | + else next.delete(itemId); |
| 298 | + pendingActionItemIds = next; |
| 299 | + } |
| 300 | +
|
| 301 | + async function moveItemToBoundary(item, boundary) { |
| 302 | + if (pendingActionItemIds.has(item.id)) return; |
| 303 | + setItemActionPending(item.id, true); |
| 304 | +
|
| 305 | + try { |
| 306 | + const boundaryItem = await api.items.getBacklogBoundary( |
| 307 | + workspaceId, |
| 308 | + collectionId, |
| 309 | + collectionStore.subFilterQL, |
| 310 | + boundary, |
| 311 | + ); |
| 312 | + if (!boundaryItem || boundaryItem.id === item.id) return; |
| 313 | +
|
| 314 | + await api.items.updateFracIndex(item.id, boundary === 'start' |
| 315 | + ? { prev_item_id: null, next_item_id: boundaryItem.id } |
| 316 | + : { prev_item_id: boundaryItem.id, next_item_id: null }); |
| 317 | +
|
| 318 | + const otherItems = collectionStore.backlogItems.filter(i => i.id !== item.id); |
| 319 | + collectionStore.backlogItems = boundary === 'start' |
| 320 | + ? [item, ...otherItems] |
| 321 | + : [...otherItems, item]; |
| 322 | + successToast(t(boundary === 'start' |
| 323 | + ? 'collections.movedToBeginningOfBacklog' |
| 324 | + : 'collections.sentToEndOfBacklog', { title: item.title })); |
| 325 | + reloadCollection(); |
| 326 | + } catch (error) { |
| 327 | + console.error(`Failed to move backlog item to ${boundary}:`, error); |
| 328 | + errorToast(t('collections.backlogActionFailed')); |
| 329 | + } finally { |
| 330 | + setItemActionPending(item.id, false); |
| 331 | + } |
| 332 | + } |
| 333 | +
|
| 334 | + async function assignItemToIteration(item, iteration) { |
| 335 | + if (pendingActionItemIds.has(item.id)) return; |
| 336 | + setItemActionPending(item.id, true); |
| 337 | +
|
| 338 | + try { |
| 339 | + if (iteration.status === 'active') { |
| 340 | + warningToast(t('iterations.activeScopeWarning')); |
| 341 | + } |
| 342 | + await api.items.update(item.id, { iteration_id: iteration.id }); |
| 343 | + collectionStore.backlogItems = collectionStore.backlogItems.map(i => |
| 344 | + i.id === item.id |
| 345 | + ? { ...i, iteration_id: iteration.id, iteration_name: iteration.name } |
| 346 | + : i |
| 347 | + ); |
| 348 | + successToast(t('collections.assignedToIteration', { |
| 349 | + title: item.title, |
| 350 | + iteration: iteration.name, |
| 351 | + })); |
| 352 | + reloadCollection(); |
| 353 | + } catch (error) { |
| 354 | + console.error('Failed to assign backlog item to iteration:', error); |
| 355 | + errorToast(t('collections.backlogActionFailed')); |
| 356 | + } finally { |
| 357 | + setItemActionPending(item.id, false); |
| 358 | + } |
| 359 | + } |
| 360 | +
|
290 | 361 | // --- Drag and Drop --- |
291 | 362 |
|
292 | 363 | // Get items belonging to a specific section |
|
654 | 725 | {styles} |
655 | 726 | {dragState} |
656 | 727 | {backlogRowGap} |
| 728 | + {assignableIterations} |
| 729 | + {pendingActionItemIds} |
657 | 730 | isGlobalAdded={addedGlobalIds.has(section.iteration.id)} |
658 | 731 | sectionHighlight={sectionDropHighlight.get(String(section.iteration.id)) || false} |
659 | 732 | onToggleCollapse={toggleCollapse} |
660 | 733 | onOpenItem={openItem} |
| 734 | + onMoveItemToBoundary={moveItemToBoundary} |
| 735 | + onAssignItemToIteration={assignItemToIteration} |
661 | 736 | onStartIteration={startIteration} |
662 | 737 | onCompleteIteration={completeIteration} |
663 | 738 | onRemoveGlobal={removeGlobalIteration} |
|
676 | 751 | {styles} |
677 | 752 | {dragState} |
678 | 753 | {backlogRowGap} |
| 754 | + {assignableIterations} |
| 755 | + {pendingActionItemIds} |
679 | 756 | sectionHighlight={sectionDropHighlight.get('unassigned') || false} |
680 | 757 | onToggleCollapse={toggleCollapse} |
681 | 758 | onOpenItem={openItem} |
| 759 | + onMoveItemToBoundary={moveItemToBoundary} |
| 760 | + onAssignItemToIteration={assignItemToIteration} |
682 | 761 | /> |
683 | 762 |
|
684 | 763 | <!-- Load More --> |
|
0 commit comments