Skip to content

Rework sortable-lists topology and enable optimistic 204 moves#24104

Open
myabc wants to merge 4 commits into
implementation/74970-backlogs-pragmatic-dndfrom
implementation/74970-backlogs-pragmatic-dnd-final-rework
Open

Rework sortable-lists topology and enable optimistic 204 moves#24104
myabc wants to merge 4 commits into
implementation/74970-backlogs-pragmatic-dndfrom
implementation/74970-backlogs-pragmatic-dnd-final-rework

Conversation

@myabc

@myabc myabc commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

🤖 Opened by an agent on Alex's behalf.

Ticket

https://community.openproject.org/wp/AGILE-251 (parent PR: #23218)
https://community.openproject.org/wp/AGILE-284 (optimistic reordering)
https://community.openproject.org/wp/AGILE-292 (controller topology decision)

What are you trying to accomplish?

This addresses the architectural feedback on #23218 and #23893 (outlets scepticism, safeguard reduction, per-list types) plus the remaining "fully enable optimistic reordering" checklist item, as a separate branch so it can be reviewed as one coherent diff before folding into #23218.

Controller communication. The root no longer declares outlets or pushes state down (connectRoot/RootAwareChild/reflectMoving are gone). Instead the leaf controllers each declare a sortable-lists outlet pointing at the root — the Stimulus analogue of context injection: values are props down, Pragmatic payloads are events up, and the child→root outlet is the pull-based read of shared root state (moving, root identity). No closest() state lookups in controllers.

Safeguards centralized. item.canDrag is the single moving gate — a drop ends the drag before the persist request starts, so the repeated checks in canDrop/canMonitor/handleDrop were unreachable and are removed. moving is now a private field with a getter (the data-sortable-lists-moving attribute is gone; nothing consumed it), and aria-busy is set on the root only.

Per-list types. Lists declare acceptedTypes (array value); the root's acceptedType is removed. Item drop targets accept same-type items only. A list is a drop target only with non-empty acceptedTypes and a non-empty type (the persisted list_type); partial config warns.

Generic beyond ul/li. Rows are now structural — direct children of a rows container, resolved via the new optional rowsContainerSelector list value (successor of data-target-container-accessor; Backlogs BorderBox lists use :scope > ul). Verified against the AGILE-292 dual-role nesting shape (item surface wrapping an inner list), with both-direction nested resolution tests. positionMode: 'absolute' and per-type moveUrlTemplates are included so #23893 rebases to a pure consumer.

Optimistic 204 (AGILE-284). A same-list drag-and-drop move now returns 204 No Content and the optimistic DOM order is final. Gated on an optimistic param the sortable-lists root appends — action-menu moves (Move up/down/to top/to bottom) have no optimistic DOM reorder and keep the full turbo-stream frame reload. On success the root dispatches sortable-lists:moved (after its moving flag clears), which drives split-view lock_version refresh on the 204 path and doubles as the deterministic settle signal for the Selenium drag specs (replacing the frame-reload wait for same-list drags).

