Skip to content

[AGILE-392] Constrain batch drags containing confined work packages - #24858

Open
myabc wants to merge 5 commits into
implementation/AGILE-364-batch-action-menusfrom
implementation/AGILE-392-batch-drag-confinement
Open

[AGILE-392] Constrain batch drags containing confined work packages#24858
myabc wants to merge 5 commits into
implementation/AGILE-364-batch-action-menusfrom
implementation/AGILE-392-batch-drag-confinement

Conversation

@myabc

@myabc myabc commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Note

This PR is the tip of a stack. Please review and merge in this order: #24525#24778#24779#24780#24781 → this PR.

Ticket

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

What are you trying to accomplish?

Since #AGILE-278 a drag moves the whole selected batch, and a batch may span lists: #AGILE-181 allows a sparse selection across backlogs and sprints. A batch may also hold a confined member — a work package whose read-only status lets it reorder within its list but never leave it, per #AGILE-226.

The drag already intersected confinement across the batch, but resolved the answer against the wrong list: the physically dragged card's, not the confined member's. That was wrong in both directions.

  • Select a confined card in sprint A and a free card in bucket B, then drag the free card to a new position inside B. The drop was offered, indicators rendered, and a request fired that the server is guaranteed to reject. Atomicity held — the batch rolled back — but the interface offered a drop it could never accept.
  • The same batch was refused a drop into sprint A, which the server accepts (only the free member changes list) and which the batch destination menus of #AGILE-362 already offer.

Two confined members in different lists showed the same split: the menus correctly offered nothing, while the drag still permitted a reorder of either one, which would have dragged the other across.

After this change the drag and the menus answer from one rule. A batch never offers a drop any member refuses, and never refuses one they all accept.

What approach did you choose and why?

The two surfaces each carried their own copy of the rule, which is what let them drift. itemAcceptsDestination in sortable-lists/list-dom.ts is now the single per-item policy; permittedDestinations is rebuilt on it and loses its own fixed guard, which the policy's fixed arm subsumes.

The drag consults the same policy through dragPermittedLists on the root controller, which resolves the lists every batch member accepts at drag start and carries them on the Pragmatic payload as permittedListElementsnull when nothing restricts the drag, an empty array when nothing accepts it. confinementAllowsDrop becomes permittedListsAllowDrop and tests the target against that set rather than the dragged card's own list. sourceListElement and the root's ownerListElementOf existed only to feed the old predicate and are removed.

Naming is deliberately about permitted destinations rather than confinement, because #AGILE-357 needs the same question answered from workflow transitions: on a board, which columns a card may enter is not a property of confinement. That lands as one more arm of the policy, with neither consumer changing.

Two spec-support changes were needed to assert the refusal honestly, rather than to weaken the assertion. The Selenium driver performs a single pointer move, which yields exactly one dragover, and the drag engine paints its drop feedback on an animation frame. A cross-container refusal paints on dragenter and is visible to that snapshot; a refusal inside the dragged card's own list has no container to enter, so its mark lands later. expect_backlogs_drag_refused now reads the last container state across the whole event stream, and perform_native_drag gained an opt-in dwell: that only the without-move path passes.

Merge checklist

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

@myabc
myabc force-pushed the implementation/AGILE-392-batch-drag-confinement branch from bf063b8 to 9a400ca Compare August 20, 2026 14:44
@myabc
myabc requested a lite review from Copilot August 20, 2026 15:04
@myabc
myabc marked this pull request as ready for review August 20, 2026 15:04
@myabc myabc added this to the 17.9.x milestone Aug 20, 2026
@myabc myabc added javascript Pull requests that update Javascript code needs review feature pullpreview labels Aug 20, 2026
@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown

Deploying openproject with PullPreview

Field Value
Latest commit fe829fb
Job deploy
Status ✅ Deploy successful
Preview URL https://pr-24858-agile-392-batch-d-ip-128-140-89-31.my.opf.run:443

View logs

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

This PR aligns Backlogs batch-drag behavior with the same per-item destination policy used by batch destination menus, ensuring drags don’t offer drops the server will reject (and don’t refuse drops that should be allowed) when the selection contains confined work packages across lists.

