Skip to content

[#73473] Make whole Backlog card draggable#22838

Merged
ulferts merged 6 commits intodevfrom
feature/73473-backlogs-card-draggable
Apr 22, 2026
Merged

[#73473] Make whole Backlog card draggable#22838
ulferts merged 6 commits intodevfrom
feature/73473-backlogs-card-draggable

Conversation

@myabc
Copy link
Copy Markdown
Contributor

@myabc myabc commented Apr 20, 2026

Ticket

https://community.openproject.org/wp/73473
https://community.openproject.org/wp/74194
https://community.openproject.org/wp/74195

What are you trying to accomplish?

Make the entire Backlog work-package card the drag surface, in both the sprint backlogs and the inbox. Previously a dedicated DragHandle grabber on the left of each card was the only drag affordance. This PR removes the handle and lets users grab the card anywhere to reorder it or move it between sprints.

Permission gating is preserved: users without :manage_sprint_items still render non-draggable cards (no Box-row--draggable class, no data-draggable-* / data-drop-url attributes) and cannot initiate a drag.

Screenshots

Before After
Screenshot 2026-04-22 at 13 57 31 Screenshot 2026-04-22 at 13 55 57

What approach did you choose and why?

Two additions to the existing Dragula-based generic-drag-and-drop Stimulus controller make it work without a dedicated handle:

  • handle-value (Boolean, default true). When false, the whole element is the drag surface and handle-selector-value is ignored. A Boolean was preferred over interpreting an empty handle-selector-value, since empty HTML attributes are ambiguous.
  • isInteractiveElement / closestInteractiveElement helpers, used on drag start to abort the gesture when the user is interacting with a button, link, input, or ActionMenu inside the card — so the kebab menu and WP detail links aren't hijacked.

Server-side, Backlogs::InboxItemComponent now gates the Box-row--draggable class and the data-draggable-* / data-drop-url attributes behind :manage_sprint_items, matching the pattern already used in SprintComponent. Removing the handle also lets us drop the drag_handle grid column, the label_drag_* translation keys, and the now-unused StoryComponent#draggable?.

Merge checklist

  • Added/updated tests
  • Tested major browsers (Chrome, Firefox, Edge, ...)

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 20, 2026

Deploying openproject with PullPreview

Field Value
Latest commit a9d61b2
Job deploy
Status 🗑️ Preview destroyed
Preview URL Destroyed

View logs

@myabc myabc changed the title Feature/73473 backlogs card draggable [#73473] Improve backlogs drag and drop Apr 20, 2026
@myabc myabc requested a review from Copilot April 20, 2026 19:11
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 modernizes Backlogs drag-and-drop to support whole-card dragging (instead of a dedicated handle) and reduces server-rendered Turbo Stream updates for same-list reorders to improve responsiveness.

Changes:

  • Replaces the Backlogs DnD surface/list implementation (new Stimulus controllers) and mounts it in the backlog view.
  • Removes the dedicated drag handle from story/inbox cards and updates styling for whole-card dragging.
  • Optimizes reorder endpoints to return 204 No Content for same-list reorders and updates Ruby/JS test coverage accordingly.

Reviewed changes

Copilot reviewed 24 out of 25 changed files in this pull request and generated no comments.

Show a summary per file
File Description
modules/backlogs/app/views/backlogs/backlog/_backlog_list.html.erb Mounts backlogs--dnd-surface and removes generic-drag-and-drop wiring.
modules/backlogs/app/controllers/backlogs/work_packages_controller.rb Returns 204 for same-sprint reorder and avoids unnecessary Turbo Stream replacements.
modules/backlogs/app/controllers/backlogs/inbox_controller.rb Returns 204 for inbox reorders to rely on optimistic client updates.
modules/backlogs/app/components/backlogs/sprint_component.rb Switches sprint drop target metadata to the new list controller data attributes.
modules/backlogs/app/components/backlogs/inbox_component.rb Switches inbox drop target metadata to the new list controller data attributes.
modules/backlogs/app/components/backlogs/story_component.html.erb Removes the dedicated drag handle markup.
modules/backlogs/app/components/backlogs/inbox_item_component.* Removes dedicated drag handle markup and renames story controller metadata to item controller metadata.
frontend/src/stimulus/controllers/dynamic/backlogs/dnd-surface.controller.ts New DnD surface controller using @dnd-kit/dom, optimistic DOM move + persistence.
frontend/src/stimulus/controllers/dynamic/backlogs/dnd-list.controller.ts New list metadata helper controller.
frontend/src/stimulus/controllers/dynamic/backlogs/item.controller.ts Renames story controller to item controller; adds pointer-move threshold to suppress click-after-drag.
frontend/src/assets/sass/backlogs/_master_backlog.sass Updates card grid layout to remove drag handle column/area.
frontend/src/global_styles/primer/_overrides.sass Updates draggable cursor styling.
frontend/package.json / frontend/package-lock.json Adds @dnd-kit/dom dependency.
modules/backlogs/spec/** and frontend/src/**.spec.ts Updates/extends request/controller/component/unit specs for the new behavior.
Files not reviewed (1)
  • frontend/package-lock.json: Language not supported
Comments suppressed due to low confidence (1)

modules/backlogs/app/components/backlogs/inbox_item_component.rb:77

  • row_options always sets Box-row--draggable and the draggable metadata (data-draggable-id, data-drop-url, etc.) regardless of draggable?. With the new whole-card drag behavior (no dedicated handle), this makes inbox rows draggable even for users who are not allowed to manage sprint items. Please gate the draggable class + draggable metadata behind draggable? (similar to SprintComponent#draggable_item_config) so unauthorized users cannot start DnD from the inbox UI.
    def row_options
      {
        id: dom_id(work_package),
        classes: "Box-row--hover-blue Box-row--focus-gray Box-row--clickable Box-row--draggable",
        data: {
          draggable_id: work_package.id,
          draggable_type: "story",
          drop_url: move_project_backlogs_inbox_path(project, work_package),
          story: true,
          controller: "backlogs--item",
          backlogs__item_id_value: work_package.id,
          backlogs__item_split_url_value: project_backlogs_backlog_details_path(project, work_package),
          backlogs__item_full_url_value: work_package_path(work_package),
          backlogs__item_selected_class: "Box-row--blue",
          test_selector: card_test_selector
        },
        tabindex: 0
      }

@myabc myabc changed the title [#73473] Improve backlogs drag and drop [#73473] Make whole Backlog card draggable Apr 20, 2026
@myabc myabc requested a review from Copilot April 20, 2026 22:21
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 31 out of 32 changed files in this pull request and generated 3 comments.

Files not reviewed (1)
  • frontend/package-lock.json: Language not supported

Comment thread modules/backlogs/app/components/backlogs/inbox_item_component.rb Outdated
Comment thread modules/backlogs/spec/support/pages/backlog.rb
Comment thread frontend/src/stimulus/controllers/dynamic/backlogs/dnd-surface.controller.spec.ts Outdated
@myabc myabc force-pushed the feature/73473-backlogs-card-draggable branch from 72ad902 to a9d61b2 Compare April 21, 2026 14:02
@myabc myabc force-pushed the feature/73473-backlogs-card-draggable branch from a9d61b2 to 187d91d Compare April 21, 2026 14:55
@myabc myabc requested a review from Copilot April 21, 2026 14:57
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 11 out of 11 changed files in this pull request and generated 3 comments.

Comment thread frontend/src/stimulus/controllers/dynamic/generic-drag-and-drop.controller.ts Outdated
Comment thread frontend/src/stimulus/controllers/dynamic/generic-drag-and-drop.controller.ts Outdated
Comment thread modules/backlogs/spec/support/pages/backlog.rb
@myabc myabc force-pushed the feature/73473-backlogs-card-draggable branch from 187d91d to 7402cb3 Compare April 21, 2026 15:33
@myabc myabc requested a review from Copilot April 21, 2026 15:35
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 2 comments.

Comments suppressed due to low confidence (1)

modules/backlogs/app/components/backlogs/inbox_item_component.html.erb:35

  • Same as for StoryComponent: removing the drag handle eliminates the explicit labelled drag control (focusable button with aria-label), which is an accessibility regression for keyboard/screen reader users. Consider retaining an accessible drag handle (optionally visually hidden or shown on focus) while still supporting handle-less pointer dragging.
<% container.with_row(**row_options) do %>
  <%= grid_layout("op-backlogs-story", tag: :article) do |grid| %>
    <% grid.with_area(:info_line) do %>
      <%= render(WorkPackages::InfoLineComponent.new(work_package:)) %>
    <% end %>
    <% grid.with_area(:points) do %>

Comment thread modules/backlogs/spec/support/pages/backlog.rb Outdated
Comment on lines 30 to 33
<%= grid_layout("op-backlogs-story", tag: :article) do |grid| %>
<% grid.with_area(:drag_handle, classes: "hide-when-print") do %>
<%= if draggable?
render(
Primer::OpenProject::DragHandle.new(
classes: "op-backlogs-story--drag_handle_button",
label: t(".label_drag_story", name: story.subject)
)
)
end %>
<% end %>

<% grid.with_area(:info_line) do %>
<%= render(WorkPackages::InfoLineComponent.new(work_package: story)) %>
<% end %>
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

Removing the dedicated drag handle also removes the only explicit, labelled control for drag-and-drop (previously a focusable button with an aria-label). In handle-less mode, mouse dragging works, but assistive-technology and keyboard users lose a clear affordance/announcement that the item is draggable. Consider keeping a drag handle button for accessibility (it can be visually hidden / only shown on focus) while still allowing handle-less dragging for pointer users.

Copilot uses AI. Check for mistakes.
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.

@myabc myabc force-pushed the feature/73473-backlogs-card-draggable branch from 7402cb3 to 4184ce5 Compare April 21, 2026 16:01
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 19 out of 20 changed files in this pull request and generated 3 comments.

Comment thread frontend/src/stimulus/controllers/dynamic/generic-drag-and-drop.controller.ts Outdated
Comment thread frontend/src/stimulus/controllers/dynamic/generic-drag-and-drop.controller.ts Outdated
Comment thread frontend/src/global_styles/primer/_overrides.sass Outdated
Copy link
Copy Markdown
Contributor

@HDinger HDinger left a comment

Choose a reason for hiding this comment

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

Works nicely 🤩 👍 I only have some minor remarks

Comment thread frontend/src/stimulus/controllers/dynamic/generic-drag-and-drop.controller.ts Outdated
Comment thread frontend/src/global_styles/primer/_overrides.sass Outdated
Comment thread frontend/src/global_styles/primer/_overrides.sass
@myabc myabc force-pushed the feature/73473-backlogs-card-draggable branch 2 times, most recently from 9bba79c to b3dce16 Compare April 22, 2026 12:41
@myabc myabc marked this pull request as ready for review April 22, 2026 12:44
myabc added 2 commits April 22, 2026 14:55
Introduces `handle-value` Boolean supporting Drag and Drop without a
separate drag handle. The drag handle selector can still be customized
via the separate `handle-selector-value`.

Also introduces `isInteractiveElement` and `closestInteractiveElement`
helpers - to prevent drag and drop being initiated from interactive
elements (e.g. an ActionMenu on a work package card).

https://community.openproject.org/wp/73473
Removes the dedicated `DragHandle` column from Backlog story and inbox
rows and makes the whole card the drag surface, opting in via
`handle-value="false"` on the drag-and-drop controller.

Permission gating is preserved: `InboxItemComponent` now matches the
`SprintComponent` pattern and emits `Box-row--draggable` along with
the `data-draggable-*` / `data-drop-url` attributes only when the
current user has `:manage_sprint_items`. Users without the permission
see a plain, non-draggable row.

Drops the now-unused `StoryComponent#draggable?`, the `drag_handle`
grid column, and the `label_drag_*` translation keys.

https://community.openproject.org/wp/73473
@myabc myabc force-pushed the feature/73473-backlogs-card-draggable branch from b3dce16 to fcb0bd5 Compare April 22, 2026 13:55
@myabc myabc requested review from HDinger and Copilot April 22, 2026 13:58
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 19 out of 20 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

modules/backlogs/app/components/backlogs/inbox_item_component.html.erb:35

  • With the drag handle removed, this row no longer contains any labelled control indicating it can be dragged. Since dragula-based drag-and-drop is pointer-driven, consider adding an accessible affordance (e.g., a visually-hidden drag handle/button with an aria-label or row-level ARIA description) so screen reader users still get a clear draggable announcement.
<% container.with_row(**row_options) do %>
  <%= grid_layout("op-backlogs-story", tag: :article) do |grid| %>
    <% grid.with_area(:info_line) do %>
      <%= render(WorkPackages::InfoLineComponent.new(work_package:)) %>
    <% end %>
    <% grid.with_area(:points) do %>

@@ -28,17 +28,6 @@ See COPYRIGHT and LICENSE files for more details.
++# %>

<%= grid_layout("op-backlogs-story", tag: :article) do |grid| %>
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

Removing the dedicated drag handle means there is no longer any labelled, focusable element that communicates “this card is draggable” to assistive technologies. In handle-less mode this can be a UX/a11y regression because dragging becomes pointer-only with no explicit announcement/instructions. Consider adding an accessible affordance (e.g., a visually-hidden drag handle/button with an aria-label, or an aria-describedby/roledescription on the draggable row) while still allowing pointer dragging from the whole card.

Suggested change
<%= grid_layout("op-backlogs-story", tag: :article) do |grid| %>
<%= grid_layout(
"op-backlogs-story",
tag: :article,
id: dom_target(story),
tabindex: 0,
"aria-roledescription": t(".drag_roledescription", default: "draggable story card"),
"aria-describedby": dom_target(story, :drag_description)
) do |grid| %>
<span id="<%= dom_target(story, :drag_description) %>"
style="position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border:0;">
<%= t(".drag_description", default: "Draggable story card. Drag and drop to reorder.") %>
</span>

Copilot uses AI. Check for mistakes.
myabc and others added 4 commits April 22, 2026 15:14
The previous guard `unless before || into || (before && into)` was
effectively `unless before || into`, so passing both arguments was
silently accepted despite the message promising "either/or". Replaces
the guard with an XOR check and corrects the error string.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Wires up `generic-drag-and-drop.controller` so the CSS3 grabbing
cursor shows for the duration of a drag, by toggling a
`data-dragging="active"` attribute on `document.body` and styling
descendants via the `body[data-dragging="active"]` rule.

Also clears the body attribute in `disconnect()`, so a Turbo morph
while a drag is in progress does not leave the cursor stuck
globally.

https://community.openproject.org/wp/74194
Adds an optional `mirrorContainer` target to the drag-and-drop
controller. When present, the dragula mirror (drag preview) is
inserted into that element instead of `document.body`, so it
inherits the container's padding and alignment.

The Backlog inbox wires its own list element as both `container` and
`mirrorContainer`, which keeps the dragged card visually aligned
with the rest of the inbox rather than jumping to the page edge.

https://community.openproject.org/wp/74195
Tags the dragula mirror with `data-dragging="mirror"` and the source
row with `data-dragging="source"`, then adds Primer overrides so the
mirror reads as a distinct floating card (background, border, shadow)
while the source row renders as a faded "ghost" at reduced opacity.

Also suppresses `.Box-row--hover-*` colors during a drag to stop
hover flicker as the mirror passes over the list.
@myabc myabc force-pushed the feature/73473-backlogs-card-draggable branch from fcb0bd5 to 88b2ea2 Compare April 22, 2026 14:14
Comment on lines +77 to +79
// A Turbo morph mid-drag can replace the element tree without the
// dragend event firing, so clear the body-level cursor flag defensively.
document.body.removeAttribute('data-dragging');
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.

Claude found this pre-emptively. I wouldn't have considered it.

Copy link
Copy Markdown
Contributor

@ulferts ulferts left a comment

Choose a reason for hiding this comment

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

I only browsed over the code but I did test the code out this morning which worked like a charm. I trust that Henriette did the code review. Together, we should be fine.

@ulferts ulferts merged commit 6e9baca into dev Apr 22, 2026
19 of 20 checks passed
@ulferts ulferts deleted the feature/73473-backlogs-card-draggable branch April 22, 2026 16:40
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 22, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

feature javascript Pull requests that update Javascript code styling

Development

Successfully merging this pull request may close these issues.

4 participants