Skip to content

[DREAM-789] Move enumerations via sortable-lists - #25467

Draft
myabc wants to merge 3 commits into
implementation/dream-775-shared-move-menu-helperfrom
code-maintenance/dream-789-enumerations-sortable-lists
Draft

myabc wants to merge 3 commits into
implementation/dream-775-shared-move-menu-helperfrom
code-maintenance/dream-789-enumerations-sortable-lists

Conversation

@myabc

@myabc myabc commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Ticket

https://community.openproject.org/wp/DREAM-789

What are you trying to accomplish?

Enumeration slice of DREAM-789: work package priorities, time-entry activities and document types move from the legacy generic-drag-and-drop controller and server-rendered move_to forms onto the shared sortable-lists stack.

Those three lists were the last admin surfaces posting a direction or a raw position to reorder. That endpoint could not express a drop anchor, re-rendered the whole list on every move, and let array or hash parameters slip past blank? and read as "move to top". The menu also rendered its available directions server-side, so it went stale the moment the user reordered anything.

Stacked on #25464 (DREAM-775), which adds the shared move-menu helper these lists now use; this PR should be reviewed and merged after that one.

What approach did you choose and why?

Enumeration and DocumentType include Lists::MoveAfterAnchor, and each concrete class carries its own SORTABLE_LIST_TYPE derived from model_name.param_key. A constant on the STI base would be visible from every subclass and would let a priority anchor against an activity, so the value is defined per concrete class.

Admin::Settings::EnumerationsControllerBase#move is now anchor-only, guarded exactly as StatusesController#valid_drop_request? is: the permitted parameters must carry the right list_type and a prev_id key, the raw list_id must be blank, and a supplied list_id must have survived scalar permitting. An empty array or an empty hash sent as a JSON list_id passes blank? but is dropped entirely by permit, the same trap the statuses endpoint guards against, so that case is rejected too rather than silently read as "move to top". A rejected request answers 422 with error_invalid_list_move_anchor and mutates nothing. A successful one morphs the index component rather than replacing it, which is what DREAM-789 asks for and is deliberately not Roles' 204 No Content. It is also deliberately not what StatusesController#move does: statuses kept their server-side move_to branch because their list is paginated and a direction can cross a page boundary, whereas these lists are unpaginated, so the work package's "retire the move-menu persistence paths" applies and move_to and position are deleted outright. The consequence is that reordering now needs JavaScript, as it already does for roles.

A successful move renders the list morph stream before the success flash stream, so a visible flash is a reliable signal that the list has already been reconciled with the new order. The feature specs wait on that flash and on the busy attribute clearing before acting again, instead of querying the database, since optimistic DOM order and persisted order can both look right before the response has actually landed.

Cross-class anchors are exercised in the shared request example only between priorities and time-entry activities, because those two share the enumerations table and therefore one ID space. Document types have their own table and their own ID space, so a priority's numeric ID can coincide with a perfectly valid document-type anchor; a cross-class rejection test would be non-deterministic there, and document types rely on the absent-ID and wrong-list-type cases instead.

The index component mirrors Admin::TextTransformActions::IndexComponent: root controller and move-URL template on the wrapper, list data on the border box, item data per row. Hosting the root controller on the component wrapper is the non-obvious part: an update stream renders only the wrapper's inner HTML, so the root element and its controller instance survive every morph by construction, while the list and item elements inside are morphed and re-registered by the root's turbo:morph-element listener. The URL template keeps the __id__ sentinel so relative-URL-root installations keep working, and stays controller-relative because three controllers share the component. The rows container needs no override: both the border box list and border_box_container render a Primer BorderBox, whose rows sit in a direct ul child, which is the list controller's default.

In the item menu, Move shares Edit's group instead of getting one of its own. with_item_group only drops an empty group at server render, while the item controller hides the Move submenu client-side from live DOM position, so a separate group would leave two adjacent separators on a single-item list. With this grouping exactly one separator renders in every state, and no moveDivider target is needed.

Document types keep their two-column grid and its sass; only the data attributes change. The table migration is DREAM-840, which stacks on this.

DREAM-789 stays open. Covered here: priorities, time-entry activities and document types reorder by drag and by menu; the drop endpoint is anchor-only and rejects malformed parameters; the update boundary morphs. Not covered: user custom field sections, project phase definitions and PDF export templates.

AI involvement

Merge checklist

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

The sortable-lists drop endpoint addresses a target by the id of the
record it lands behind, so the three enumeration lists need the shared
anchor concern and a list type that cannot be confused across classes.

https://community.openproject.org/wp/DREAM-789
The enumeration lists kept their own drag controller, position-based
drops and server-rendered move forms. They join the shared stack: one
anchor-only endpoint with strict parameter guards serves drag and menu
moves, and its response morphs the list. The endpoint changes together
with its consumers so that no commit leaves the lists unable to reorder.

https://community.openproject.org/wp/DREAM-789
A second drag after a completed morph is what catches stale Pragmatic
registrations; a second menu move alone passes while they are broken.
The single-item examples guard the menu grouping against a duplicate.

https://community.openproject.org/wp/DREAM-789

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.

🔵 Needs a closer look

The cross-cutting changes span multiple modules and require final human review.

Pull request overview

Migrates work package priorities, time-entry activities, and document types to the shared sortable-list reordering system.

Changes:

  • Adds anchor-based moves with strict validation and Turbo morph responses.
  • Replaces legacy drag-and-drop and move forms.
  • Adds model, request, component, and feature coverage.
File summaries
File Description
spec/support/shared/enumeration_anchor_move.rb Shared endpoint validation coverage
spec/requests/admin/settings/work_package_priorities_spec.rb Priority move request coverage
spec/models/enumeration/enumeration_sortable_list_type_spec.rb Sortable type and anchor behavior
spec/features/admin/settings/work_package_priorities_spec.rb Priority drag/menu flows
spec/components/admin/enumerations/item_component_spec.rb Enumeration move menu coverage
spec/components/admin/enumerations/index_component_spec.rb Sortable wiring coverage
modules/documents/spec/requests/documents/admin/settings/document_types_spec.rb Document type request coverage
modules/documents/spec/features/documents/admin/settings/document_types_spec.rb Document type UI flows
modules/documents/spec/components/documents/admin/document_types/index_component_spec.rb Document sortable wiring
modules/documents/app/models/document_type.rb Enables anchor movement and sortable type
modules/documents/app/components/documents/admin/document_types/item_component.html.erb Adds sortable handle
modules/documents/app/components/documents/admin/document_types/index_component.html.erb Adds sortable list data
modules/costs/spec/requests/admin/settings/time_entry_activities_spec.rb Activity move request coverage
modules/costs/spec/features/admin/settings/time_entry_activities_spec.rb Activity menu flow
modules/costs/app/models/time_entry_activity.rb Adds sortable list type
app/models/issue_priority.rb Adds sortable list type
app/models/enumeration.rb Enables anchor movement
app/controllers/admin/settings/enumerations_controller_base.rb Validates moves and renders morph responses
app/components/admin/enumerations/item_component.rb Provides the shared move menu
app/components/admin/enumerations/item_component.html.erb Adds sortable handle
app/components/admin/enumerations/index_component.rb Configures sortable controllers
app/components/admin/enumerations/index_component.html.erb Provides sortable list and item markup
Review details
  • Files reviewed: 22/22 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants