[AGILE-251] Migrate Backlogs to Pragmatic DnD#23218
Conversation
8ac3e26 to
9994516
Compare
Deploying openproject with ⚡ PullPreview
|
9994516 to
4a84694
Compare
There was a problem hiding this comment.
Pull request overview
This PR migrates Backlogs drag-and-drop from the legacy approach to Atlassian’s Pragmatic Drag and Drop, moving Stimulus wiring onto the inner work package card and introducing new frontend helpers/controllers to compute drop targets and ordering.
Changes:
- Introduces Pragmatic DnD-based Stimulus controllers/helpers for Backlogs item dragging and list drop targeting.
- Moves Backlogs-specific data attributes (story + draggable item wiring) from BorderBox rows to the inner work package card, and updates Backlogs views/components accordingly.
- Adds Atlaskit Pragmatic DnD dependencies and updates Ruby/JS specs to reflect the new DOM + data attributes.
Reviewed changes
Copilot reviewed 24 out of 25 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| spec/components/open_project/common/work_package_card_component_spec.rb | Adds coverage for forwarding system arguments to the root card element. |
| modules/backlogs/spec/support/pages/backlog.rb | Updates test selectors to target the new draggable item data attribute on the card. |
| modules/backlogs/spec/requests/backlogs/backlog_spec.rb | Adds assertions for Backlogs controller wiring and list targets in rendered backlog HTML. |
| modules/backlogs/spec/components/backlogs/work_package_card_list_item_component_spec.rb | Adjusts expectations to verify Backlogs Stimulus wiring is on the card, not the row. |
| modules/backlogs/spec/components/backlogs/work_package_card_list_component_spec.rb | Updates list/component specs for new target attributes and card-level data wiring. |
| modules/backlogs/spec/components/backlogs/story_points_component_spec.rb | Adds expectation around wrapper positioning for SR-only label placement. |
| modules/backlogs/spec/components/backlogs/sprint_component_spec.rb | Updates sprint DnD expectations to use Backlogs list targets + card-level draggable attributes. |
| modules/backlogs/spec/components/backlogs/inbox_component_spec.rb | Updates inbox drop-target assertions to new Backlogs list target attributes. |
| modules/backlogs/spec/components/backlogs/bucket_component_spec.rb | Updates bucket drop-target + card wiring assertions for new DnD approach. |
| modules/backlogs/app/views/backlogs/backlog/show.html.erb | Removes legacy content_controller wiring for Backlogs. |
| modules/backlogs/app/views/backlogs/backlog/_backlog_list.html.erb | Replaces legacy generic-drag-and-drop wiring with data-controller="backlogs" root. |
| modules/backlogs/app/components/backlogs/work_package_card_list_item_component.rb | Moves Backlogs Stimulus wiring/data to the inner card; adds draggable card args. |
| modules/backlogs/app/components/backlogs/work_package_card_list_component.rb | Changes list target data attributes to data-backlogs-target="list" + id. |
| modules/backlogs/app/components/backlogs/work_package_card_component.rb | Forwards system arguments to the shared/common work package card. |
| modules/backlogs/app/components/backlogs/inbox_component.html.erb | Updates inbox list container data attributes to match Backlogs controller targets. |
| frontend/src/stimulus/controllers/dynamic/backlogs/item.controller.ts | Adds a Stimulus controller to make individual cards draggable + item drop targets. |
| frontend/src/stimulus/controllers/dynamic/backlogs/item.controller.spec.ts | Adds unit tests for item controller state rendering + preventUnhandled integration. |
| frontend/src/stimulus/controllers/dynamic/backlogs/drag-and-drop.ts | Adds helper utilities for resolving targets and computing prev_id for move requests. |
| frontend/src/stimulus/controllers/dynamic/backlogs/drag-and-drop.spec.ts | Adds unit tests for drag-and-drop helper logic (target resolution, ordering). |
| frontend/src/stimulus/controllers/dynamic/backlogs.controller.ts | Replaces HAL-events refresh behavior with Pragmatic DnD monitoring + move requests. |
| frontend/src/global_styles/primer/_overrides.sass | Updates styles for new drag/drop indicators and Box-card dragging visuals. |
| frontend/package.json | Adds Atlaskit Pragmatic DnD dependencies. |
| frontend/package-lock.json | Locks Atlaskit Pragmatic DnD dependencies and transitive packages. |
| app/components/open_project/common/work_package_card_component.rb | Allows forwarding system arguments; ensures Box-card class on root; adds card_arguments. |
| app/components/open_project/common/work_package_card_component.html.erb | Uses card_arguments to apply forwarded system arguments to the root element. |
Files not reviewed (1)
- frontend/package-lock.json: Language not supported
cca6f5e to
10fa291
Compare
2ffd5e6 to
ba69795
Compare
8c94399 to
3e53b87
Compare
|
Hi @HDinger, I'd be grateful if you could take another look at this. Since your last review of the CSS, there have been a number of changes:
Screen.Recording.2026-07-05.at.21.31.53.movI've tried to clean up the CSS as best I can - consolidating as much as possible in For discussion:
|
| description: nil, | ||
| icon: nil, | ||
| interactive: false, | ||
| drop_target_label: nil, |
There was a problem hiding this comment.
not sure about the naming
|
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. |
Card links and the avatar image are natively draggable and pre-empt the Pragmatic card drag; mark them `draggable="false"`.
`data-drop-target-for-element` is a Pragmatic DnD implementation detail; note it in-code to re-verify on upgrades.
Move Backlogs drag auto-scroll out of the sortable-lists root into a dedicated outlet-connected `sortable-lists--scrollable` child, mirroring the list and item controllers. The root drops to monitor and drop orchestration, and scroll containers wire in through an outlet instead of being descendant targets. No behaviour change; axis and speed keep their `vertical`/`standard` defaults.
| "Box-row--clickable", | ||
| "Box-row--draggable" => draggable? | ||
| "Box-card", | ||
| "Box-card--clickable", |
There was a problem hiding this comment.
for follow up: implement a clickable? option
|
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. |
Derives the bucket, sprint, and inbox wire-format list types from `Backlogs::Target` instead of hand-spelling them across the six components, keeping the `(list_type, list_id)` contract in one place. The move dialogs read the value into a local before the Primer form block, which is instance-exec'd against the form object and so can not resolve the component method directly. Updates the component specs to assert the shared target values.
Replaces the runtime `vi.doMock` calls in `beforeAll` with hoisted top-level `vi.mock` declarations, so the specs no longer mutate the shared, non-isolated Vitest module registry mid-run — the source of the chromium-only spy-contention flake.
The `.Box-row[data-dragging="source"]` and `[data-drop-position]` rules sat at the top level of the shared border-box component, so the global `generic-drag-and-drop` `data-dragging` marker restyled dragged rows in admin and settings dragula lists. Nests them under `.op-border-box-list`, and renames the drop-gap custom properties to match that scope.
A list-refresh morph triggered by an unrelated WorkPackage update can replace the captured parent list mid-request. Restoring an optimistic move into that detached node dropped the row out of the live DOM until the next reload. Skips restoration when the captured parent is no longer connected, letting the pending refresh reconcile the position.
`renderDropIndicatorOn` writes the drop position onto the item's next sibling. When a list-refresh morph removes the hovering item mid-drag, its controller disconnects without an `onDrop`, and no other controller may clear a foreign owner — so the indicator lingered as a phantom drop line until a full reload. Clears the owned indicator in `disconnect`.
The frontend ubiquitous-language doc named only the per-item Drop target, yet the code also outlines the whole list via `data-drop-container`. Adds a Drop container term for that list-level state, documents the wired `drop-position-value` in the consumer contract, and aligns the sass comment. Keeps the existing `_Avoid_: Container` notes, since Drop container is a distinct state, not a synonym for a target or list.
| // Box density variant classes copied onto the drag-preview container. The preview is | ||
| // mounted outside the originating Box, so variant-scoped card styles (e.g. | ||
| // `.Box--condensed .Box-card`) would not apply to it otherwise. | ||
| const BOX_DENSITY_VARIANT_CLASSES = ['Box--condensed', 'Box--spacious'] as const; |
There was a problem hiding this comment.
@HDinger I do not like that we have to hard-code these classes here.
Drops the unused `require "json"` and the `expect_backlog_blankslate*` aliases, which had no callers.
Uses 2-space continuation indent for the `move` service call and the `target_open_sprints` / `target_buckets` query chains instead of paren-aligned indent, and moves `move_params` / `move_service_params` to the bottom of the private section.
Breaks the two card-list data guards, the bucket-dialog select-list arguments, and the item controller's missing-value warnings across lines to stay within the 120-column guide.
Updates the selected/current-state comments to name the `backlogs--work-package` controller, renamed from `backlogs--story` earlier on this branch.
Types the `dropTargets.find` predicates as type guards so the target item and list narrow to `HTMLElement` with their payload types, dropping the three redundant re-checks that followed.
A drop onto the list from a row already in it is a no-op — the item drop targets own within-list reorder — yet the list-level `canDrop` still accepted it, so the source list painted its container outline for nothing. Adds the `isSourceListTarget` exclusion `resolveDropIntent` already applies.
Moves the `op:toasters:add` event name into a dependency-free `toast-event` module so the Stimulus controller can dispatch it via the shared `OPToastEvent` constant without importing the Angular `ToastService` graph; `toast.service` now re-exports it. Also drops an unused filter arg the touched file carried.
This reverts commit 2a617b6.
Moves the aria-busy progress/dim rule out of the backlogs page module and onto `.op-border-box-list`, beside its sibling drop state. The rule targeted the sortable list by its Stimulus controller attribute; `.op-border-box-list` names the same element and keeps the busy styling next to the list it belongs to.
Replaces the unbounded `loop`/`next`-on-stale-element retry in `pick_up_and_release_work_package` with the shared `retry_block` helper, capped at three tries on the stale-node errors. Because `retry_block` no-ops under `RSPEC_RETRY_RETRY_COUNT=0`, single and local runs now fail fast on a stale node instead of spinning, so the underlying fault surfaces for the follow-up rather than being masked.
Moves the drag-preview clone, sanitisation and Box density copying out of the item controller into a `preview` helper, leaving the controller to wire pragmatic drag and drop. The sanitisation and density logic is a pure DOM transform, so it gains a focused spec that exercises it without booting a controller.
|
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. |
| blankslate_description: "Drag items here to add them." | ||
| blankslate_title: "Backlog bucket is empty" | ||
| drop_zone_label: "Drop here to add item to this bucket" | ||
| label_actions: "Backlog bucket actions" |
| drop_zone_label: "Drop here to add item to inbox" | ||
| show_more: | ||
| one: "Show 1 more item" | ||
| other: "Show %{count} more items" |
| blankslate_description: "No items planned yet. Drag items here to add them." | ||
| blankslate_title: "%{name} is empty" | ||
| drop_zone_label: "Drop here to add item to this sprint" | ||
| label_actions: "Sprint actions" |