Changes:

  • Centralizes per-item destination acceptance into itemAcceptsDestination, and rebuilds destination intersections on that policy.
  • Reworks drag payload gating from “confined + source list” to “permitted list elements” resolved across the whole batch at drag start.
  • Updates Backlogs feature specs and DnD helpers to more reliably observe refusal feedback (animation-frame timing / event stream).

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
spec/support/shared/drag_and_drop_helper_spec.rb Adds dwell: option to stabilize native drag event streams for assertions.
modules/backlogs/spec/support/pages/backlog.rb Updates Backlogs drag helpers and refusal assertions to track container state across events; threads dwell: through Selenium drag helper.
modules/backlogs/spec/features/work_packages/batch_move_spec.rb Adds feature specs covering batch moves with confined members across lists.
frontend/src/stimulus/controllers/dynamic/sortable-lists/scrollable.controller.spec.ts Updates mocks to the new root API (dragPermittedLists).
frontend/src/stimulus/controllers/dynamic/sortable-lists/list.controller.ts Switches list indicator gating to permittedListsAllowDrop.
frontend/src/stimulus/controllers/dynamic/sortable-lists/list.controller.spec.ts Updates test payloads to use permittedListElements.
frontend/src/stimulus/controllers/dynamic/sortable-lists/list-dom.ts Introduces itemAcceptsDestination and uses it to compute permittedDestinations.
frontend/src/stimulus/controllers/dynamic/sortable-lists/list-dom.spec.ts Adds unit coverage for itemAcceptsDestination.
frontend/src/stimulus/controllers/dynamic/sortable-lists/item.controller.ts Emits permittedListElements on the drag payload instead of confinement/source-list fields.
frontend/src/stimulus/controllers/dynamic/sortable-lists/item.controller.spec.ts Updates DnD payload and acceptance tests to the new permitted-lists model.
frontend/src/stimulus/controllers/dynamic/sortable-lists/drag-and-drop.ts Replaces confinement checks with permittedListsAllowDrop and updates payload shape.
frontend/src/stimulus/controllers/dynamic/sortable-lists/drag-and-drop.spec.ts Updates helper specs for the new payload shape.
frontend/src/stimulus/controllers/dynamic/sortable-lists.controller.ts Adds dragPermittedLists to compute permitted lists across batch members using the shared policy.
frontend/src/stimulus/controllers/dynamic/sortable-lists.controller.spec.ts Extends root controller specs to validate permitted-lists behavior across multi-list batches.

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

Comment thread frontend/src/stimulus/controllers/dynamic/sortable-lists.controller.ts Outdated
@myabc myabc removed the pullpreview label Aug 20, 2026
Two surfaces decide whether a move may land somewhere: the action
menus project the answer over a batch, and a drag resolves it per
container. Each carries its own copy of the rule, free to drift.
Naming the policy once gives the drag something to consult, and a
board gating moves by workflow a single arm to add.

https://community.openproject.org/wp/AGILE-392
@myabc
myabc force-pushed the implementation/AGILE-392-batch-drag-confinement branch from 9a400ca to 078374e Compare August 20, 2026 17:07
@myabc
myabc requested a balanced review from Copilot August 20, 2026 17:09

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

Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.

Suppressed comments (1)

frontend/src/stimulus/controllers/dynamic/sortable-lists.controller.ts:315

  • This confinement-specific early return bypasses itemAcceptsDestination, so the drag is not actually driven solely by the shared per-item policy. In particular, the PR description says AGILE-357 can add a workflow restriction as another policy arm without changing either consumer, but a batch with no confined members will still return null here and accept every drag destination. Please evaluate the registered destinations through itemAcceptsDestination for every batch, and derive null only when that evaluation leaves all destinations unrestricted.
    const members = [itemElement, ...this.prospectiveDragMateElements(itemElement)];
    if (members.every((member) => !isConfinedItem(member))) {
      return null;

myabc added 4 commits August 20, 2026 18:24
A batch may span lists, so the list the dragged card sits in answers
for that card alone. Resolving the whole batch's permitted lists at
drag start stops the interface offering a drop a batch-mate cannot
make, and stops it refusing one the menus already offer.

https://community.openproject.org/wp/AGILE-392
Nothing reads it now that a drop asks the batch's permitted lists
instead of the dragged card's own, and the root method that filled
it existed for no other caller.

https://community.openproject.org/wp/AGILE-392
A refusal inside the dragged card's own list has no container to
enter, so the engine paints it on a later animation frame than the
one dragover a single pointer move produces. Reading the whole event
stream, and dwelling over the target, makes it observable.
Covers the sparse batch the drag used to get wrong in both
directions: a drop inside the dragged card's own list is refused
while a batch-mate is confined elsewhere, and the list that mate
occupies accepts the whole block.

https://community.openproject.org/wp/AGILE-392
@myabc
myabc force-pushed the implementation/AGILE-392-batch-drag-confinement branch from 078374e to fe829fb Compare August 20, 2026 17:24
@github-actions

Copy link
Copy Markdown

Warning

Flaky specs

  • rspec ./spec/features/work_packages/table/switch_types_spec.rb[1:1:1]
🤖 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 #24858, linked for reference only):

- `rspec ./spec/features/work_packages/table/switch_types_spec.rb[1:1:1]`

Treat this as a standalone task, unrelated to PR #24858. Create a new branch from origin/dev and open a new pull request targeting dev — do not stack it on PR #24858 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.

@EinLama EinLama 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.

Works like a charm! Tested multiple scenarios and could never break it. Code looks great. I had one particular function that made me pause for a bit. Please have a look 🔍

Approving since even if this was a valid point, it's not a blocker.

Comment on lines +521 to +522
permittedListElements: this.root?.dragPermittedLists(this.element)
?? (itemMobility(this.element) === 'free' ? null : []),

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.

Shouldn't this rather check for this.root?.dragPermittedLists(this.element) === false explicitly? A returned null means "no restrictions" here if I understand it correctly. In that case, the itemMobility check is always executed - which should come to the same conclusion, but it's an unnecessary hop into that function.

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

Labels

feature javascript Pull requests that update Javascript code needs review pullpreview

Development

Successfully merging this pull request may close these issues.

3 participants