Skip to content

[AGILE-278] Move selected work packages atomically - #24778

Draft
myabc wants to merge 9 commits into
fix/pragmatic-autoscroll-spec-fixturesfrom
implementation/AGILE-278-batch-move
Draft

[AGILE-278] Move selected work packages atomically#24778
myabc wants to merge 9 commits into
fix/pragmatic-autoscroll-spec-fixturesfrom
implementation/AGILE-278-batch-move

Conversation

@myabc

@myabc myabc commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Ticket

https://community.openproject.org/wp/AGILE-278

What are you trying to accomplish?

Dragging a selected Backlogs card now moves the complete batch selection as one ordered, atomic operation; dragging an unselected card keeps the existing collapse-and-move-alone behavior. This is the batch-movement slice of #AGILE-181, building on the selection capability from PR #24525.

  • A new collection action (PUT projects/:id/backlogs/work_packages/move) accepts ordered ids[] with a three-state prev_id (after an anchor, top, append) and rejects blanks, duplicates, unresolvable members, invalid targets, and stale or in-batch predecessors outright.
  • Backlogs::WorkPackages::BatchUpdateService chains each member after the previously moved one inside one outer transaction, acquires advisory locks in ascending id order across the batch and its anchor, and revalidates both the anchor and the project cohort under lock — the batch commits as one contiguous block or not at all, and after-commit hooks only ever observe the completed batch.
  • The client freezes the drag's batch at drag start, resolves the drop against the excluded selected ids, moves all rows as one optimistic block with a single guarded rollback boundary, marks every represented row as dragging, and badges the preview with the batch count.
  • Same-list success skips the frame reload only after the persisted rows verifiably form the requested block; cross-list or divergent results reconcile via reload. Success clears the selection, failure preserves it, and an unverifiable rollback announces a check-your-positions warning even when the server's 422 flash is otherwise silent.
  • The moved Turbo event carries ordered work_package_ids from the collection action while the member action keeps its scalar, and the split-view synchronizer consumes both.

Screenshots

Visual changes (multi-row dragging treatment, batch count badge on the drag preview) have unit coverage but no design pass yet — screenshots to follow after a manual QA round.

What approach did you choose and why?

The collection action is deliberately not an overloaded member action: existing singular callers (menu moves, non-selection sortable roots) keep the member route untouched, while a selection-enabled root submits one or many cards through the collection contract. The service wraps the existing single-work-package UpdateService per member rather than reimplementing the move, so validation and journaling stay in one place; a real BatchFailure exception (never ActiveRecord::Rollback, which joined transactions swallow) carries the failed result out of the transaction. Advisory locks are pre-acquired in canonical ascending-id order because each inner update takes the same per-work-package lock with an unbounded wait — opposing batches would otherwise deadlock. Two subtle acts_as_list staleness bugs surfaced during review and are covered by regression tests: in-memory positions feeding remove_from_list thresholds mid-batch (which could persist duplicate positions), and after-commit hook contexts observing interim positions.

