From 3eed9fd65864654c3bc102a6f5f912c0ac29b2c9 Mon Sep 17 00:00:00 2001
From: Jens Kraemer <jk@jkraemer.net>
Date: Sat, 22 Feb 2025 18:42:30 +0800
Subject: [PATCH] Configurable columns for related and sub issues lists

Adds a global setting to define the set of columns to display in
sub- and related issues lists.

Also adds a setting to optionally show table headers for these issue
lists.
---
 app/assets/stylesheets/application.css    |   1 +
 app/helpers/issues_helper.rb              | 105 +++++++++++-----------
 app/views/settings/_issues.html.erb       |  19 ++++
 config/locales/de.yml                     |   2 +
 config/locales/en.yml                     |   2 +
 config/settings.yml                       |  10 +++
 test/functional/issues_controller_test.rb |  11 +++
 7 files changed, 97 insertions(+), 53 deletions(-)

diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css
index 96fbb6f8fa..272da5ad0c 100644
--- a/app/assets/stylesheets/application.css
+++ b/app/assets/stylesheets/application.css
@@ -361,6 +361,7 @@ table.list td.reorder {width:15%; white-space:nowrap; text-align:center; }
 table.list table.progress td {padding-right:0px;}
 table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
 table.list tr.overdue td.due_date  { color: #c22; }
+table.list thead.related-issues th { background-color: inherit; font-size: 11px; border: none; }
 #role-permissions-trackers table.list th {white-space:normal;}
 
 .table-list-cell {display: table-cell; vertical-align: top; padding:2px; }
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index b4c87f7585..4e1918cc8d 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -89,9 +89,28 @@ module IssuesHelper
     s.html_safe
   end
 
+  def get_related_issues_columns_for_project(issue)
+    query = IssueQuery.new project: issue.project
+    available_columns = query.available_inline_columns
+    column_names = Setting.related_issues_default_columns
+
+    (column_names - %w[tracker subject]).map do |name|
+      available_columns.find { |f| f.name.to_s == name }
+    end.compact
+  end
+
   def render_descendants_tree(issue)
+    columns_list = get_related_issues_columns_for_project(issue)
+
     manage_relations = User.current.allowed_to?(:manage_subtasks, issue.project)
     s = +'<table class="list issues odd-even">'
+
+    if Setting.display_related_issues_table_headers?
+      headers = [l(:field_subject)]
+      headers += columns_list.map(&:caption)
+      s << content_tag(:thead, content_tag(:tr, safe_join(headers.map{|h| content_tag :th, h})), class: "related-issues")
+    end
+
     issue_list(
       issue.descendants.visible.
         preload(:status, :priority, :tracker,
@@ -115,29 +134,17 @@ module IssuesHelper
           "".html_safe
         end
       buttons << link_to_context_menu
-      s <<
-        content_tag(
-          'tr',
-          content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil),
-                      :class => 'checkbox') +
-             content_tag('td',
-                         link_to_issue(
-                           child,
-                           :project => (issue.project_id != child.project_id)),
-                         :class => 'subject') +
-             content_tag('td', h(child.status), :class => 'status') +
-             content_tag('td', link_to_user(child.assigned_to), :class => 'assigned_to') +
-             content_tag('td', format_date(child.start_date), :class => 'start_date') +
-             content_tag('td', format_date(child.due_date), :class => 'due_date') +
-             content_tag('td',
-                         (if child.disabled_core_fields.include?('done_ratio')
-                            ''
-                          else
-                            progress_bar(child.done_ratio)
-                          end),
-                         :class=> 'done_ratio') +
-             content_tag('td', buttons, :class => 'buttons'),
-          :class => css)
+
+      row_content =
+        content_tag('td', check_box_tag('ids[]', child.id, false, id: nil), class: 'checkbox') +
+        content_tag('td', link_to_issue(child, project: (issue.project_id != child.project_id)), class: 'subject')
+
+      columns_list.each do |column|
+        row_content << content_tag('td', column_content(column, child), class: column.css_classes.to_s)
+      end
+
+      row_content << content_tag('td', buttons, class: 'buttons')
+      s << content_tag('tr', row_content, class: css, id: "issue-#{child.id}").html_safe
     end
     s << '</table>'
     s.html_safe
@@ -199,8 +206,17 @@ module IssuesHelper
 
   # Renders the list of related issues on the issue details view
   def render_issue_relations(issue, relations)
+    columns_list = get_related_issues_columns_for_project(issue)
+
     manage_relations = User.current.allowed_to?(:manage_issue_relations, issue.project)
     s = ''.html_safe
+
+    if Setting.display_related_issues_table_headers?
+      headers = [l(:field_subject)]
+      headers += columns_list.map(&:caption)
+      s = content_tag :thead, content_tag(:tr, safe_join(headers.map{|h| content_tag :th, h})), class: "related-issues"
+    end
+
     relations.each do |relation|
       other_issue = relation.other_issue(issue)
       css = "issue hascontextmenu #{other_issue.css_classes} #{relation.css_classes_for(other_issue)}"
@@ -219,36 +235,19 @@ module IssuesHelper
           "".html_safe
         end
       buttons << link_to_context_menu
-      s <<
-        content_tag(
-          'tr',
-          content_tag('td',
-                      check_box_tag(
-                        "ids[]", other_issue.id,
-                        false, :id => nil),
-                      :class => 'checkbox') +
-             content_tag('td',
-                         relation.to_s(@issue) do |other|
-                           link_to_issue(
-                             other,
-                             :project => Setting.cross_project_issue_relations?
-                           )
-                         end.html_safe,
-                         :class => 'subject') +
-             content_tag('td', other_issue.status, :class => 'status') +
-             content_tag('td', link_to_user(other_issue.assigned_to), :class => 'assigned_to') +
-             content_tag('td', format_date(other_issue.start_date), :class => 'start_date') +
-             content_tag('td', format_date(other_issue.due_date), :class => 'due_date') +
-             content_tag('td',
-                         (if other_issue.disabled_core_fields.include?('done_ratio')
-                            ''
-                          else
-                            progress_bar(other_issue.done_ratio)
-                          end),
-                         :class=> 'done_ratio') +
-             content_tag('td', buttons, :class => 'buttons'),
-          :id => "relation-#{relation.id}",
-          :class => css)
+
+      subject_content = relation.to_s(@issue) { |other| link_to_issue other, project: Setting.cross_project_issue_relations? }.html_safe
+
+      row_content =
+        content_tag('td', check_box_tag('ids[]', other_issue.id, false, id: nil), class: 'checkbox') +
+        content_tag('td', subject_content, class: 'subject')
+
+      columns_list.each do |column|
+        row_content << content_tag('td', column_content(column, other_issue), class: column.css_classes.to_s)
+      end
+
+      row_content << content_tag('td', buttons, class: 'buttons')
+      s << content_tag('tr', row_content, id: "relation-#{relation.id}", class: css)
     end
     content_tag('table', s, :class => 'list issues odd-even')
   end
diff --git a/app/views/settings/_issues.html.erb b/app/views/settings/_issues.html.erb
index 0e978fa6b5..3a3a36d1b0 100644
--- a/app/views/settings/_issues.html.erb
+++ b/app/views/settings/_issues.html.erb
@@ -62,5 +62,24 @@
   </p>
 </fieldset>
 
+<fieldset class="box">
+  <legend><%= l(:setting_related_issues_default_columns) %></legend>
+  <div id="list-definition">
+    <div>
+      <%= render_query_columns_selection(
+            IssueQuery.new(:column_names => Setting.related_issues_default_columns),
+            :name => 'settings[related_issues_default_columns]') %>
+      <%= javascript_tag do %>
+        $('#available_settings_related_issues_default_columns option[value="tracker"]').remove();
+        $('#available_settings_related_issues_default_columns option[value="subject"]').remove();
+      <% end %>
+    </div>
+  </div>
+
+  <div class="tabular settings">
+    <p><%= setting_check_box :display_related_issues_table_headers %></p>
+  </div>
+</fieldset>
+
 <%= submit_tag l(:button_save) %>
 <% end %>
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 4947ead29a..e227ad0778 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -1059,6 +1059,8 @@ de:
   setting_user_format: Benutzer-Anzeigeformat
   setting_welcome_text: Willkommenstext
   setting_wiki_compression: Wiki-Historie komprimieren
+  setting_related_issues_default_columns: Standard-Spalten für verknüpfte und untergeordnete Aufgaben
+  setting_display_related_issues_table_headers: Spaltenüberschriften anzeigen
 
   status_active: aktiv
   status_locked: gesperrt
diff --git a/config/locales/en.yml b/config/locales/en.yml
index aa15095c65..1195b613d6 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -522,6 +522,8 @@ en:
   setting_show_status_changes_in_mail_subject: Show status changes in issue mail notifications subject
   setting_project_list_defaults: Projects list defaults
   setting_twofa: Two-factor authentication
+  setting_related_issues_default_columns: Related and sub issues list defaults
+  setting_display_related_issues_table_headers: Show table headers
 
   permission_add_project: Create project
   permission_add_subprojects: Create subprojects
diff --git a/config/settings.yml b/config/settings.yml
index 48da91a10d..6ce1356103 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -231,6 +231,16 @@ issue_list_default_columns:
 issue_list_default_totals:
   serialized: true
   default: []
+related_issues_default_columns:
+  serialized: true
+  default:
+  - status
+  - assigned_to
+  - start_date
+  - due_date
+  - done_ratio
+display_related_issues_table_headers:
+  default: 0
 display_subprojects_issues:
   default: 1
 time_entry_list_defaults:
diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb
index 2c781bed98..1c631bf070 100644
--- a/test/functional/issues_controller_test.rb
+++ b/test/functional/issues_controller_test.rb
@@ -8837,4 +8837,15 @@ class IssuesControllerTest < Redmine::ControllerTest
       end
     end
   end
+
+  def test_related_issues_columns_setting
+    with_settings :related_issues_default_columns => ['status', 'total_estimated_hours'], display_related_issues_table_headers: 1 do
+      Issue.find(1).update!(parent_id: 2)
+      get :show, params: { id: 2 }
+      assert_response :success
+      assert_select 'thead.related-issues th', text: 'Subject'
+      assert_select 'thead.related-issues th', text: 'Status'
+      assert_select 'thead.related-issues th', text: 'Total estimated time'
+    end
+  end
 end
-- 
2.49.0

