Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,11 @@ class BorderBoxListComponent < ApplicationComponent
# # @param title [String] header title.
# # @param show_drag_handle [Boolean] whether the header renders a
# # leading drag handle.
# # @param drag_handle_arguments [Hash] forwarded to `Primer::OpenProject::DragHandle`.
# # @param system_arguments [Hash] forwarded to {Header}. List wiring
# # arguments are supplied internally.
# # @return [ViewComponent::Slot]
# def with_header(title: nil, show_drag_handle: false, **system_arguments, &block)
# def with_header(title: nil, show_drag_handle: false, drag_handle_arguments: {}, **system_arguments, &block)
# end
renders_one :header, ->(**system_arguments) {
system_arguments = system_arguments.except(:id, :list_id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ See COPYRIGHT and LICENSE files for more details.
) do |grid| %>
<% if show_drag_handle? %>
<% grid.with_area(:drag_handle, classes: "hide-when-print") do %>
<%= render(Primer::OpenProject::DragHandle.new) %>
<%= render(Primer::OpenProject::DragHandle.new(**drag_handle_arguments)) %>
<% end %>
<% end %>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@ class Header < ApplicationComponent
:interactive,
:collapsed,
:collapsible,
:show_drag_handle
:show_drag_handle,
:drag_handle_arguments

alias_method :show_drag_handle?, :show_drag_handle

Expand All @@ -177,6 +178,7 @@ class Header < ApplicationComponent
# with a toggle button.
# @param show_drag_handle [Boolean] whether the header renders a leading
# drag handle. Defaults to `false`.
# @param drag_handle_arguments [Hash] forwarded to `Primer::OpenProject::DragHandle`.
# @param system_arguments [Hash] forwarded to `Primer::Beta::BorderBox#with_header`.
def initialize(
title: nil,
Expand All @@ -190,6 +192,7 @@ def initialize(
collapsed: false,
collapsible: false,
show_drag_handle: false,
drag_handle_arguments: {},
**system_arguments
)
super()
Expand All @@ -206,6 +209,7 @@ def initialize(
@collapsed = collapsed
@collapsible = collapsible
@show_drag_handle = show_drag_handle
@drag_handle_arguments = drag_handle_arguments
@system_arguments = system_arguments
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,19 @@ See COPYRIGHT and LICENSE files for more details.

++#%>

<%= component_wrapper(data: { controller: "generic-drag-and-drop" }) do %>
<%= flex_layout(data: drop_target_config) do |container| %>
<%= component_wrapper(data: root_data) do %>
<%= flex_layout(
role: :list,
aria: { label: t(:label_type_plural) },
data: drop_target_config
) do |container| %>
<% types.each do |root| %>
<% container.with_row(mt: 3, data: draggable_item_config(root)) do %>
<% container.with_row(
id: "sortable-type-#{root.id}",
role: :listitem,
mt: 3,
data: draggable_item_config(root)
) do %>
<%= render(
OpenProject::Common::BorderBoxListComponent.new(
container: "op-types-group-#{root.id}",
Expand All @@ -41,6 +50,7 @@ See COPYRIGHT and LICENSE files for more details.
<% list.with_header(
collapsed: collapsed?(root),
show_drag_handle: reorderable?(root),
drag_handle_arguments: { data: { sortable_lists__item_target: "handle" } },
count: variants_count(root)
) do |header| %>
<% header.with_title do %>
Expand Down Expand Up @@ -97,6 +107,8 @@ See COPYRIGHT and LICENSE files for more details.
<% end %>
<% end %>
<% end %>
<%= helpers.pagination_links_full(
types,
params: { controller: "/work_package_types/types", action: "index", id: nil, **context_args }
) %>
<% end %>

<%= helpers.pagination_links_full(types) %>
37 changes: 28 additions & 9 deletions app/components/work_package_types/types/grouped_list_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,17 @@ class GroupedListComponent < ApplicationComponent
include OpTurbo::Streamable
include WorkPackageTypes::VariantRoutes

def initialize(types:, expanded_type_id: nil)
def initialize(types:, expanded_type_id: nil, page_args: {})
super()

@types = types
@expanded_type_id = expanded_type_id
@page_args = page_args.presence || { page: types.current_page, per_page: types.per_page }
end

private

attr_reader :types, :expanded_type_id
attr_reader :types, :expanded_type_id, :page_args

def collapsed?(root)
root.id != expanded_type_id
Expand Down Expand Up @@ -89,7 +90,7 @@ def menu_id(type)
end

def menu_src(type)
menu_type_path(type)
menu_type_path(type, **context_args)
end

def variant_menu_id(variant)
Expand All @@ -104,19 +105,37 @@ def reorderable?(type)
!(type.first? && type.last?)
end

def context_args
page_args.merge(expand: expanded_type_id).compact
end

def root_data
{
controller: "sortable-lists",
sortable_lists_move_url_template_value: drop_type_path("__id__", **context_args).sub("__id__", "{id}"),
sortable_lists_sortable_lists__list_outlet: "##{wrapper_key} [data-controller~='sortable-lists--list']",
sortable_lists_sortable_lists__item_outlet: "##{wrapper_key} [data-controller~='sortable-lists--item']"
}
end

def drop_target_config
{
generic_drag_and_drop_target: "container",
"target-allowed-drag-type": "work-package-type"
controller: "sortable-lists--list",
sortable_lists__list_type_value: ::Type.model_name.param_key,
sortable_lists__list_accepted_type_value: ::Type.model_name.param_key,
sortable_lists__list_name_value: t(:label_type_plural)
}
end

def draggable_item_config(root)
{
"draggable-type": "work-package-type",
"draggable-id": root.id,
"drop-url": drop_type_path(root)
}
controller: "sortable-lists--item",
sortable_lists__item_target: "preview",
sortable_lists__item_id_value: root.id,
sortable_lists__item_type_value: ::Type.model_name.param_key,
sortable_lists__item_label_value: root.name,
sortable_lists__item_mobility_value: ("fixed" unless reorderable?(root))
}.compact
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,12 @@ def self.menu_id(type)
"type-#{type.id}-action-menu"
end

def initialize(type:)
def initialize(type:, page_args: {}, expanded_type_id: nil)
super()

@type = type
@page_args = page_args
@expanded_type_id = expanded_type_id
end

def menu_id
Expand All @@ -50,7 +52,7 @@ def menu_id

private

attr_reader :type
attr_reader :type, :page_args, :expanded_type_id

def type_actions(menu)
configure_action(menu)
Expand Down Expand Up @@ -175,8 +177,9 @@ def move_action(menu)
def move_item(submenu, move_to, label, icon)
submenu.with_item(
label:,
href: move_types_path(type, type: { move_to: }),
form_arguments: { method: :post }
tag: :button,
href: move_types_path(type, **page_args, expand: expanded_type_id),
form_arguments: { method: :post, inputs: [{ name: "type[move_to]", value: move_to.to_s }] }

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.

Am I right in assuming that we can't use SortableList::MoveMenu here because of the additional page_args and expanded_type_id arguments? Or is it the first/last detection that is limited for paginated lists?

@myabc myabc Sep 30, 2026 •

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.

Right — SortableLists::MoveMenu works out availability and moves from the visible DOM siblings, so it can't move across pages. Paginated statuses use the same server-backed menu approach. Menus now carry page and expansion context (91023d0).

(Server-backed move menus are hand-rolled in statuses, types, project phase definitions, CF hierarchy items and backlogs. A sibling SortableLists::ServerMoveMenu mixin could share the directions and gating; I'll fold that into the helpers follow-up.)

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.

FYI, I've created DREAM-880 for this.

) do |item|
item.with_leading_visual_icon(icon:)
end
Expand Down
78 changes: 63 additions & 15 deletions app/controllers/work_package_types/types_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ class TypesController < ApplicationController
end

def index
@expanded_type_id = params[:expand].presence&.to_i
@expanded_type_id = expanded_type_id
@page_args = page_args
@types = types_for_index
end

Expand All @@ -52,12 +53,7 @@ def type
end

def move
if @type.update(permitted_params.type_move)
flash[:notice] = I18n.t(:notice_successful_update)
else
flash.now[:error] = I18n.t(:error_type_could_not_be_saved)
end
redirect_to types_path
render_ordering_result(move_in_direction, error_message: I18n.t(:error_type_could_not_be_saved))
end

def destroy
Expand Down Expand Up @@ -93,22 +89,17 @@ def duplicate
end

def drop
unless @type.update(params.permit(:position))
render_error_flash_message_via_turbo_stream(message: @type.errors.full_messages.to_sentence)
end

update_via_turbo_stream(component: Types::GroupedListComponent.new(types: types_for_index))
respond_to_with_turbo_streams
render_ordering_result(move_after_anchor, error_message: I18n.t(:error_invalid_list_move_anchor))
end

def menu
render Types::TypeActionsComponent.new(type: @type), layout: false
render Types::TypeActionsComponent.new(type: @type, page_args:, expanded_type_id:), layout: false
end

protected

def find_type
@type = ::Type.find(params[:id])
@type = ::Type.find(params.expect(:id))
end

def types_for_index
Expand Down Expand Up @@ -158,5 +149,62 @@ def belonging_wps_url(type_id)
def archived_projects
@archived_projects ||= @type.projects.archived
end

private

def page_args
{ page: page_param, per_page: per_page_param }
end

def expanded_type_id
Integer(params[:expand].to_s, exception: false)
end

def ordering_component
Types::GroupedListComponent.new(types: types_for_index, page_args:, expanded_type_id:)
end

def render_ordering_result(moved, error_message:)
if moved
update_via_turbo_stream(component: ordering_component, method: :morph)
render_success_flash_message_via_turbo_stream(message: I18n.t(:notice_successful_update))
else
render_error_flash_message_via_turbo_stream(message: error_message)
end
respond_with_turbo_streams(status: moved ? :ok : :unprocessable_entity)
end

def move_in_direction
type_params = params[:type]
return false unless type_params.is_a?(ActionController::Parameters)

direction = type_params[:move_to]
direction.in?(%w[highest higher lower lowest]) && @type.update(move_to: direction)
end

def valid_drop_request?
params[:list_type] == ::Type.model_name.param_key &&
(params[:list_id].nil? || params[:list_id] == "") &&
params.key?(:prev_id)
end

def move_after_anchor
return false unless valid_drop_request?

predecessor = params[:prev_id]
if predecessor.nil? || predecessor == ""
move_to_page_start
else
@type.move_after_anchor(predecessor, scope: ::Type.all)
end
end

def move_to_page_start
current_page = ::Type.page(page_param).per_page(per_page_param)
return false if current_page.empty?

predecessor = ::Type.offset(current_page.offset - 1).pick(:id) if current_page.offset.positive?
@type.move_after_anchor(predecessor, scope: ::Type.all)
end
end
end
4 changes: 0 additions & 4 deletions app/models/permitted_params.rb
Original file line number Diff line number Diff line change
Expand Up @@ -241,10 +241,6 @@ def type(args = {})
whitelisted
end

def type_move
params.require(:type).permit(*self.class.permitted_attributes[:move_to])
end

def enumerations_move
params.require(:enumeration).permit(*self.class.permitted_attributes[:move_to])
end
Expand Down
2 changes: 2 additions & 0 deletions app/models/type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ class Type < ApplicationRecord
has_many :project_types, dependent: :delete_all
has_many :projects, through: :project_types

include Lists::MoveAfterAnchor

acts_as_list

validates :name,
Expand Down
6 changes: 5 additions & 1 deletion app/views/work_package_types/types/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,11 @@ See COPYRIGHT and LICENSE files for more details.
%>

<% if @types.any? %>
<%= render WorkPackageTypes::Types::GroupedListComponent.new(types: @types, expanded_type_id: @expanded_type_id) %>
<%= render WorkPackageTypes::Types::GroupedListComponent.new(
types: @types,
expanded_type_id: @expanded_type_id,
page_args: @page_args
) %>
<% else %>
<%=
no_results_box(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@
header_padding:,
collapsible:
) do |list| %>
<% list.with_header(title: "Reorderable section", count: true, show_drag_handle: true) %>
<% list.with_header(
title: "Reorderable section",
Comment thread
lwassermann marked this conversation as resolved.
count: true,
show_drag_handle: true,
drag_handle_arguments: { data: { sortable_lists__item_target: "handle" } }
) %>
<% list.with_item do %>
<%= render(Primer::OpenProject::FlexLayout.new(align_items: :center, ml: -2)) do |row| %>
<% row.with_column(style: "width: 1.5rem") { render(Primer::OpenProject::DragHandle.new) } %>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1323,4 +1323,15 @@ def template_placeholder(rendered)
expect(page).to have_no_css("ul [data-empty-list-item]")
end
end

it "forwards drag handle arguments to the handle only" do
rendered = render_inline(described_class.new(container: "sortable-header")) do |list|
list.with_header(title: "Types", show_drag_handle: true,
drag_handle_arguments: { data: { sortable_lists__item_target: "handle" } })
end

expect(rendered).to have_css(".DragHandle[data-sortable-lists--item-target='handle']")
expect(rendered).to have_no_css(".Box-header[data-sortable-lists--item-target]")
expect(rendered).to have_no_css("[drag_handle_arguments]")
end
end
Loading
Loading