On the client, the frozen batch lives in root-owned state consumed exactly once per drop, so Escape or a Turbo morph mid-drag can never change what is submitted, and the drag preview freezes the batch itself because Pragmatic renders previews before onDragStart fires. Keep-batch-on-menu-invocation, positional batch actions, and batch destination dialogs are follow-up slices (#AGILE-362, #AGILE-363, #AGILE-364).

Merge checklist

  • Added/updated tests
  • Added/updated documentation in Lookbook (patterns, previews, etc)
  • Tested major browsers (Chrome, Firefox, Edge, ...)

myabc added 5 commits August 15, 2026 22:12
Adds PUT projects/:id/backlogs/work_packages/move taking ordered
ids and a three-state prev_id (after an anchor, top, or append). A
batch service chains each member after the previously moved one
inside one outer transaction, acquires advisory locks in ascending
id order, and revalidates the anchor and the project cohort under
lock, so a batch commits as one contiguous block or not at all and
after-commit hooks only ever observe the completed batch. The
optimistic response skips the frame reload only once the persisted
rows verifiably form the requested block, and the moved event
carries the ordered ids.

https://community.openproject.org/wp/AGILE-278
Dragging a selected card now moves the whole selection: the root
freezes the batch at drag start, resolves the drop against the
excluded selected ids so no member can anchor its own insertion,
reorders every row as one block, and submits ordered ids to a
root-configured collection URL for one card or many. Every
represented row carries the dragging treatment and batches show a
count on the preview, which renders before drag start and so
freezes the batch itself. Success clears the selection, an
unverifiable rollback warns even when the server flash stays
silent, and move announcements speak the consumer's vocabulary
through a dedicated scope value.

https://community.openproject.org/wp/AGILE-278
Iterates the ordered work_package_ids from the collection event
and falls back to the singular field, because no single scalar can
name whichever batch member the split view has open.

https://community.openproject.org/wp/AGILE-278
Adds the collection move URL and announcement scope values to the
Backlogs root, with work-package wording for every announcement
the shared controller can speak.

https://community.openproject.org/wp/AGILE-278
Proves the ordered cross-list block, the collapse-and-move-alone
path, atomic rejection with preserved selection, and the reload-
free optimistic same-list reorder against a real browser and
database.

https://community.openproject.org/wp/AGILE-278

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements atomic, ordered batch movement of Backlogs work packages when dragging a selected card, via a new collection move endpoint and corresponding client-side batch drag behavior. This extends the existing sortable-lists selection foundation by adding a batch-aware move contract, server-side locking/validation for correctness under concurrency, and updated announcements/UX affordances (dragging marks + preview badge).

Changes:

  • Added PUT /projects/:id/backlogs/work_packages/move (collection action) and server-side Backlogs::WorkPackages::BatchUpdateService to move ordered batches atomically with advisory locks and revalidation under lock.
  • Updated sortable-lists Stimulus controllers to freeze the drag batch at drag start, submit ordered ids[] to the collection endpoint, exclude selected ids when resolving prev_id, and handle optimistic rollback/announcements for batch moves.
  • Added/updated routing, request, service, and Selenium feature specs; extended i18n strings and internal developer docs (frontend AGENTS).

Reviewed changes

Copilot reviewed 31 out of 31 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
modules/backlogs/spec/support/pages/backlog.rb Adds a drag helper tailored for rejected moves (no frame reload).
modules/backlogs/spec/services/backlogs/work_packages/batch_update_service_spec.rb New spec coverage for batch move service behavior (atomicity, locks, staleness).
modules/backlogs/spec/routing/backlogs/work_packages_routing_spec.rb Verifies routing for the new collection move endpoint.
modules/backlogs/spec/requests/work_packages/move_collection_spec.rb New request specs for validation, success/failure responses, and invisibility flashes.
modules/backlogs/spec/requests/backlogs/backlog_spec.rb Ensures the Backlogs page includes collection move URL + announcement scope values.
modules/backlogs/spec/features/work_packages/batch_move_spec.rb New Selenium feature spec covering batch drag, collapse behavior, and failure preservation.
modules/backlogs/lib/open_project/backlogs/engine.rb Grants permission for the new move_collection action.
modules/backlogs/config/routes.rb Adds the collection move route mapped to move_collection.
modules/backlogs/config/locales/js-en.yml Adds Backlogs-specific move announcement strings for batch/singular moves.
modules/backlogs/config/locales/en.yml Adds server-side batch move error messages and plural invisible-after-move notice.
modules/backlogs/app/views/backlogs/backlog/show.html.erb Wires new Stimulus values (collection move URL + move announcement scope) into the view.
modules/backlogs/app/services/backlogs/work_packages/batch_update_service.rb New atomic batch move service chaining member moves under locks/transaction.
modules/backlogs/app/controllers/backlogs/work_packages_controller.rb Adds move_collection action, validation/loading of ordered batch ids, and optimistic-skip logic.
frontend/src/stimulus/controllers/dynamic/sortable-lists/selection-orchestrator.ts Adds drag batch snapshotting and post-success silent selection clearing.
frontend/src/stimulus/controllers/dynamic/sortable-lists/selection-orchestrator.spec.ts Unit tests for batch snapshotting and silent clear behavior.
frontend/src/stimulus/controllers/dynamic/sortable-lists/scrollable.controller.spec.ts Updates root port mocks to match new drag-batch API.
frontend/src/stimulus/controllers/dynamic/sortable-lists/preview.ts Adds a batch-count badge overlay to the drag preview for multi-row drags.
frontend/src/stimulus/controllers/dynamic/sortable-lists/preview.spec.ts Tests preview badge behavior for batch vs single drags.
frontend/src/stimulus/controllers/dynamic/sortable-lists/list.controller.spec.ts Updates root port mocks to match new drag-batch API.
frontend/src/stimulus/controllers/dynamic/sortable-lists/list-dom.ts Updates append-resolution helper to exclude a set of ids (batch-aware).
frontend/src/stimulus/controllers/dynamic/sortable-lists/list-dom.spec.ts Updates and extends tests for excluded-id append resolution.
frontend/src/stimulus/controllers/dynamic/sortable-lists/item.controller.ts Begins drag batch on preview and drag start; passes batch size to preview renderer.
frontend/src/stimulus/controllers/dynamic/sortable-lists/item.controller.spec.ts Tests preview badge behavior and beginDragBatch invocation ordering.
frontend/src/stimulus/controllers/dynamic/sortable-lists/drag-and-drop.ts Adds ids[] payload support and predecessor resolution that excludes selected/batch ids.
frontend/src/stimulus/controllers/dynamic/sortable-lists/drag-and-drop.spec.ts Tests batch form payload and excluded-id predecessor resolution.
frontend/src/stimulus/controllers/dynamic/sortable-lists.controller.ts Implements frozen drag batch lifecycle, batch row marking/cleanup, collection move submission, and batch announcements/failure handling.
frontend/src/stimulus/controllers/dynamic/sortable-lists.controller.spec.ts Extensive unit coverage for batch drag/drop behavior, requests, cleanup, and announcements.
frontend/src/stimulus/controllers/dynamic/backlogs/split-view-sync.controller.ts Consumes both singular and batch moved events and refreshes cached work packages accordingly.
frontend/src/stimulus/controllers/dynamic/backlogs/split-view-sync.controller.spec.ts Tests batch event consumption, uncached skipping, and no-id events.
frontend/AGENTS.md Updates internal documentation to reflect new batch drag + move announcement scope behavior.
config/locales/js-en.yml Adds generic sortable-lists batch announcement strings for non-Backlogs consumers.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread modules/backlogs/app/services/backlogs/work_packages/batch_update_service.rb Outdated
@myabc myabc added feature DO NOT MERGE javascript Pull requests that update Javascript code pullpreview labels Aug 15, 2026
@github-actions

github-actions Bot commented Aug 15, 2026

Copy link
Copy Markdown

Deploying openproject with PullPreview

Field Value
Latest commit 2ed9b04
Job deploy
Status ✅ Deploy successful
Preview URL https://pr-24778-agile-278-batch-m-ip-128-140-89-31.my.opf.run:443

View logs

@github-actions

Copy link
Copy Markdown

Warning

Flaky specs

  • rspec ./modules/team_planner/spec/features/query_handling_spec.rb[1:3:1:1]
  • rspec ./spec/features/work_packages/table/switch_types_spec.rb[1:1:1]
  • rspec ./spec/features/work_packages/table/switch_types_spec.rb[1:1:2]
🤖 Ask Copilot to investigate

Copy 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.

@copilot The following spec(s) are flaky in CI (first seen on PR #24778, linked for reference only):

- `rspec ./modules/team_planner/spec/features/query_handling_spec.rb[1:3:1:1]`
- `rspec ./spec/features/work_packages/table/switch_types_spec.rb[1:1:1]`
- `rspec ./spec/features/work_packages/table/switch_types_spec.rb[1:1:2]`

Treat this as a standalone task, unrelated to PR #24778. Create a new branch from origin/dev and open a new pull request targeting dev — do not stack it on PR #24778 or reuse that branch.

Follow the playbook in docs/development/testing/handling-flaky-tests/README.md to find the root cause and fix the underlying race — do not skip, delete, or weaken the spec to make it pass; disabling is a last resort per the playbook, and only with a bug ticket. Verify the fix by running the spec(s) repeatedly (e.g. `script/bulk_run_rspec --run-count 10`).

If you cannot reproduce the flake or are not confident in a fix after reasonable investigation, do not fabricate a change or skip the spec to force CI green. Instead, leave the pull request in draft and document what you tried, the suspected cause, and any leads in its description, then assign @myabc to take over.

Once the fix is verified, title the PR after the spec(s) it fixes, and use the PR description to explain the root cause, how the change resolves it, and the before/after results. Label the PR `flaky-spec`, assign @myabc, and request a review from @myabc.
On every commit, set @myabc as the sole co-author with a `Co-authored-by:` trailer (use their GitHub no-reply email so it links to their account), so it is traceable who dispatched the fix.

Reuses the Counter contract at the CSS level so the badge is
stylesheet-targetable instead of carrying a one-off inline skin,
rendered with lit-html per existing precedent. Firefox folds
overflow into its drag-image snapshot and misaligns the grab
offset, so the badge gets an inset, shadowless variant there.

https://community.openproject.org/wp/AGILE-278
@myabc
myabc force-pushed the implementation/AGILE-278-batch-move branch from a31087a to 320b49b Compare August 15, 2026 21:53
myabc added 3 commits August 15, 2026 23:05
Adds a target-availability check under the batch move's lock,
since the contract's sprint_id/backlog_bucket_id change guard
never trips for a same-list reorder, leaving a sprint that
completed (or a bucket reassigned) after page load still
silently accepted.

https://community.openproject.org/wp/AGILE-278
A Turbo morph mid-drag can replace a batch-mate's row with a
fresh element that never went through beginDragBatch, so it
loses the dragging treatment while the rest of the frozen batch
keeps it. The registration heal now re-applies the mark to every
row the drag still represents.

https://community.openproject.org/wp/AGILE-278
Recursing on entries.drop(1) allocated a fresh array per lock
acquired, making the traversal quadratic in batch size. Recurses
on an index into the same array instead.

https://community.openproject.org/wp/AGILE-278
drag_backlogs_item(source: moved_element, target: target_element, edge: :bottom)
end
rescue Capybara::Cuprite::ObsoleteNode, Selenium::WebDriver::Error::StaleElementReferenceError
retry

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unbounded!

Comment on lines +87 to +88
{ "backlogs/work_packages": %i[move move_collection move_to_sprint_dialog move_to_bucket_dialog
add_existing_dialog add_existing] },

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: single line per item

# prev_id → no anchor), :append (absent prev_id → the last non-batch
# member of the target, resolved NOW so it participates in the lock set
# and can be revalidated under lock; nil anchor means an empty target).
Placement = Struct.new(:mode, :anchor) do

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Data.define

@github-actions

Copy link
Copy Markdown

Warning

Flaky specs

  • rspec ./spec/features/projects/creation_wizard/wizard_from_template_flow_spec.rb[1:1]
  • rspec ./spec/features/work_packages/table/queries/parent_field_ranking_spec.rb[1:1]
  • rspec ./spec/features/work_packages/table/switch_types_spec.rb[1:1:1]
  • rspec ./spec/features/work_packages/table/switch_types_spec.rb[1:1:2]
🤖 Ask Copilot to investigate

Copy 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.

@copilot The following spec(s) are flaky in CI (first seen on PR #24778, linked for reference only):

- `rspec ./spec/features/projects/creation_wizard/wizard_from_template_flow_spec.rb[1:1]`
- `rspec ./spec/features/work_packages/table/queries/parent_field_ranking_spec.rb[1:1]`
- `rspec ./spec/features/work_packages/table/switch_types_spec.rb[1:1:1]`
- `rspec ./spec/features/work_packages/table/switch_types_spec.rb[1:1:2]`

Treat this as a standalone task, unrelated to PR #24778. Create a new branch from origin/dev and open a new pull request targeting dev — do not stack it on PR #24778 or reuse that branch.

Follow the playbook in docs/development/testing/handling-flaky-tests/README.md to find the root cause and fix the underlying race — do not skip, delete, or weaken the spec to make it pass; disabling is a last resort per the playbook, and only with a bug ticket. Verify the fix by running the spec(s) repeatedly (e.g. `script/bulk_run_rspec --run-count 10`).

If you cannot reproduce the flake or are not confident in a fix after reasonable investigation, do not fabricate a change or skip the spec to force CI green. Instead, leave the pull request in draft and document what you tried, the suspected cause, and any leads in its description, then assign @myabc to take over.

Once the fix is verified, title the PR after the spec(s) it fixes, and use the PR description to explain the root cause, how the change resolves it, and the before/after results. Label the PR `flaky-spec`, assign @myabc, and request a review from @myabc.
On every commit, set @myabc as the sole co-author with a `Co-authored-by:` trailer (use their GitHub no-reply email so it links to their account), so it is traceable who dispatched the fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

DO NOT MERGE feature javascript Pull requests that update Javascript code pullpreview

Development

Successfully merging this pull request may close these issues.

2 participants