[AGILE-362] Make Backlogs destination actions batch-aware - #24779
Draft
myabc wants to merge 16 commits into
Draft
Conversation
Render sprint and backlog bucket move dialogs for ordered work package batches. Accept controller-filtered destinations and submit the collection move route with one ID per selected work package.
Remove the redundant project argument from both batch move dialog components. Keep controller builders and component fixtures aligned with the destination records that already carry their project context.
Load sprint and backlog bucket dialogs through collection POST forms with the ordered action scope frozen at activation time. Submit inbox moves through the non-optimistic collection endpoint and preserve selection while Turbo reconciles the response. https://community.openproject.org/wp/AGILE-362
Require direct destination movement on the sortable root contract and keep generic controller test roots conformant. Clear detached-request busy markers when Turbo reconnects a cached root so destination actions can run again. https://community.openproject.org/wp/AGILE-362
Keep restored sortable roots busy while a request started before disconnect is still pending. Share request counting across direct destination and existing persisted moves so one settlement cannot clear another active request. https://community.openproject.org/wp/AGILE-362
Render server live-region feedback for visible non-optimistic collection moves, preserving the existing singular wording and adding batch position ranges. Keep invisibility flashes authoritative and cover ordered stale submissions plus submit-time destination races. https://community.openproject.org/wp/AGILE-362
Exercise batch-aware destination menus through Selenium, including ordered IDs, projected destinations, stale targets, and atomic failures. Clear Backlogs batch selection when a successful collection move event is rendered so Turbo-backed dialog and direct actions share the drag lifecycle. https://community.openproject.org/wp/AGILE-362
Ensure the disappearing-destination Selenium scenario rejects any visible modal, including a blank or unexpectedly titled dialog. https://community.openproject.org/wp/AGILE-362
Serialize append placement by project and destination. Lock destination rows before authoritative policy checks. Batch read-only status queries and cancel busy dialog loads. https://community.openproject.org/wp/AGILE-362
Share destination advisory locks with sprint lifecycle services. This prevents append placement from crossing cohort enumeration. Harden concurrency cleanup without masking original failures. https://community.openproject.org/wp/AGILE-362
Collect worker failures while stopping every concurrency-test thread. Preserve an original example failure over cleanup errors. https://community.openproject.org/wp/AGILE-362
This was referenced Aug 16, 2026
Contributor
There was a problem hiding this comment.
Pull request overview
This PR makes Backlogs destination actions (move to sprint/bucket/inbox) operate on the current ordered action scope (single card or batch selection), while keeping the server authoritative via destination intersection + revalidation under advisory locking.
Changes:
- Introduces server-side destination intersection (
DestinationAvailability) and updates dialog + menu endpoints to use orderedids[]batches. - Extends the atomic batch move service with destination lifecycle + append serialization locks to keep placement and availability checks atomic under concurrency.
- Updates the sortable-lists Stimulus stack to project destination candidates, freeze/submit action scopes, handle direct destination moves, and clear selection on successful move events.
Reviewed changes
Copilot reviewed 38 out of 38 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| modules/backlogs/spec/support/pages/backlog.rb | Adds feature helper APIs for batch selection, dialogs, and announcements. |
| modules/backlogs/spec/services/backlogs/work_packages/destination_availability_spec.rb | Unit coverage for destination intersection/authorization semantics. |
| modules/backlogs/spec/services/backlogs/work_packages/batch_update_service_spec.rb | Extends batch move specs for new locking and revalidation behavior. |
| modules/backlogs/spec/services/backlogs/work_packages/batch_update_service_concurrency_spec.rb | Adds concurrency specs validating serialization/locking guarantees. |
| modules/backlogs/spec/routing/backlogs/work_packages_routing_spec.rb | Updates routing for dialog endpoints to collection POST routes. |
| modules/backlogs/spec/requests/work_packages/move_collection_spec.rb | Adds request coverage for plural announcements + dialog staleness rejection. |
| modules/backlogs/spec/features/work_packages/batch_destination_menu_spec.rb | End-to-end Selenium coverage for batch destination menus/dialogs. |
| modules/backlogs/spec/controllers/backlogs/work_packages_controller_spec.rb | Updates controller specs for new dialog contracts + candidate passing. |
| modules/backlogs/spec/components/backlogs/work_package_card_menu_component_spec.rb | Updates menu component spec for destination metadata + POST dialog loading. |
| modules/backlogs/spec/components/backlogs/move_to_sprint_dialog_component_spec.rb | Updates dialog component spec to submit ordered ids[] collection. |
| modules/backlogs/spec/components/backlogs/move_to_bucket_dialog_component_spec.rb | Updates dialog component spec to submit ordered ids[] collection. |
| modules/backlogs/config/routes.rb | Moves sprint/bucket dialog routes to collection post. |
| modules/backlogs/config/locales/en.yml | Adds no-destination messages and plural moved announcements. |
| modules/backlogs/app/views/backlogs/backlog/show.html.erb | Clears selection on successful move events at the root controller. |
| modules/backlogs/app/services/backlogs/work_packages/destination_availability.rb | New service intersecting destinations across a batch + permissions. |
| modules/backlogs/app/services/backlogs/work_packages/batch_update_service.rb | Adds destination lifecycle + append locks and revalidates via DestinationAvailability. |
| modules/backlogs/app/controllers/backlogs/work_packages_controller.rb | Dialog endpoints now load/authorize ordered collections and render plural announcements. |
| modules/backlogs/app/components/backlogs/work_package_card_menu_component.rb | Menu now receives destination candidate ids and emits destination metadata. |
| modules/backlogs/app/components/backlogs/work_package_card_menu_component.html.erb | Renders destination actions as projected items with dialog POST forms and metadata. |
| modules/backlogs/app/components/backlogs/move_to_sprint_dialog_component.rb | Refactors dialog component to accept work package collections + controller-provided sprints. |
| modules/backlogs/app/components/backlogs/move_to_sprint_dialog_component.html.erb | Submits ids[] and shows selected count in the sprint dialog. |
| modules/backlogs/app/components/backlogs/move_to_bucket_dialog_component.rb | Refactors dialog component to accept work package collections + controller-provided buckets. |
| modules/backlogs/app/components/backlogs/move_to_bucket_dialog_component.html.erb | Submits ids[] and shows selected count in the bucket dialog. |
| frontend/src/stimulus/controllers/dynamic/sortable-lists/selection.ts | Adds ordered selected elements helper for action-scope projection. |
| frontend/src/stimulus/controllers/dynamic/sortable-lists/selection.spec.ts | Unit coverage for ordered selected element resolution. |
| frontend/src/stimulus/controllers/dynamic/sortable-lists/selection-orchestrator.ts | Adds action-scope APIs (prospective vs mutating) for menu/dialog invocation. |
| frontend/src/stimulus/controllers/dynamic/sortable-lists/selection-orchestrator.spec.ts | Covers action-scope semantics and anchor preservation. |
| frontend/src/stimulus/controllers/dynamic/sortable-lists/scrollable.controller.spec.ts | Updates root mocks for new destination/action-scope interface. |
| frontend/src/stimulus/controllers/dynamic/sortable-lists/list.controller.spec.ts | Updates root mocks for new destination/action-scope interface. |
| frontend/src/stimulus/controllers/dynamic/sortable-lists/list-dom.ts | Adds destination identity + intersection helpers for client-side projection. |
| frontend/src/stimulus/controllers/dynamic/sortable-lists/list-dom.spec.ts | Unit coverage for destination intersection and occupancy filtering. |
| frontend/src/stimulus/controllers/dynamic/sortable-lists/item.controller.ts | Projects destination items, freezes ids[] into dialog forms, and supports direct destination moves. |
| frontend/src/stimulus/controllers/dynamic/sortable-lists/item.controller.spec.ts | Extensive coverage for projection, dialog freezing, and destination activation. |
| frontend/src/stimulus/controllers/dynamic/sortable-lists/drag-and-drop.ts | Extends root interface with action-scope + destination move capabilities. |
| frontend/src/stimulus/controllers/dynamic/sortable-lists.controller.ts | Implements destination availability, direct destination moves, and busy-state recovery. |
| frontend/src/stimulus/controllers/dynamic/sortable-lists.controller.spec.ts | Covers action scopes, direct destination submission, and busy-state reconciliation. |
| frontend/src/stimulus/controllers/async-dialog.controller.ts | Reimplements async dialog loading using @rails/request.js + beforeLoad hook. |
| frontend/src/stimulus/controllers/async-dialog.controller.spec.ts | New unit coverage for beforeLoad behavior, loading state, and response handling. |
Suppressed comments (2)
frontend/src/stimulus/controllers/async-dialog.controller.ts:49
- Same as the click path: key activation uses
void this.triggerTurboStream()without a catch, which can lead to unhandled promise rejections on request failures or unexpected responses. Catch/log in the handler to fail gracefully.
private readonly handleKeydown = (event:Event):void => {
const keyboardEvent = event as KeyboardEvent;
if (keyboardEvent.key === 'Enter' || keyboardEvent.key === ' ') {
event.preventDefault();
void this.triggerTurboStream();
frontend/src/stimulus/controllers/async-dialog.controller.ts:106
handleOpenDialogalso callstriggerTurboStream()viavoid ...without handling rejections. If the request fails, this can likewise create an unhandled promise rejection. Catch/log here as well (or handle errors centrally intriggerTurboStream).
handleOpenDialog(event:CustomEvent<{ url:string }>):void {
// Trigger the dialog with custom URL
void this.triggerTurboStream(event.detail.url);
}
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+40
to
+43
| private readonly handleClick = (event:Event):void => { | ||
| event.preventDefault(); | ||
| void this.triggerTurboStream(); | ||
| }; |
Comment on lines
+275
to
+283
| # Resolve without policy scoping so completion, deletion, and reassignment | ||
| # all share the same model advisory identity. DestinationAvailability remains | ||
| # the authority after the lifecycle and row locks have refreshed state. | ||
| def raw_destination(target) | ||
| case target | ||
| in Backlogs::Target::SprintId | ||
| Sprint.assignable(project: batch_project, user:).exists?(id: target.list_id) | ||
| Sprint.find_by(id: target.list_id) | ||
| in Backlogs::Target::BucketId | ||
| BacklogBucket.for_project(batch_project).exists?(id: target.list_id) | ||
| BacklogBucket.find_by(id: target.list_id) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ticket
AGILE-362
This PR is stacked directly on #24778.
What are you trying to accomplish?
Make the Backlogs inbox, sprint, and backlog-bucket destination actions operate on the current ordered action scope while preserving existing one-card behavior.
The change:
What approach did you choose and why?
The client projects the live action scope onto the existing menu, but the server remains authoritative: collection dialog routes reload every submitted work package, intersect available destinations, and validate the chosen target again under the movement lock. Direct inbox and dialog moves share the existing atomic batch service.
Destination model advisory locks coordinate batch placement with sprint lifecycle operations, and a stable project/destination lock serializes concurrent append placement. The client keeps selection until successful reconciliation and restores its busy state on every result.
Follow-up work:
Merge checklist