Notes for reviewers

  • resolveItemElement implements the "prefer the row's own item surface" rule via self-or-descendant resolution and document order rather than a :scope > selector chain — equivalent for the agreed dual-role markup, and the ancestor-climbing variant was an actual bug (an inner card row would resolve to the outer bucket's item). Tests cover both directions.
  • Known limitation for Experiment/project attributes dnd #23893: resolveItemPosition (absolute mode) appends to the end when the previous item is hidden behind a truncation marker row. Not reachable in Backlogs (relative mode); flagged for the Experiment/project attributes dnd #23893 rebase.
  • optimistic is deliberately not in the permitted service params — Backlogs::WorkPackages::UpdateService#call takes keyword args.

Test evidence

  • Frontend: 1016 unit tests green via the ng-test harness (chromium), including new coverage for outlet gating, per-list acceptance, payload modes, turbo-frame query preservation, moved-event ordering, and nested row resolution.
  • Backend: backlogs suite green incl. new request specs (204 / menu / cross-list / failure / optimistic=false) and a same-list-drag-with-split-view-open feature spec.
  • Selenium drag suite green; stress runs show zero wait-timeouts (remaining flakes match the pre-existing documented signatures).

Merge checklist

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

Copilot AI review requested due to automatic review settings July 4, 2026 18:03
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown

Caution

The provided work package version does not match the core version

Details:

Please make sure that:

  • The work package version OR your pull request target branch is correct

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 refactors the sortable-lists Stimulus controller suite to remove root→child outlet wiring, make list acceptance rules per-list (and more DOM-structure-agnostic), and fully enable “optimistic” same-list drag reorders by allowing a 204 No Content backend response while introducing a deterministic sortable-lists:moved settle signal used by Backlogs and tests.

Changes:

  • Reworked controller topology so list/item controllers outlet to the root and read shared root state (e.g., moving) from that outlet rather than root-managed callbacks.
  • Introduced per-list acceptedTypes plus a configurable rowsContainerSelector, and updated DOM-resolution/reorder logic accordingly (including nested/dual-role shapes).
  • Enabled optimistic same-list moves with a 204 backend response and added a sortable-lists:moved event to drive Backlogs split-view lock_version refresh and Selenium settle waiting.

Reviewed changes

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

Show a summary per file
File Description
spec/support/capybara/wait_helpers.rb Adds a Capybara helper that waits for sortable-lists:moved as the settle signal.
modules/backlogs/spec/support/pages/backlog.rb Switches Backlogs drag helper to wait on either turbo reload (cross-list) or moved event (same-list).
modules/backlogs/spec/requests/backlogs/work_packages_move_spec.rb Adds request coverage for 204 optimistic same-list moves vs turbo-stream reload paths.
modules/backlogs/spec/requests/backlogs/backlog_spec.rb Updates expectations for new root data attributes (moveUrlTemplates).
modules/backlogs/spec/features/work_packages/edit_in_split_view_after_move_spec.rb Adds a selenium scenario ensuring split-view edits work after same-list 204 move.
modules/backlogs/spec/features/work_packages/drag_in_sprint_spec.rb Updates a cross-list drag call site to opt into the turbo-reload wait.
modules/backlogs/spec/components/backlogs/work_package_card_list_item_component_spec.rb Verifies item controller outlets to the root in rendered attributes.
modules/backlogs/spec/components/backlogs/work_package_card_list_component_spec.rb Verifies list outlet + acceptedTypes + rowsContainerSelector attributes.
modules/backlogs/spec/components/backlogs/sprint_component_spec.rb Verifies sprint list wiring for the new list controller contract.
modules/backlogs/spec/components/backlogs/inbox_component_spec.rb Verifies inbox list wiring for the new list controller contract.
modules/backlogs/spec/components/backlogs/bucket_component_spec.rb Verifies bucket list wiring for the new list controller contract.
modules/backlogs/app/views/backlogs/backlog/show.html.erb Updates root wiring to moveUrlTemplates + binds sortable-lists:moved for split-view sync.
modules/backlogs/app/views/backlogs/backlog/_backlog_list.html.erb Updates turbo-frame partial wiring to moveUrlTemplates.
modules/backlogs/app/controllers/backlogs/work_packages_controller.rb Implements 204 response for optimistic same-list moves.
modules/backlogs/app/components/backlogs/work_package_card_list_item_component.rb Adds item→root outlet attribute for sortable-lists item controllers.
modules/backlogs/app/components/backlogs/work_package_card_list_component.rb Adds list→root outlet + per-list accepted types + rows container selector.
modules/backlogs/app/components/backlogs/inbox_component.html.erb Updates inbox list wiring to new per-list values/outlet contract.
frontend/src/stimulus/controllers/dynamic/sortable-lists/list.controller.ts Converts list controller to outlet-to-root model; enforces per-list accepted types.
frontend/src/stimulus/controllers/dynamic/sortable-lists/list.controller.spec.ts Updates unit tests to mount real root+list and cover acceptedTypes/outlet behavior.
frontend/src/stimulus/controllers/dynamic/sortable-lists/list-dom.ts Refactors DOM contract to support arbitrary row containers and row resolution.
frontend/src/stimulus/controllers/dynamic/sortable-lists/list-dom.spec.ts Adds coverage for new container/row/item resolution and absolute-position calculations.
frontend/src/stimulus/controllers/dynamic/sortable-lists/item.controller.ts Converts item controller to outlet-to-root model; centralizes moving gate in canDrag.
frontend/src/stimulus/controllers/dynamic/sortable-lists/item.controller.spec.ts Updates unit tests to use real outlets and validate moving gate + type/root scoping.
frontend/src/stimulus/controllers/dynamic/sortable-lists/drag-and-drop.ts Updates payload/type rules; adds root identity scoping and absolute-position payload mode.
frontend/src/stimulus/controllers/dynamic/sortable-lists/drag-and-drop.spec.ts Expands helper tests for root scoping, per-list acceptance, rows containers, and payload shapes.
frontend/src/stimulus/controllers/dynamic/sortable-lists.controller.ts Refactors root controller to moveUrlTemplates, position mode, root-only aria-busy, and moved event dispatch.
frontend/src/stimulus/controllers/dynamic/sortable-lists.controller.spec.ts Updates root controller tests for new templates map, moved-event ordering, and aria-busy semantics.
frontend/src/stimulus/controllers/dynamic/backlogs/split-view-sync.controller.ts Adds handling for client-dispatched sortable-lists:moved to refresh lock_version cache.
frontend/src/stimulus/controllers/dynamic/backlogs/split-view-sync.controller.spec.ts Adds unit coverage for the new sortable-lists moved event path.

@myabc myabc requested a review from ulferts July 4, 2026 18:25
@myabc myabc added javascript Pull requests that update Javascript code ruby Pull requests that update Ruby code needs review experiment pullpreview labels Jul 4, 2026
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown

Deploying openproject with PullPreview

Field Value
Latest commit 6f399c7
Job deploy
Status ✅ Deploy successful
Preview URL https://pr-24104-74970-backlogs-p-ip-167-233-75-249.my.opf.run:443

View logs

myabc added 4 commits July 4, 2026 20:13
Resolves sortable-list rows through an explicit rows container so
consumers are no longer tied to ul/li-only markup. Rows are the
direct children of the container named by the list's optional
rowsContainerSelector value (e.g. ':scope > ul' for Primer
BorderBox lists, which own their inner ul); without it, the list
element itself holds the rows. Row lookup never resolves an
ancestor, so an inner row of a nested dual-role surface resolves
its own item rather than the outer one.

resolveDropIntent computes the rows container once and carries it
on the returned intent, so list-only end drops append after the
true last row. buildMoveFormData receives the whole drop intent
and supports both payload modes: relative (prev_id) and absolute
(target_id plus a position counted over item rows only).
Replaces the root-pushed child state (outlet callbacks, the
connectRoot handshake, RootAwareChild) with leaf controllers that
read the sortable-lists root through a child-declared Stimulus
outlet: values act as props down, Pragmatic payloads as events
up, and the outlet as the pull-based context read.

Lists now declare their own acceptedTypes and item drop targets
accept same-type items only; the root's acceptedType value is
gone. The item's canDrag becomes the single moving gate: a drop
ends the drag before the persist request starts, so the repeated
checks in canDrop and canMonitor were unreachable. moving becomes
a private field behind a getter, the data-sortable-lists-moving
attribute disappears, and aria-busy is set on the root element
only.

Root turbo frames declare a single moveUrlTemplates value (item
type to URL template, JSON-encoded) instead of separate
accepted-type/move-url-template values and CSS-selector-based
list/item outlet attributes. List components (inbox, sprint,
bucket, work package card list) outlet directly to the
#backlogs_container root and declare their own acceptedTypes and
rowsContainerSelector values; item draggable data outlets to the
same root. Updates the affected component and request specs to
the new attribute contract with an explicit assertion per list
component for the outlet and acceptedTypes wiring.
Extracts the shared cache-refresh logic in
SplitViewSyncController into a private refreshWorkPackage helper
and adds onSortableListsMoved, bound to the client-dispatched
sortable-lists:moved document event. Same-list moves resolve with
a 204 and never trigger the server-dispatched
backlogs:work-package-moved event, so the split view's cached
work package would otherwise keep a stale lock_version after such
a move.

Adds the second action binding to the backlogs turbo frame in
show.html.erb, alongside two new spec cases covering the refresh
and the no-item-id guard.
Makes sortable-lists mark safe moves as optimistic so
Backlogs::WorkPackagesController#move can answer a same-list drag
with 204 No Content: the client has already reordered the row and
nothing further needs to render. Menu-driven moves carry no
optimistic param and cross-list moves change lists, so both keep
the turbo-stream frame reload they need for sprint totals, blank
slates, and buckets that no longer match their filter. The param
stays out of the service params (the update service takes keyword
arguments) and is cast with ActiveModel::Type::Boolean, so
optimistic=false is not treated as truthy.

Truncated lists opt out of the optimistic param: their visible
window (a head/tail slice plus a show-more marker row) is
server-computed, so hasTruncationMarkerRow keeps them on the
stream reload path while the optimistic DOM reorder still happens
client-side for responsiveness. Also hardens
resolveRowsContainer against invalid rowsContainerSelector
values, warning and falling back to the list element instead of
letting querySelector's SyntaxError break the drop.

On success the root dispatches sortable-lists:moved strictly
after its moving flag clears; the split view listens to it (the
204 path has no server-dispatched event) and the new
wait_for_sortable_lists_moved helper makes same-list drag specs
deterministic, while cross-list and truncated-list drags keep
waiting on the frame reload. Request specs cover the optimistic
same-list, menu same-list, cross-list, optimistic=false, and
failure branches; Selenium specs cover the truncated inbox and a
same-list drag with the split view open.
@myabc myabc force-pushed the implementation/74970-backlogs-pragmatic-dnd-final-rework branch from cbdcaaf to 6f399c7 Compare July 4, 2026 19:14
controller: "sortable-lists--list",
sortable_lists__list_sortable_lists_outlet: "#backlogs_container",
sortable_lists__list_type_value: "inbox",
sortable_lists__list_accepted_types_value: ["work_package"].to_json,

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.

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

Labels

experiment javascript Pull requests that update Javascript code needs review pullpreview ruby Pull requests that update Ruby code

Development

Successfully merging this pull request may close these issues.

2 participants