[AGILE-362] Make Backlogs destination actions batch-aware - #24779
Conversation
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.
5895d3e to
9c1af86
Compare
f800d44 to
d7e10f7
Compare
|
Warning Flaky specs
🤖 Ask Copilot to investigateCopy the prompt below into a new comment on this PR to delegate the investigation to GitHub Copilot. It will look into the flakiness and open a separate pull request with you as reviewer. |
d7e10f7 to
7a8768a
Compare
|
Warning Flaky specs
🤖 Ask Copilot to investigateCopy the prompt below into a new comment on this PR to delegate the investigation to GitHub Copilot. It will look into the flakiness and open a separate pull request with you as reviewer. |
7a8768a to
d7e10f7
Compare
d7e10f7 to
fdc3e4e
Compare
|
Warning Flaky specs
🤖 Ask Copilot to investigateCopy the prompt below into a new comment on this PR to delegate the investigation to GitHub Copilot. It will look into the flakiness and open a separate pull request with you as reviewer. |
fdc3e4e to
41fa8e2
Compare
41fa8e2 to
65025d0
Compare
| availability = Backlogs::WorkPackages::DestinationAvailability.new( | ||
| project: batch_project, | ||
| user:, | ||
| work_packages: WorkPackage.where(id: work_packages.map(&:id)).to_a | ||
| ) | ||
| raise BatchFailure, unavailable_target_failure unless availability.permitted?(target) |
|
Warning Flaky specs
🤖 Ask Copilot to investigateCopy the prompt below into a new comment on this PR to delegate the investigation to GitHub Copilot. It will look into the flakiness and open a separate pull request with you as reviewer. |
46ec91f to
0adff93
Compare
0adff93 to
0154d4b
Compare
0154d4b to
0bf78a8
Compare
Adds live action scopes for ordered destination batches while keeping selection state out of the rendered menu. Intersects candidates across mobility and list ownership. https://community.openproject.org/wp/AGILE-362
Projects server candidates from the live action scope. Omits unavailable and fully occupied destinations while covering deferred, confined, and fixed-card projection. https://community.openproject.org/wp/AGILE-362
Adds a cancelable pre-load hook and form-backed POST support. Sends ordered IDs without encoding batches in cacheable URLs while preserving request.js CSRF and Turbo Stream handling. https://community.openproject.org/wp/AGILE-362
Reloads every member and intersects authoritative destinations. Rejects stale, invisible, foreign, or mixed batches atomically and shares the same destination policy with under-lock submission checks. https://community.openproject.org/wp/AGILE-362
Renders dialogs from ordered work-package batches and consumes controller-filtered destination collections. Keeps form IDs ordered and removes a redundant project input. https://community.openproject.org/wp/AGILE-362
Submits direct and dialog moves through the collection endpoint while preserving ordered IDs and retry selection. Tracks in-flight movement across disconnect and reconnect to prevent duplicate requests and stale busy state. https://community.openproject.org/wp/AGILE-362
Renders live-region feedback from the authoritative response while suppressing misleading positions for invisible results. https://community.openproject.org/wp/AGILE-362
Exercises ordered destination batches through Selenium, covering confinement, stale menus, rejection, and selection cleanup. Rejects every visible modal in the disappearing-target case. https://community.openproject.org/wp/AGILE-362
Serializes append placement with destination lifecycle changes and locks target rows before rechecking authority. Batches read-only status queries and cleans up concurrency tests without masking original failures. https://community.openproject.org/wp/AGILE-362
Serializes unanchored placement before resolving target state. Orders source and target lifecycle mutexes before item locks and revalidates each captured source under lock. https://community.openproject.org/wp/AGILE-362
Preserves the retry batch while a request owns movement. Consumes selection Escape only after overlays and empty state are handled. https://community.openproject.org/wp/AGILE-362
Shares the two matching non-optimistic response paths while keeping caller-owned cleanup and #AGILE-393 behavior unchanged. Deduplicates owner identity without merging policy layers. https://community.openproject.org/wp/AGILE-362
Updates the inherited sprint-completion race to assert the shared unavailable-destination policy used by collection moves. https://community.openproject.org/wp/AGILE-362
Replaces the persisted-order database checks with a reload and a rendered-order assertion, so the spec proves persistence by what the user sees surviving the reload. Payload and ordering semantics remain covered by the controller unit specs and the request specs.
The move dialogs stated only how many work packages were selected, so a batch assembled by rubber band or shift-click could not be checked before committing to the move. They now list every affected work package, reusing the treatment the work package delete dialog already gives its descendants.
"Selected items" named neither how many work packages a move would touch nor what they were. A pluralised heading states both, and carrying the description id makes the visible text the destination select's description instead of a duplicate hidden node.
A long selection outgrows the 320px medium dialog and scrolls behind an unmarked footer, leaving Cancel and Move floating over the list.
The suites pin the platform to Windows, where the selection orchestrator reads Ctrl alone and treats a Meta-modified click as a plain one, so the scope and destination examples were collapsing their own batches. Builds them through Ctrl, as the selection specs do.
0bf78a8 to
7632202
Compare
Note
This PR is part of a stack. Please review and merge in this order: #24525 → #24778 → this PR.
Ticket
https://community.openproject.org/wp/AGILE-362
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:
The dialog listing delivers the "list affected elements" half of AGILE-374 for the two move dialogs.
Screenshots
|
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.
The dialogs reuse the treatment
WorkPackages::DeleteDialogComponentalready gives its descendant list — an inset box ofWorkPackages::InfoLineComponentrows — behind a sharedBacklogs::SelectedWorkPackagesComponent. The list is a sibling of the form rather than a field inside it, and the dialog's own scrolling body carries a long selection. The selected count moves to a hidden element that the destination select keeps describing itself by, so replacing the visible copy costs assistive technology nothing.Follow-up work:
Merge checklist