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
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
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.
Ignores submenu overlays when projecting divider visibility and keeps position children consistent with their parent. Filters menu toggles to the primary popover and coalesces deferred target refreshes, with coverage for early opening, disconnect and singleton recovery.
Rejects an append when an independent writer fills its initially empty target before validation. Covers the interleaving with an independent connection and preserves the remaining AGILE-426 race after validation.
Shares destination acceptance between menus and dragging and rejects malformed metadata with a bounded debug diagnostic. Extracts menu projection and the shared action scope while retaining lifecycle wiring and guarded activation in the item controller.
Shares candidate loading and member acceptance while preserving the menu candidate universe. Rejects unauthorized targets before movement locks and checks fresh availability under lock. Preloads rendered types and statuses used by the shared policy.
Extracts ordered transaction-scoped lock acquisition while retaining service-owned validation and placement. Moves target lock identity and class-level wire types beside their target representations.
Uses the shared progress wrapper for direct destinations and dialog loading. Preserves cleanup and async-dialog rejection behavior while including HTTP status in non-stream request errors.
3f243c8 to
a868c2d
Compare
Shares ordered ID validation and member/batch result handling. Tests rendered dialog forms and association query counts, and removes unused builder and persistence helpers. Preserves singular feedback and uses explicit translation keys.
Adds selected bucket and inbox moves and a shrinking Shift range through the sprint dialog. Uses contextual fixtures, rendered persistence and announcements, and checks the divider in the same menu opening.
Uses OpPrimer::ListComponent to expose each selected work package as a list item while preserving the heading and selection order.
Removes the unused root enumeration wrapper after its orchestrator counterpart was removed. Retains the complexity allowance needed by the destination preflight added above the transaction helper.
a868c2d to
cb32f24
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. |
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 selection while preserving existing one-card behavior. Invoking an action from an unselected card replaces the previous selection with that card.
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 ordered selection onto the existing menu, while the server remains authoritative.
DestinationAvailabilitysupplies the candidate universe for menu markup and filters the full batch for dialog choices. Candidates are not filtered solely by the invoking card: another selected card may validly enter the invoker's current list.Dialog routes validate the ordered IDs before loading, preload the type and status associations, and render the affected work packages. Direct inbox and dialog moves share the atomic batch service. A permission/candidacy preflight rejects unavailable targets before movement locks; a separate, fresh availability check runs under those locks. Source and destination lifecycle locks use stable ordering, and project/destination locks serialize concurrent append placement.
The empty-append guard rejects a batch if an independent writer fills an initially empty target between placement resolution and revalidation. It does not close the remaining member-write race after revalidation; that remains AGILE-426. Existing member positional and add-existing callers remain supported.
Menu projection and destination acceptance are shared rather than duplicated. Refreshes are limited to the primary menu's opening toggle and coalesced when deferred targets connect; tooltip and submenu toggles do not trigger a full refresh. Destination requests use the shared progress wrapper with guaranteed cleanup. Malformed destination metadata fails closed with a dev-only diagnostic that omits the payload.
The dialogs reuse the inset
WorkPackages::InfoLineComponenttreatment behindBacklogs::SelectedWorkPackagesComponent. The affected-work-package list is a sibling of the form, and the dialog body scrolls for long selections. A hidden selected count remains associated with the destination select for assistive technology.Follow-up work:
Merge checklist
prev_idto append; blank still means top. AGILE-426 remains separate. Review discussion.dropRouteForretains the collection-URL-only drop decision, including one-member drops; destination moves retainresolveCollectionMoveUrl(false). Review discussion.