[AGILE-392] Constrain batch drags containing confined work packages - #24858
[AGILE-392] Constrain batch drags containing confined work packages#24858myabc wants to merge 2 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.
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;
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.
|
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. |
09d4c77 to
c1d4132
Compare
c1d4132 to
1d788ce
Compare
1d788ce to
1510c7c
Compare
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
Treats a root's null permitted-lists result as the authoritative unrestricted answer instead of falling back to the dragged item's mobility. Keeps the mobility fallback for rootless items, covering the distinction with a regression test. https://community.openproject.org/wp/AGILE-392
1510c7c to
9fd9bb4
Compare
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