From 292d2a534c19043b0e06f171f6d1bf954cfc4437 Mon Sep 17 00:00:00 2001 From: MAEDA Go Date: Sun, 26 Apr 2026 14:11:57 +0900 Subject: [PATCH] Add groups_then_users_by_group to Assignee List Display Format --- app/helpers/application_helper.rb | 86 +++++++++++++++-------- app/helpers/settings_helper.rb | 3 +- config/locales/en.yml | 1 + test/functional/issues_controller_test.rb | 31 ++++++++ test/helpers/application_helper_test.rb | 25 +++++++ 5 files changed, 116 insertions(+), 30 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index f89b14995..dd6c8f464 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -687,46 +687,74 @@ module ApplicationHelper s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id) end - involved_principals_html = +'' + involved_principals = [] # This optgroup is displayed only when editing a single issue if @issue.present? && !@issue.new_record? - involved_principals = [@issue.author, @issue.prior_assigned_to].uniq.compact - involved_principals_html = involved_principals.map do |p| - content_tag('option', p.name, value: p.id, disabled: !collection.include?(p)) - end.join + involved_principals = + [@issue.author, @issue.prior_assigned_to].uniq.compact.map do |principal| + [principal, {:disabled => !collection.include?(principal)}] + end end - users_html = +'' - groups_html = +'' - collection.sort.each do |element| - if option_value_selected?(element, selected) || element.id.to_s == selected - selected_attribute = ' selected="selected"' - end - (element.is_a?(Group) ? groups_html : users_html) << - %() - end - if involved_principals_html.blank? && groups_html.blank? - s << users_html + users, groups = collection.sort.partition {|principal| principal.is_a?(User)} + if involved_principals.blank? && groups.blank? + s << principals_option_tags(users, selected) else - principal_optgroups = case Setting.assignee_dropdown_display_format.to_s - when 'groups_then_users' - [ - [l(:label_group_plural), groups_html], - [l(:label_user_plural), users_html] - ] - else - [ - [l(:label_user_plural), users_html], - [l(:label_group_plural), groups_html] - ] - end - ([[l(:label_involved_principals), involved_principals_html]] + principal_optgroups).each do |label, options_html| + optgroups = [[l(:label_involved_principals), involved_principals]] + optgroups.concat( + case Setting.assignee_dropdown_display_format.to_s + when 'groups_then_users' + [ + [l(:label_group_plural), groups], + [l(:label_user_plural), users] + ] + when 'users_by_group' + principal_users_by_group_optgroups_for_select(users, groups) + else + # Default to 'users_then_groups' + [ + [l(:label_user_plural), users], + [l(:label_group_plural), groups] + ] + end + ) + + optgroups.each do |label, principals| + options_html = principals_option_tags(principals, selected) s << %(#{options_html}) if options_html.present? end end s.html_safe end + # Renders option tags for users and groups, preserving per-option attributes. + def principals_option_tags(principals, selected) + principals.map do |principal, options| + options ||= {} + selected_attribute = %( selected="selected") if option_value_selected?(principal, selected) || principal.id.to_s == selected + disabled_attribute = %( disabled="disabled") if options[:disabled] + + %() + end.join + end + + # Builds optgroups that list groups first, then each group's users, then ungrouped users. + def principal_users_by_group_optgroups_for_select(users, groups) + users_by_group_optgroups = + groups.filter_map do |group| + group_user_ids = group.users.ids + group_users = users.select {|user| group_user_ids.include?(user.id)} + [group, group_users] if group_users.present? + end + + users_by_group_ids = users_by_group_optgroups.flat_map {|_, principals| principals.map(&:id)}.uniq + ungrouped_users = users.reject {|user| users_by_group_ids.include?(user.id)} + + [[l(:label_group_plural), groups]] + + users_by_group_optgroups.map {|group, principals| [group.name, principals]} + + [[l(:label_user_plural), ungrouped_users]] + end + def option_tag(name, text, value, selected=nil, options={}) content_tag 'option', value, options.merge(:value => value, :selected => (value == selected)) end diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 7aec3ebb9..89cbc6289 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -203,7 +203,8 @@ module SettingsHelper def assignee_dropdown_display_format_options options = [ [:label_assignee_dropdown_display_format_users_then_groups, 'users_then_groups'], - [:label_assignee_dropdown_display_format_groups_then_users, 'groups_then_users'] + [:label_assignee_dropdown_display_format_groups_then_users, 'groups_then_users'], + [:label_assignee_dropdown_display_format_users_by_group, 'users_by_group'] ] options.map {|label, value| [l(label), value.to_s]} diff --git a/config/locales/en.yml b/config/locales/en.yml index dd275725f..789694d67 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1175,6 +1175,7 @@ en: label_involved_principals: Author / Previous assignee label_assignee_dropdown_display_format_users_then_groups: Users then groups label_assignee_dropdown_display_format_groups_then_users: Groups then users + label_assignee_dropdown_display_format_users_by_group: Users by group label_progressbar: Progress bar label_oauth_permission_admin: Administrate this Redmine label_oauth_admin_access: Administration diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb index 4b7d76087..e02fd8ce8 100644 --- a/test/functional/issues_controller_test.rb +++ b/test/functional/issues_controller_test.rb @@ -4369,6 +4369,37 @@ class IssuesControllerTest < Redmine::ControllerTest end end + def test_new_should_render_users_by_group_in_assignee_select_when_configured + project = Project.find(1) + group_a = Group.find(10) + group_b = Group.find(11) + project.members << Member.new(:principal => group_a, :roles => [Role.givable.first]) + project.members << Member.new(:principal => group_b, :roles => [Role.givable.first]) + + with_settings :issue_group_assignment => '1', :assignee_dropdown_display_format => 'users_by_group' do + @request.session[:user_id] = 2 + get :new, :params => {:project_id => project.id} + assert_response :success + end + + assert_select 'select[name=?]', 'issue[assigned_to_id]' do + assert_select %(optgroup:nth-of-type(1)[label="#{l(:label_group_plural)}"]) do + assert_select 'option[value="10"]', text: 'A Team' + assert_select 'option[value="11"]', text: 'B Team' + end + assert_select 'optgroup:nth-of-type(2)[label="A Team"]' do + assert_select 'option[value="8"]', text: 'User Misc' + end + assert_select 'optgroup:nth-of-type(3)[label="B Team"]' do + assert_select 'option[value="8"]', text: 'User Misc' + end + assert_select %(optgroup:nth-of-type(4)[label="#{l(:label_user_plural)}"]) do + assert_select 'option[value="2"]', text: 'John Smith' + assert_select 'option[value="8"]', 0 + end + end + end + def test_post_create_without_start_date_and_default_start_date_is_not_creation_date with_settings :default_issue_start_date_to_creation_date => 0 do @request.session[:user_id] = 2 diff --git a/test/helpers/application_helper_test.rb b/test/helpers/application_helper_test.rb index f76a2773e..ef0488c8c 100644 --- a/test/helpers/application_helper_test.rb +++ b/test/helpers/application_helper_test.rb @@ -1956,6 +1956,31 @@ class ApplicationHelperTest < Redmine::HelperTest end end + def test_principals_options_for_select_with_users_and_groups_with_users_by_group + User.current = nil + set_language_if_valid 'en' + principals = [User.find(2), User.find(8), Group.find(11), Group.find(10)] + + with_settings :assignee_dropdown_display_format => 'users_by_group' do + result = principals_options_for_select(principals) + + assert_select_in result, 'optgroup:nth-of-type(1)[label="Groups"]' do + assert_select 'option[value="10"]', text: 'A Team' + assert_select 'option[value="11"]', text: 'B Team' + end + assert_select_in result, 'optgroup:nth-of-type(2)[label="A Team"]' do + assert_select 'option[value="8"]', text: 'User Misc' + end + assert_select_in result, 'optgroup:nth-of-type(3)[label="B Team"]' do + assert_select 'option[value="8"]', text: 'User Misc' + end + assert_select_in result, 'optgroup:nth-of-type(4)[label="Users"]' do + assert_select 'option[value="2"]', text: 'John Smith' + assert_select 'option[value="8"]', 0 + end + end + end + def test_principals_options_for_select_with_empty_collection assert_equal '', principals_options_for_select([]) end -- 2.50.1