Ticket
https://community.openproject.org/wp/AGILE-251
https://community.openproject.org/wp/AGILE-284 (optimistic reordering, folded in from #23643)
https://community.openproject.org/wp/AGILE-272
Resolves BUGs: https://community.openproject.org/wp/AGILE-252, https://community.openproject.org/wp/AGILE-258
What are you trying to accomplish?
Migrate Backlogs drag and drop to Pragmatic Drag and Drop while preserving the existing inbox / sprint / backlog-bucket move workflows.
The legacy generic (jQuery/Dragula) wiring is replaced by a small Stimulus suite on
@atlaskit/pragmatic-drag-and-drop. Move semantics and server endpoints are unchanged — only the client interaction layer. Backlogs cards now render as ViewComponents (server-rendered markup), and a Turbo guard (pragmatic-dnd-morph-attributes.ts) preserves Pragmatic's registration attributes across morphs so DnD survives stream updates.What approach did you choose and why?
The DnD layer is a reusable, Backlogs-agnostic
sortable-listsStimulus suite:sortable-lists.controller.ts— list "root": owns the move request, drag monitor, and auto-scroll; scopes each drop to its root (accepted type matches, source + resolved target contained) so multiple roots don't steal drops.item.controller.ts— per-card draggable + drop target with closest-edge indicators and a sanitized drag preview.drag-and-drop.ts— the Pragmatic payload contract plus pure, unit-testable drop-intent resolution.list-dom.ts— DnD-agnostic list/row DOM contract: selectors, item/row resolution (incl. truncation markers), optimistic reorder with snapshot/restore.turbo/pragmatic-dnd-morph-attributes.ts— aturbo:before-morph-attributeguard preserving PDD marker attributes across idiomorph morphs.data-drop-target-for-elementis PDD-internal, so it is regression-tested and flagged in-code to re-verify on PDD upgrades.list-refresh.controller.ts— non-DnD: reloads the frame when a work package changes outside a drag (split-pane / Angular edits emit HAL events, not a Turbo stream), hardened against a subscribe-after-disconnect race.Reordering is optimistic (AGILE-284, from #23643): the row moves on drop and persists in the background; the turbo-stream response reconciles on success, and a failure rolls back with a
prefers-reduced-motion-aware FLIP animation.Server moves still flow through
Backlogs::WorkPackages::UpdateService, which normalizeslist_idbefore validatinglist_type(sprint / backlog bucket / inbox) so an empty inbox id and an integer id are handled consistently.Keyboard handling is intentionally minimal: the card is the single focus target and the action menu is the accessible move path. Prototyped keyboard DnD and the drag-specific ARIA-role layer were removed rather than shipped half-complete; full accessible DnD is tracked under follow-up work.
Working checklist
draggable="false"so a native link/image drag can't pre-empt the card drag (complementscanDragFromPoint).positionMode: 'absolute', flexible DOM structure204 No Contentfrom backend)Addressed from review (2026-07-05 triage)
.op-border-box-listso the globaldata-draggingmarker no longer restyles admin/settings dragula lists (review#5)#9)#7)drop-position-valueinfrontend/CONTEXT.md(review#15)#13)list_typethroughBacklogs::Targetinstead of hand-spelled literals (review#14)Review follow-ups (deferred; 2026-07-05 triage)
movingguard until the reconciling frame reload lands, not on PUT resolve — a second drag races the about-to-morph DOM (review#2;sortable-lists.controller.ts)data-selectedplus a revertedaria-currentcan leave two cards highlighted (review#6;backlogs/work-package.controller.ts)aria-currentremoval inside the backlogs frame (review#8a;pragmatic-dnd-morph-attributes.ts)#10;sortable-lists/drag-and-drop.ts)UpdateServicecommit + post-commitmove_after) in one transaction — a failure between the phases can leave a stale/nil position (review#12;work_packages/update_service.rb)#4;work_packages_controller.rb)vi.mocknot at top level" deprecation in the sortable-lists specs (top-levelvi.mock+ aliasedcore-stimulus/test-helpers; vitest-v4 hoister × vite-alias, non-fatal today)pick_up_and_release_work_packageis now capped viaretry_block, but the stale node it retries on (likely a mid-drag list-refresh morph detaching the grabbed row) should be waited out or re-queried inside the drag so no retry is needed (spec/support/pages/backlog.rb)Archived
bundle exec rspec modules/backlogs/spec/features/work_packagesFollow-up work
Merge checklist