[AGILE-392] Constrain batch drags containing confined work packages - #24858
[AGILE-392] Constrain batch drags containing confined work packages#24858myabc wants to merge 5 commits into
Conversation
bf063b8 to
9a400ca
Compare
Deploying openproject with ⚡ PullPreview
|
There was a problem hiding this comment.
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.
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
9a400ca to
078374e
Compare
There was a problem hiding this comment.
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 returnnullhere and accept every drag destination. Please evaluate the registered destinations throughitemAcceptsDestinationfor every batch, and derivenullonly when that evaluation leaves all destinations unrestricted.
const members = [itemElement, ...this.prospectiveDragMateElements(itemElement)];
if (members.every((member) => !isConfinedItem(member))) {
return null;
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
078374e to
fe829fb
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. |
EinLama
left a comment
There was a problem hiding this comment.
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.
| permittedListElements: this.root?.dragPermittedLists(this.element) | ||
| ?? (itemMobility(this.element) === 'free' ? null : []), |
There was a problem hiding this comment.
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.
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.
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.
itemAcceptsDestinationinsortable-lists/list-dom.tsis now the single per-item policy;permittedDestinationsis rebuilt on it and loses its ownfixedguard, which the policy'sfixedarm subsumes.The drag consults the same policy through
dragPermittedListson the root controller, which resolves the lists every batch member accepts at drag start and carries them on the Pragmatic payload aspermittedListElements—nullwhen nothing restricts the drag, an empty array when nothing accepts it.confinementAllowsDropbecomespermittedListsAllowDropand tests the target against that set rather than the dragged card's own list.sourceListElementand the root'sownerListElementOfexisted 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 ondragenterand 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_refusednow reads the last container state across the whole event stream, andperform_native_draggained an opt-indwell:that only the without-move path passes.Merge checklist