From 98d2d6bfaf47bae644cb65290eb5996bf625f461 Mon Sep 17 00:00:00 2001 From: "CSPLXX-RENAME.name" Date: Wed, 17 Jul 2024 13:12:37 +0300 Subject: [PATCH 01/28] dir/file model --- .../issue_view_columns_issues_helper.rb | 106 +++++++++++++++--- .../_issue_view_columns_settings.html.slim | 11 ++ config/locales/en.yml | 2 + config/settings.yml | 1 + 4 files changed, 105 insertions(+), 15 deletions(-) diff --git a/app/helpers/issue_view_columns_issues_helper.rb b/app/helpers/issue_view_columns_issues_helper.rb index 5d3152d..9f086fe 100644 --- a/app/helpers/issue_view_columns_issues_helper.rb +++ b/app/helpers/issue_view_columns_issues_helper.rb @@ -2,19 +2,27 @@ module IssueViewColumnsIssuesHelper def render_descendants_tree(issue) - columns_list = get_fields_for_project issue - # no field defined, then use render from core redmine (or whatever by other plugins loaded before this) - return super if columns_list.count.zero? + sort_dir_file_model = RedmineIssueViewColumns.setting(:sort_dir_file_model) + columns_list = get_fields_for_project(issue) + + # Proceed with default rendering if no columns are defined + return super if columns_list.empty? + + if sort_dir_file_model == '1' + render_descendants_tree_dir_file_model(issue, columns_list) + else + render_descendants_tree_default(issue, columns_list) + end + end - # continue here if there are fields defined + def render_descendants_tree_dir_file_model(issue, columns_list) field_values = +'' - s = table_start_for_relations columns_list - manage_relations = User.current.allowed_to? :manage_subtasks, issue.project - # set data - issue_list(issue.descendants.visible.preload(:status, :priority, :tracker, :assigned_to).sort_by(&:lft)) do |child, level| - next if child.closed? && !issue_columns_with_closed_issues? + s = table_start_for_relations(columns_list) + manage_relations = User.current.allowed_to?(:manage_subtasks, issue.project) + rendered_issues = Set.new - tr_classes = +"hascontextmenu #{child.css_classes} #{cycle 'odd', 'even'}" + render_issue_row = lambda do |child, level| + tr_classes = +"hascontextmenu #{child.css_classes} #{cycle('odd', 'even')}" tr_classes << " idnt idnt-#{level}" if level.positive? buttons = if manage_relations @@ -32,25 +40,93 @@ def render_descendants_tree(issue) end buttons << link_to_context_menu - field_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') + field_content = content_tag('td', check_box_tag('ids[]', child.id, false, id: nil), class: 'checkbox') + + if child.descendants.any? + expand_icon = content_tag('span', '+', class: 'expand-icon', title: 'Expand') + subject_content = "#{expand_icon} #{link_to_issue(child, project: (issue.project_id != child.project_id))}".html_safe + else + subject_content = link_to_issue(child, project: (issue.project_id != child.project_id)) + end + + field_content << content_tag('td', subject_content, class: 'subject') columns_list.each do |column| field_content << content_tag('td', column_content(column, child), class: column.css_classes.to_s) end field_content << content_tag('td', buttons, class: 'buttons') - field_values << content_tag('tr', field_content, - class: tr_classes, - id: "issue-#{child.id}").html_safe + + content_tag('tr', field_content, class: tr_classes, id: "issue-#{child.id}").html_safe end + render_issue_with_descendants = lambda do |parent, level| + issues_with_subissues = [] + issues_without_subissues = [] + + parent.descendants.visible.preload(:status, :priority, :tracker, :assigned_to).sort_by(&:lft).each do |child| + next if (child.closed? && !issue_columns_with_closed_issues?) || rendered_issues.include?(child.id) + + rendered_issues.add(child.id) + + if child.descendants.any? + issues_with_subissues << render_issue_row.call(child, level) + subissues_with, subissues_without = render_issue_with_descendants.call(child, level + 1) + issues_with_subissues.concat(subissues_with) + issues_with_subissues.concat(subissues_without) + else + issues_without_subissues << render_issue_row.call(child, level) if child.parent_id == parent.id + end + end + + return issues_with_subissues, issues_without_subissues + end + + issues_with_subissues, issues_without_subissues = render_issue_with_descendants.call(issue, 0) + + field_values << issues_with_subissues.join('').html_safe + field_values << issues_without_subissues.join('').html_safe + s << field_values s << table_end_for_relations s.html_safe end + def render_descendants_tree_default(issue, columns_list) + field_values = "".dup # Ensure field_values is mutable + s = String.new("") + + s << content_tag('th', l(:field_subject), style: 'text-align:left') + columns_list.each do |column| + s << content_tag('th', column.caption) + end + + s << content_tag('th', '', style: 'text-align:right') if Redmine::VERSION::MAJOR >= 4 + + issue_list(issue.descendants.visible.preload(:status, :priority, :tracker, :assigned_to).sort_by(&:lft)) do |child, level| + css = "issue issue-#{child.id} hascontextmenu #{child.css_classes}" + css << " idnt idnt-#{level}" if level > 0 + + field_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', style: 'width: 30%') + + columns_list.each do |column| + field_content << content_tag('td', column_content(column, child), class: column.css_classes.to_s) + end + + field_content << content_tag('td', link_to_context_menu, class: 'buttons', style: 'text-align:right') if Redmine::VERSION::MAJOR >= 4 + + field_values << content_tag('tr', field_content, class: css).html_safe + end + + s << field_values + s << '
' + s.html_safe + end + + + # Renders the list of related issues on the issue details view def render_issue_relations(issue, relations) columns_list = get_fields_for_project issue diff --git a/app/views/settings/_issue_view_columns_settings.html.slim b/app/views/settings/_issue_view_columns_settings.html.slim index eaa8466..2c15b15 100644 --- a/app/views/settings/_issue_view_columns_settings.html.slim +++ b/app/views/settings/_issue_view_columns_settings.html.slim @@ -12,8 +12,19 @@ p br +p + = additionals_settings_checkbox :sort_dir_file_model, + label: l(:label_sort_dir_file_model), + active_value: @settings[:sort_dir_file_model], + tag_name: 'settings[sort_dir_file_model]' + em.info + = l :info_sort_dir_file_model + +br + = render 'additionals/settings_list_defaults', query_class: IssueQuery, query_type: 'issue', legend: l(:label_select_issue_view_columns), totalable_columns: false + diff --git a/config/locales/en.yml b/config/locales/en.yml index 36e56eb..df65d1b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -15,3 +15,5 @@ en: info_issue_view_columns_project_settings: By activating the project module "Issue view columns" you can individually define columns for subtasks and issue relationships for the project (if these need to be different from system default). If you want to use system default you have to deactivate this project module. label_issue_view_column: Issue column + label_sort_dir_file_model: Sort using dir/file model + info_sort_dir_file_model: Enable this option to sort issues using the directory/file model. \ No newline at end of file diff --git a/config/settings.yml b/config/settings.yml index aa43f00..4b75f3d 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -1,5 +1,6 @@ --- +sort_dir_file_model: '1' issue_scope: all issue_list_defaults: column_names: From 78199c20d62b91705c6bd3ff636812248e785d92 Mon Sep 17 00:00:00 2001 From: Andrei Munteanu Date: Thu, 18 Jul 2024 18:11:22 +0300 Subject: [PATCH 02/28] Added expand/collapse both on client and server --- app/controllers/custom_issues_controller.rb | 15 +++ .../issue_view_columns_issues_helper.rb | 31 +++-- .../collapse_expand/_collapse_expand.js.erb | 109 ++++++++++++++++++ assets/javascripts/issue_view_columns.js | 2 + config/routes.rb | 5 + db/migrate/003_add_collapsed_ids_to_issues.rb | 5 + .../hooks/view_hook.rb | 3 + 7 files changed, 161 insertions(+), 9 deletions(-) create mode 100644 app/controllers/custom_issues_controller.rb create mode 100644 app/views/collapse_expand/_collapse_expand.js.erb create mode 100644 db/migrate/003_add_collapsed_ids_to_issues.rb diff --git a/app/controllers/custom_issues_controller.rb b/app/controllers/custom_issues_controller.rb new file mode 100644 index 0000000..50836c9 --- /dev/null +++ b/app/controllers/custom_issues_controller.rb @@ -0,0 +1,15 @@ +class CustomIssuesController < ApplicationController + skip_before_action :verify_authenticity_token, only: :update_collapsed_ids + + def update_collapsed_ids + @issue = Issue.find(params[:id]) + + json_data = JSON.parse(request.body.read) + if @issue.update(collapsed_ids: json_data["collapsed_ids"]) + render json: { message: 'Collapsed IDs updated successfully' }, status: :ok + else + render json: { error: 'Failed to update collapsed IDs' }, status: :unprocessable_entity + end + + end +end diff --git a/app/helpers/issue_view_columns_issues_helper.rb b/app/helpers/issue_view_columns_issues_helper.rb index 9f086fe..934eba2 100644 --- a/app/helpers/issue_view_columns_issues_helper.rb +++ b/app/helpers/issue_view_columns_issues_helper.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true module IssueViewColumnsIssuesHelper + def render_descendants_tree(issue) sort_dir_file_model = RedmineIssueViewColumns.setting(:sort_dir_file_model) columns_list = get_fields_for_project(issue) @@ -16,12 +17,17 @@ def render_descendants_tree(issue) end def render_descendants_tree_dir_file_model(issue, columns_list) + @issue = Issue.find(params[:id]) + + # Initialize variables + collapsed_ids = issue.collapsed_ids.to_s.split.map(&:to_i) + field_values = +'' s = table_start_for_relations(columns_list) manage_relations = User.current.allowed_to?(:manage_subtasks, issue.project) rendered_issues = Set.new - render_issue_row = lambda do |child, level| + render_issue_row = lambda do |child, level, hidden = false| tr_classes = +"hascontextmenu #{child.css_classes} #{cycle('odd', 'even')}" tr_classes << " idnt idnt-#{level}" if level.positive? @@ -43,7 +49,8 @@ def render_descendants_tree_dir_file_model(issue, columns_list) field_content = content_tag('td', check_box_tag('ids[]', child.id, false, id: nil), class: 'checkbox') if child.descendants.any? - expand_icon = content_tag('span', '+', class: 'expand-icon', title: 'Expand') + icon_class = collapsed_ids.include?(child.id) ? 'icon icon-toggle-plus' : 'icon icon-toggle-minus' + expand_icon = content_tag('span', '', class: icon_class, onclick: 'collapseExpand(this)') subject_content = "#{expand_icon} #{link_to_issue(child, project: (issue.project_id != child.project_id))}".html_safe else subject_content = link_to_issue(child, project: (issue.project_id != child.project_id)) @@ -57,10 +64,12 @@ def render_descendants_tree_dir_file_model(issue, columns_list) field_content << content_tag('td', buttons, class: 'buttons') - content_tag('tr', field_content, class: tr_classes, id: "issue-#{child.id}").html_safe + # Add style attribute to hide the row if hidden is true + row_style = hidden ? 'display: none;' : '' + content_tag('tr', field_content, class: tr_classes, id: "issue-#{child.id}", style: row_style).html_safe end - render_issue_with_descendants = lambda do |parent, level| + render_issue_with_descendants = lambda do |parent, level, hidden = false| issues_with_subissues = [] issues_without_subissues = [] @@ -69,21 +78,26 @@ def render_descendants_tree_dir_file_model(issue, columns_list) rendered_issues.add(child.id) + child_hidden = hidden || collapsed_ids.include?(child.id) + if child.descendants.any? - issues_with_subissues << render_issue_row.call(child, level) - subissues_with, subissues_without = render_issue_with_descendants.call(child, level + 1) + issues_with_subissues << render_issue_row.call(child, level, hidden) + subissues_with, subissues_without = render_issue_with_descendants.call(child, level + 1, child_hidden) issues_with_subissues.concat(subissues_with) issues_with_subissues.concat(subissues_without) else - issues_without_subissues << render_issue_row.call(child, level) if child.parent_id == parent.id + issues_without_subissues << render_issue_row.call(child, level, child_hidden) if child.parent_id == parent.id end end return issues_with_subissues, issues_without_subissues end + + # Initial call to render the top-level issues issues_with_subissues, issues_without_subissues = render_issue_with_descendants.call(issue, 0) + # Append issues with subissues first, then issues without subissues field_values << issues_with_subissues.join('').html_safe field_values << issues_without_subissues.join('').html_safe @@ -93,6 +107,7 @@ def render_descendants_tree_dir_file_model(issue, columns_list) s.html_safe end + def render_descendants_tree_default(issue, columns_list) field_values = "".dup # Ensure field_values is mutable s = String.new("") @@ -125,8 +140,6 @@ def render_descendants_tree_default(issue, columns_list) s.html_safe end - - # Renders the list of related issues on the issue details view def render_issue_relations(issue, relations) columns_list = get_fields_for_project issue diff --git a/app/views/collapse_expand/_collapse_expand.js.erb b/app/views/collapse_expand/_collapse_expand.js.erb new file mode 100644 index 0000000..08465e1 --- /dev/null +++ b/app/views/collapse_expand/_collapse_expand.js.erb @@ -0,0 +1,109 @@ + + \ No newline at end of file diff --git a/assets/javascripts/issue_view_columns.js b/assets/javascripts/issue_view_columns.js index a8b3c2b..e7f105c 100644 --- a/assets/javascripts/issue_view_columns.js +++ b/assets/javascripts/issue_view_columns.js @@ -4,4 +4,6 @@ $(function() { $('#available_settings_issue_list_defaults_column_names option[value="subject"]').remove(); $('#tab-content-issue_view_columns #available_c option[value="tracker"]').remove(); $('#tab-content-issue_view_columns #available_c option[value="subject"]').remove(); + }); + diff --git a/config/routes.rb b/config/routes.rb index da36f48..5743d72 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -4,4 +4,9 @@ resources :projects, only: [] do resource :issue_view_columns, only: %i[update] end + resources :custom_issues do + member do + patch 'update_collapsed_ids' + end + end end diff --git a/db/migrate/003_add_collapsed_ids_to_issues.rb b/db/migrate/003_add_collapsed_ids_to_issues.rb new file mode 100644 index 0000000..0604192 --- /dev/null +++ b/db/migrate/003_add_collapsed_ids_to_issues.rb @@ -0,0 +1,5 @@ +class AddCollapsedIdsToIssues < ActiveRecord::Migration[6.0] + def change + add_column :issues, :collapsed_ids, :text + end +end diff --git a/lib/redmine_issue_view_columns/hooks/view_hook.rb b/lib/redmine_issue_view_columns/hooks/view_hook.rb index b375999..9371f67 100644 --- a/lib/redmine_issue_view_columns/hooks/view_hook.rb +++ b/lib/redmine_issue_view_columns/hooks/view_hook.rb @@ -11,5 +11,8 @@ module Hooks class ViewHook < Redmine::Hook::ViewListener render_on :view_issues_show_description_bottom, partial: 'issues/columns_issue_description_bottom' end + class CollapseExpandHook < Redmine::Hook::ViewListener + render_on :view_issues_show_description_bottom, partial: 'collapse_expand/collapse_expand.js' + end end end From 2fbfa43defbea6a57d6ed2fbaa5ac7744c232b88 Mon Sep 17 00:00:00 2001 From: Andrei Munteanu Date: Tue, 23 Jul 2024 15:39:51 +0300 Subject: [PATCH 03/28] Implemented dir/file model , sorting criteria and min-width for columns --- app/controllers/custom_issues_controller.rb | 6 +- .../issue_view_columns_controller.rb | 2 +- .../issue_view_columns_issues_helper.rb | 308 ++++++++++++------ .../issue_view_columns_projects_helper.rb | 6 +- .../collapse_expand/_collapse_expand.js.erb | 46 +-- .../_issue_view_columns_settings.html.slim | 19 ++ assets/stylesheets/issue_view_columns.css | 16 + config/locales/de.yml | 10 + config/locales/en.yml | 9 +- config/routes.rb | 1 + config/settings.yml | 1 + 11 files changed, 293 insertions(+), 131 deletions(-) diff --git a/app/controllers/custom_issues_controller.rb b/app/controllers/custom_issues_controller.rb index 50836c9..f364e1d 100644 --- a/app/controllers/custom_issues_controller.rb +++ b/app/controllers/custom_issues_controller.rb @@ -1,15 +1,15 @@ class CustomIssuesController < ApplicationController skip_before_action :verify_authenticity_token, only: :update_collapsed_ids + #Controller used for modifying collapsed_ids field of an issue def update_collapsed_ids @issue = Issue.find(params[:id]) json_data = JSON.parse(request.body.read) if @issue.update(collapsed_ids: json_data["collapsed_ids"]) - render json: { message: 'Collapsed IDs updated successfully' }, status: :ok + render json: { message: "Collapsed IDs updated successfully" }, status: :ok else - render json: { error: 'Failed to update collapsed IDs' }, status: :unprocessable_entity + render json: { error: "Failed to update collapsed IDs" }, status: :unprocessable_entity end - end end diff --git a/app/controllers/issue_view_columns_controller.rb b/app/controllers/issue_view_columns_controller.rb index e9eddf3..491639e 100644 --- a/app/controllers/issue_view_columns_controller.rb +++ b/app/controllers/issue_view_columns_controller.rb @@ -25,6 +25,6 @@ def update c.save! end - redirect_to settings_project_path(@project, tab: 'issue_view_columns'), notice: l(:label_issue_columns_created_sucessfully) + redirect_to settings_project_path(@project, tab: "issue_view_columns"), notice: l(:label_issue_columns_created_sucessfully) end end diff --git a/app/helpers/issue_view_columns_issues_helper.rb b/app/helpers/issue_view_columns_issues_helper.rb index 934eba2..3cbd9a9 100644 --- a/app/helpers/issue_view_columns_issues_helper.rb +++ b/app/helpers/issue_view_columns_issues_helper.rb @@ -1,85 +1,115 @@ # frozen_string_literal: true module IssueViewColumnsIssuesHelper - def render_descendants_tree(issue) - sort_dir_file_model = RedmineIssueViewColumns.setting(:sort_dir_file_model) + # Retrieve the list of columns to display for the project columns_list = get_fields_for_project(issue) - - # Proceed with default rendering if no columns are defined return super if columns_list.empty? - if sort_dir_file_model == '1' - render_descendants_tree_dir_file_model(issue, columns_list) - else - render_descendants_tree_default(issue, columns_list) + # Retrieve minimum width settings for columns + min_width_setting = RedmineIssueViewColumns.setting(:columns_min_width) + min_widths = {} + if min_width_setting.present? + min_width_setting.split(",").each do |column_setting| + column_name, min_width = column_setting.split(":").map(&:strip) + min_widths[column_name] = min_width + end end - end - def render_descendants_tree_dir_file_model(issue, columns_list) - @issue = Issue.find(params[:id]) - - # Initialize variables + # Retrieve sorting settings and determine if sorting by directory/file model is enabled + sort_dir_file_model = RedmineIssueViewColumns.setting(:sort_dir_file_model) collapsed_ids = issue.collapsed_ids.to_s.split.map(&:to_i) - - field_values = +'' + field_values = +"" s = table_start_for_relations(columns_list) manage_relations = User.current.allowed_to?(:manage_subtasks, issue.project) rendered_issues = Set.new - render_issue_row = lambda do |child, level, hidden = false| - tr_classes = +"hascontextmenu #{child.css_classes} #{cycle('odd', 'even')}" - tr_classes << " idnt idnt-#{level}" if level.positive? + # Determine which rendering method to use based on sorting model + if sort_dir_file_model == "1" + render_issues_dir_file_model(issue, ->(child, level, hidden) { + render_issue_row(child, level, hidden, columns_list, min_widths, manage_relations, collapsed_ids, issue) + }, collapsed_ids, rendered_issues, columns_list, field_values) + else + render_issues_default(issue, ->(child, level, hidden) { + render_issue_row(child, level, hidden, columns_list, min_widths, manage_relations, collapsed_ids, issue) + }, collapsed_ids, columns_list, field_values) + end - buttons = if manage_relations - link_to l(:label_delete_link_to_subtask), - issue_path({ id: child.id, - issue: { parent_issue_id: '' }, - back_url: issue_path(issue.id), - no_flash: '1' }), - method: :put, - data: { confirm: l(:text_are_you_sure) }, - title: l(:label_delete_link_to_subtask), - class: 'icon-only icon-link-break' - else - ''.html_safe - end - buttons << link_to_context_menu + # Append the rendered field values and end the relations table + s << field_values + s << table_end_for_relations - field_content = content_tag('td', check_box_tag('ids[]', child.id, false, id: nil), class: 'checkbox') + s.html_safe + end - if child.descendants.any? - icon_class = collapsed_ids.include?(child.id) ? 'icon icon-toggle-plus' : 'icon icon-toggle-minus' - expand_icon = content_tag('span', '', class: icon_class, onclick: 'collapseExpand(this)') - subject_content = "#{expand_icon} #{link_to_issue(child, project: (issue.project_id != child.project_id))}".html_safe + def render_issue_row(child, level, hidden = false, columns_list, min_widths, manage_relations, collapsed_ids, issue) + # Construct the row classes with context menu and alternating row colors + tr_classes = +"hascontextmenu #{child.css_classes}" + tr_classes << " #{cycle("odd", "even")}" unless hidden + tr_classes << " idnt-#{level}" if level.positive? + + # Generate buttons for deleting if the user has the right permissions + buttons = if manage_relations + link_to l(:label_delete_link_to_subtask), + issue_path(id: child.id, + issue: { parent_issue_id: "" }, + back_url: issue_path(issue.id), + no_flash: "1"), + method: :put, + data: { confirm: l(:text_are_you_sure) }, + title: l(:label_delete_link_to_subtask), + class: "icon-only icon-link-break" else - subject_content = link_to_issue(child, project: (issue.project_id != child.project_id)) + "".html_safe end + buttons << link_to_context_menu - field_content << content_tag('td', subject_content, class: 'subject') + # Build the content for each table cell + field_content = content_tag("td", check_box_tag("ids[]", child.id, false, id: nil), class: "checkbox") - columns_list.each do |column| - field_content << content_tag('td', column_content(column, child), class: column.css_classes.to_s) - end + if child.descendants.any? + # Generate toggle icon for expanding/collapsing subissues + icon_class = collapsed_ids.include?(child.id) ? "icon icon-toggle-plus" : "icon icon-toggle-minus" + expand_icon = content_tag("span", "", class: icon_class, onclick: "collapseExpand(this)") + subject_content = "#{expand_icon} #{link_to_issue(child, project: (issue.project_id != child.project_id))}".html_safe + else + subject_content = link_to_issue(child, project: (issue.project_id != child.project_id)) + end - field_content << content_tag('td', buttons, class: 'buttons') + field_content << content_tag("td", subject_content, class: "subject") - # Add style attribute to hide the row if hidden is true - row_style = hidden ? 'display: none;' : '' - content_tag('tr', field_content, class: tr_classes, id: "issue-#{child.id}", style: row_style).html_safe + # Add columns with their respective content and minimum width style + columns_list.each do |column| + column_name = column.caption # Convert symbol to string + min_width_style = min_widths[column_name].present? ? "min-width: #{min_widths[column_name]};" : "" + field_content << content_tag("td", column_content(column, child), class: column.css_classes.to_s, style: min_width_style) end + field_content << content_tag("td", buttons, class: "buttons") + + # Apply style to hide the row if hidden is true + row_style = hidden ? "display: none;" : "" + content_tag("tr", field_content, class: tr_classes, id: "issue-#{child.id}", style: row_style).html_safe + end + + def render_issues_dir_file_model(issue, render_issue_row, collapsed_ids, rendered_issues, columns_list, field_values) render_issue_with_descendants = lambda do |parent, level, hidden = false| issues_with_subissues = [] issues_without_subissues = [] - parent.descendants.visible.preload(:status, :priority, :tracker, :assigned_to).sort_by(&:lft).each do |child| + # Get direct descendants and sort them + direct_descendants = parent.descendants.select { |descendant| descendant.parent_id == parent.id } + sorted_issues = sort_issues(direct_descendants, columns_list) + + sorted_issues.each do |child| next if (child.closed? && !issue_columns_with_closed_issues?) || rendered_issues.include?(child.id) rendered_issues.add(child.id) child_hidden = hidden || collapsed_ids.include?(child.id) + # Append the folders(child with descendants) before the files(child without descendants) + # Traverse sorted issues recursevely if child.descendants.any? issues_with_subissues << render_issue_row.call(child, level, hidden) subissues_with, subissues_without = render_issue_with_descendants.call(child, level + 1, child_hidden) @@ -93,51 +123,121 @@ def render_descendants_tree_dir_file_model(issue, columns_list) return issues_with_subissues, issues_without_subissues end - - # Initial call to render the top-level issues + # Start rendering from the top-level issue issues_with_subissues, issues_without_subissues = render_issue_with_descendants.call(issue, 0) - # Append issues with subissues first, then issues without subissues - field_values << issues_with_subissues.join('').html_safe - field_values << issues_without_subissues.join('').html_safe + # Append the rendered issues to the field values + field_values << issues_with_subissues.join("").html_safe + field_values << issues_without_subissues.join("").html_safe + end - s << field_values - s << table_end_for_relations + def render_issues_default(issue, render_issue_row, collapsed_ids, columns_list, field_values) + render_issue_with_descendants = lambda do |parent, level, hidden = false| + issues = [] - s.html_safe - end + # Get direct descendants and sort them + direct_descendants = parent.descendants.select { |descendant| descendant.parent_id == parent.id } + sorted_issues = sort_issues(direct_descendants, columns_list) + # Traverse sorted issues recursevely + sorted_issues.each do |child| + next if (child.closed? && !issue_columns_with_closed_issues?) - def render_descendants_tree_default(issue, columns_list) - field_values = "".dup # Ensure field_values is mutable - s = String.new("
") + child_hidden = hidden || collapsed_ids.include?(child.id) - s << content_tag('th', l(:field_subject), style: 'text-align:left') - columns_list.each do |column| - s << content_tag('th', column.caption) + issues << render_issue_row.call(child, level, hidden) + subissues = render_issue_with_descendants.call(child, level + 1, child_hidden) + issues.concat(subissues) + end + + issues end - s << content_tag('th', '', style: 'text-align:right') if Redmine::VERSION::MAJOR >= 4 + # Start rendering from the root issue + rendered_issues = render_issue_with_descendants.call(issue, 0, false) + field_values << rendered_issues.join("").html_safe + end - issue_list(issue.descendants.visible.preload(:status, :priority, :tracker, :assigned_to).sort_by(&:lft)) do |child, level| - css = "issue issue-#{child.id} hascontextmenu #{child.css_classes}" - css << " idnt idnt-#{level}" if level > 0 + def sort_issues(issues, columns_list) + columns_sorting_setting = RedmineIssueViewColumns.setting(:columns_sorting) + return issues unless columns_sorting_setting.present? - field_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', style: 'width: 30%') + # Build sorting criteria as an array of hashes with keys :column_name and :direction + sorting_criteria = columns_sorting_setting.split(",").map do |column_setting| + column_name, direction = column_setting.split(":").map(&:strip) + { column_name: caption_to_name(column_name, columns_list).downcase, direction: direction } + end - columns_list.each do |column| - field_content << content_tag('td', column_content(column, child), class: column.css_classes.to_s) - end + # Use the extracted comparison lambda + sorted_issues = issues.to_a.sort(&comparison_lambda(sorting_criteria)) - field_content << content_tag('td', link_to_context_menu, class: 'buttons', style: 'text-align:right') if Redmine::VERSION::MAJOR >= 4 + sorted_issues + end - field_values << content_tag('tr', field_content, class: css).html_safe + # Define a method for comparison lambda + def comparison_lambda(sorting_criteria) + lambda do |a, b| + sorting_criteria.each do |criterion| + column_name = criterion[:column_name] + direction = criterion[:direction] == "ASC" ? 1 : -1 + + if column_name.start_with?("cf_") + # Handle custom fields + cf_id = column_name.sub(/^cf_/, "") + a_value = CustomValue.where(customized_id: a.id, customized_type: "Issue", custom_field_id: cf_id).first&.value + b_value = CustomValue.where(customized_id: b.id, customized_type: "Issue", custom_field_id: cf_id).first&.value + else + # Handle regular fields + a_value = get_nested_attribute_value(a, column_name) rescue nil + b_value = get_nested_attribute_value(b, column_name) rescue nil + end + + comparison = if a_value.nil? && b_value.nil? + 0 + elsif a_value.nil? + -1 + elsif b_value.nil? + 1 + else + case a_value + when Numeric + a_value <=> b_value + when String + a_value.to_s <=> b_value.to_s + when Enumerable + a_value.length <=> b_value.length + when User + (a_value.firstname + a_value.lastname) <=> (b_value.firstname + b_value.lastname) + when ActiveRecord::Base + a_value.respond_to?(:name) ? a_value.name <=> b_value.name : a_value.id <=> b_value.id + else + a_value.to_s <=> b_value.to_s + end + end + + # If comparison is not zero, return it adjusted by direction + return comparison * direction if comparison != 0 + end + 0 end + end - s << field_values - s << '
' - s.html_safe + def caption_to_name(caption, columns_list) + # Create a mapping from column caption to column name + caption_to_name_map = columns_list.each_with_object({}) do |column, hash| + hash[column.caption] = column.name.to_s + end + + # Return the column name corresponding to the given caption + caption_to_name_map[caption] || caption + end + + # Retrieves a nested attribute value from an object based on a dot-separated attribute path ( used for parent.subject ) + def get_nested_attribute_value(object, attribute_path) + attribute_parts = attribute_path.split(".") + attribute_parts.inject(object) do |current_object, method| + current_object.public_send(method) if current_object + end end # Renders the list of related issues on the issue details view @@ -151,32 +251,32 @@ def render_issue_relations(issue, relations) other_issue = relation.other_issue issue next if other_issue.closed? && !issue_columns_with_closed_issues? - tr_classes = "hascontextmenu #{other_issue.css_classes} #{cycle 'odd', 'even'} #{relation.css_classes_for other_issue}" + tr_classes = "hascontextmenu #{other_issue.css_classes} #{cycle "odd", "even"} #{relation.css_classes_for other_issue}" buttons = if manage_relations - link_to l(:label_relation_delete), - relation_path(relation), - remote: true, - method: :delete, - data: { confirm: l(:text_are_you_sure) }, - title: l(:label_relation_delete), - class: 'icon-only icon-link-break' - else - ''.html_safe - end + link_to l(:label_relation_delete), + relation_path(relation), + remote: true, + method: :delete, + data: { confirm: l(:text_are_you_sure) }, + title: l(:label_relation_delete), + class: "icon-only icon-link-break" + else + "".html_safe + end buttons << link_to_context_menu subject_content = relation.to_s(@issue) { |other| link_to_issue other, project: Setting.cross_project_issue_relations? }.html_safe - field_content = content_tag('td', check_box_tag('ids[]', other_issue.id, false, id: nil), class: 'checkbox') + - content_tag('td', subject_content, class: 'subject') + field_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| - field_content << content_tag('td', column_content(column, other_issue), class: column.css_classes.to_s) + field_content << content_tag("td", column_content(column, other_issue), class: column.css_classes.to_s) end - field_content << content_tag('td', buttons, class: 'buttons') + field_content << content_tag("td", buttons, class: "buttons") - s << content_tag('tr', field_content, + s << content_tag("tr", field_content, id: "relation-#{relation.id}", class: tr_classes) end @@ -195,16 +295,16 @@ def issue_columns_with_closed_issues? issue_scope = RedmineIssueViewColumns.setting :issue_scope return true if issue_scope_with_closed? issue_scope - @issue_columns_with_closed_issues = if issue_scope == 'without_closed_by_default' - RedminePluginKit.true? params[:with_closed_issues] - else - RedminePluginKit.false? params[:without_closed_issues] - end + @issue_columns_with_closed_issues = if issue_scope == "without_closed_by_default" + RedminePluginKit.true? params[:with_closed_issues] + else + RedminePluginKit.false? params[:without_closed_issues] + end end def link_to_closed_issues(issue, issue_scope) - css_class = 'closed-issue-switcher' - if issue_scope == 'without_closed_by_default' + css_class = "closed-issue-switcher" + if issue_scope == "without_closed_by_default" if issue_columns_with_closed_issues? link_to l(:label_hide_closed_issues), issue_path(issue), class: "#{css_class} hide-switch" else @@ -222,18 +322,18 @@ def link_to_closed_issues(issue, issue_scope) def table_start_for_relations(columns_list) s = +'
' - s << content_tag('th', l(:field_subject), class: 'subject') + s << content_tag("th", l(:field_subject), class: "subject") columns_list.each do |column| - s << content_tag('th', column.caption, class: column.name) + s << content_tag("th", column.caption, class: column.name) end - s << content_tag('th', '', class: 'buttons') - s << '' + s << content_tag("th", "", class: "buttons") + s << "" s end def table_end_for_relations - '
' + "" end def get_fields_for_project(issue) diff --git a/app/helpers/issue_view_columns_projects_helper.rb b/app/helpers/issue_view_columns_projects_helper.rb index 8df51a9..0773053 100644 --- a/app/helpers/issue_view_columns_projects_helper.rb +++ b/app/helpers/issue_view_columns_projects_helper.rb @@ -5,9 +5,9 @@ def project_settings_tabs tabs = super if User.current.allowed_to? :manage_issue_view_columns, @project - tabs << { name: 'issue_view_columns', + tabs << { name: "issue_view_columns", action: :issue_view_columns, - partial: 'projects/settings/issue_view_columns', + partial: "projects/settings/issue_view_columns", label: :issue_view_columns_settings } end @@ -16,7 +16,7 @@ def project_settings_tabs def build_query_for_project @selected_columns = IssueViewColumn.where(project_id: @project).sorted.pluck(:name) - @selected_columns = ['#'] if @selected_columns.count.zero? + @selected_columns = ["#"] if @selected_columns.count.zero? @query = IssueQuery.new column_names: @selected_columns @query.project = @project @query diff --git a/app/views/collapse_expand/_collapse_expand.js.erb b/app/views/collapse_expand/_collapse_expand.js.erb index 08465e1..0d358f7 100644 --- a/app/views/collapse_expand/_collapse_expand.js.erb +++ b/app/views/collapse_expand/_collapse_expand.js.erb @@ -1,16 +1,14 @@ - \ No newline at end of file + diff --git a/app/views/settings/_issue_view_columns_settings.html.slim b/app/views/settings/_issue_view_columns_settings.html.slim index 2c15b15..dc721dc 100644 --- a/app/views/settings/_issue_view_columns_settings.html.slim +++ b/app/views/settings/_issue_view_columns_settings.html.slim @@ -28,3 +28,22 @@ br legend: l(:label_select_issue_view_columns), totalable_columns: false +br + +p + = additionals_settings_textfield :columns_min_width, + value: @settings[:columns_min_width], + label: l(:label_columns_min_width), + tag_name: 'settings[columns_min_width]' + em.info + = l :info_columns_min_width + +br + +p + = additionals_settings_textfield :columns_sorting, + value: @settings[:columns_sorting], + label: l(:label_columns_sorting), + tag_name: 'settings[columns_sorting]' + em.info + = l :info_columns_sorting diff --git a/assets/stylesheets/issue_view_columns.css b/assets/stylesheets/issue_view_columns.css index 7521f3f..5c19609 100644 --- a/assets/stylesheets/issue_view_columns.css +++ b/assets/stylesheets/issue_view_columns.css @@ -35,3 +35,19 @@ padding: 5px !important; } } + +#sorting_criteria_box{ + display: flex; + justify-content: space-around; + align-items: center; +} + +.capitalize-text { + text-transform: capitalize; +} + +#input_header{ + display: flex; + justify-content: space-around; + align-items: center; +} \ No newline at end of file diff --git a/config/locales/de.yml b/config/locales/de.yml index 80e463e..53ec758 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -15,3 +15,13 @@ de: info_issue_view_columns_project_settings: Durch Aktivierung des Projekt-Modul "Spalten in Ticketansicht" kann man individuell für das Projekt Spalten für Untertickets und Ticketbeziehungen festlegen (falls diese vom der systemweiten Einstellung abweichen sollen). Wenn die systemweite Einstellung verwendet werden soll, muss das Projekt Module deaktiviert werden. label_issue_view_column: Ticketspalte + label_sort_dir_file_model: Sortierung nach Ordner-/Dateimodell + info_sort_dir_file_model: Aktivieren Sie diese Option, um Probleme nach dem Ordner-/Dateimodell zu sortieren. + label_sorting_priority_columns: Spaltenpriorität beim Sortieren + label_sort_order: Sortierreihenfolge + label_sorting_columns: Sortierkriterien und Spalten-Mindestbreite festlegen + label_columns_min_width: Mindestbreite für Spalten festlegen + info_columns_min_width: "Geben Sie die Mindestbreite für jede Spalte an. Beispiel: Projekt:200px, Tracker:30vw" + label_columns_sorting: Sortierreihenfolge für Spalten festlegen + info_columns_sorting: "Geben Sie die Sortierreihenfolge für jede Spalte an. Verwenden Sie das Format 'Spaltenname:ASC' für aufsteigend und 'Spaltenname:DESC' für absteigend. Beispiel: 'Projekt:ASC, Aktualisiert:DESC'." + diff --git a/config/locales/en.yml b/config/locales/en.yml index df65d1b..bf94711 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -16,4 +16,11 @@ en: If you want to use system default you have to deactivate this project module. label_issue_view_column: Issue column label_sort_dir_file_model: Sort using dir/file model - info_sort_dir_file_model: Enable this option to sort issues using the directory/file model. \ No newline at end of file + info_sort_dir_file_model: Enable this option to sort issues using the directory/file model. + label_sorting_priority_columns: Column priority in sorting + label_sort_order: Sorting order + label_sorting_columns: Set sorting criteria and column min-width + label_columns_min_width: Set min-width property for columns + info_columns_min_width: "Specify the minimum width for each column. Example: Project:200px, Tracker:30vw" + label_columns_sorting: Set sorting order for columns + info_columns_sorting: "Specify the sorting order for each column. Use the format 'ColumnName:ASC' for ascending and 'ColumnName:DESC' for descending. Example: 'Project:ASC, Updated:DESC'." diff --git a/config/routes.rb b/config/routes.rb index 5743d72..efe169e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -9,4 +9,5 @@ patch 'update_collapsed_ids' end end + patch 'settings/update_column_settings', to: 'column_settings#update_column_settings' end diff --git a/config/settings.yml b/config/settings.yml index 4b75f3d..ccbde32 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -4,3 +4,4 @@ sort_dir_file_model: '1' issue_scope: all issue_list_defaults: column_names: + From 8e60a8b8b52cd5888d98e05892045e880056f1f2 Mon Sep 17 00:00:00 2001 From: Andrei Munteanu Date: Tue, 23 Jul 2024 16:45:07 +0300 Subject: [PATCH 04/28] Implemented dir/file model , sorting criteria and min-width for columns --- config/locales/de.yml | 2 +- config/locales/en.yml | 2 +- config/routes.rb | 1 - config/settings.yml | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/config/locales/de.yml b/config/locales/de.yml index 53ec758..53461d5 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -21,7 +21,7 @@ de: label_sort_order: Sortierreihenfolge label_sorting_columns: Sortierkriterien und Spalten-Mindestbreite festlegen label_columns_min_width: Mindestbreite für Spalten festlegen - info_columns_min_width: "Geben Sie die Mindestbreite für jede Spalte an. Beispiel: Projekt:200px, Tracker:30vw" + info_columns_min_width: "Geben Sie die Mindestbreite für jede Spalte an. Beispiel: Projekt:200px, Status:30vw" label_columns_sorting: Sortierreihenfolge für Spalten festlegen info_columns_sorting: "Geben Sie die Sortierreihenfolge für jede Spalte an. Verwenden Sie das Format 'Spaltenname:ASC' für aufsteigend und 'Spaltenname:DESC' für absteigend. Beispiel: 'Projekt:ASC, Aktualisiert:DESC'." diff --git a/config/locales/en.yml b/config/locales/en.yml index bf94711..70742d5 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -21,6 +21,6 @@ en: label_sort_order: Sorting order label_sorting_columns: Set sorting criteria and column min-width label_columns_min_width: Set min-width property for columns - info_columns_min_width: "Specify the minimum width for each column. Example: Project:200px, Tracker:30vw" + info_columns_min_width: "Specify the minimum width for each column. Example: Project:200px, Status:30vw" label_columns_sorting: Set sorting order for columns info_columns_sorting: "Specify the sorting order for each column. Use the format 'ColumnName:ASC' for ascending and 'ColumnName:DESC' for descending. Example: 'Project:ASC, Updated:DESC'." diff --git a/config/routes.rb b/config/routes.rb index efe169e..5743d72 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -9,5 +9,4 @@ patch 'update_collapsed_ids' end end - patch 'settings/update_column_settings', to: 'column_settings#update_column_settings' end diff --git a/config/settings.yml b/config/settings.yml index ccbde32..44bd4f6 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -1,6 +1,6 @@ --- -sort_dir_file_model: '1' +sort_dir_file_model: '0' issue_scope: all issue_list_defaults: column_names: From f6e61c3756b5c5fc16c726b2fd9e50f0a15d5bfb Mon Sep 17 00:00:00 2001 From: Andrei Munteanu Date: Wed, 24 Jul 2024 13:52:31 +0300 Subject: [PATCH 05/28] pull request --- app/controllers/custom_issues_controller.rb | 15 ---- .../issue_view_columns_controller.rb | 40 +++++++++-- .../issue_view_columns_issues_helper.rb | 68 +++++++++---------- .../issue_view_columns_projects_helper.rb | 6 +- .../collapse_expand/_collapse_expand.js.erb | 6 +- assets/stylesheets/issue_view_columns.css | 16 ----- config/locales/de.yml | 7 +- config/locales/en.yml | 6 +- config/routes.rb | 2 +- db/migrate/003_add_collapsed_ids_to_issues.rb | 2 +- 10 files changed, 85 insertions(+), 83 deletions(-) delete mode 100644 app/controllers/custom_issues_controller.rb diff --git a/app/controllers/custom_issues_controller.rb b/app/controllers/custom_issues_controller.rb deleted file mode 100644 index f364e1d..0000000 --- a/app/controllers/custom_issues_controller.rb +++ /dev/null @@ -1,15 +0,0 @@ -class CustomIssuesController < ApplicationController - skip_before_action :verify_authenticity_token, only: :update_collapsed_ids - - #Controller used for modifying collapsed_ids field of an issue - def update_collapsed_ids - @issue = Issue.find(params[:id]) - - json_data = JSON.parse(request.body.read) - if @issue.update(collapsed_ids: json_data["collapsed_ids"]) - render json: { message: "Collapsed IDs updated successfully" }, status: :ok - else - render json: { error: "Failed to update collapsed IDs" }, status: :unprocessable_entity - end - end -end diff --git a/app/controllers/issue_view_columns_controller.rb b/app/controllers/issue_view_columns_controller.rb index 491639e..4602e8c 100644 --- a/app/controllers/issue_view_columns_controller.rb +++ b/app/controllers/issue_view_columns_controller.rb @@ -1,15 +1,33 @@ # frozen_string_literal: true class IssueViewColumnsController < ApplicationController - before_action :find_project_by_project_id - before_action :authorize - before_action :build_query_for_project + before_action :find_project_by_project_id, only: [:update] + before_action :authorize, only: [:update] + before_action :build_query_for_project, only: [:update] + before_action :check_user_logged_in_and_has_rights, only: [:update_collapsed_ids] include QueriesHelper include IssueViewColumnsProjectsHelper - # refactor update, it's not good to do save like this - def update + def update_collapsed_ids + @issue = Issue.find(params[:id]) + + begin + json_data = JSON.parse(request.body.read) + rescue JSON::ParserError + render json: { error: I18n.t('invalid_json_format') }, status: :unprocessable_entity + return + end + + if @issue.update(collapsed_ids: json_data["collapsed_ids"]) + render json: { message: I18n.t('update_successful') }, status: :ok + else + render json: { error: I18n.t('update_failed') }, status: :unprocessable_entity + end + end + + # refactor update, it's not good to do save like this + def update update_selected_columns = params[:c] || [] IssueViewColumn.where(project_id: @project).delete_all position = 0 @@ -25,6 +43,16 @@ def update c.save! end - redirect_to settings_project_path(@project, tab: "issue_view_columns"), notice: l(:label_issue_columns_created_sucessfully) + redirect_to settings_project_path(@project, tab: 'issue_view_columns'), notice: l(:label_issue_columns_created_sucessfully) end + + private + + def check_user_logged_in_and_has_rights + @issue = Issue.find(params[:id]) + unless User.current.logged? && User.current.allowed_to?(:edit_issues, @issue.project) + render json: { error: I18n.t('access_denied') }, status: :forbidden + end + end + end diff --git a/app/helpers/issue_view_columns_issues_helper.rb b/app/helpers/issue_view_columns_issues_helper.rb index 3cbd9a9..012905f 100644 --- a/app/helpers/issue_view_columns_issues_helper.rb +++ b/app/helpers/issue_view_columns_issues_helper.rb @@ -2,9 +2,9 @@ module IssueViewColumnsIssuesHelper def render_descendants_tree(issue) - # Retrieve the list of columns to display for the project - columns_list = get_fields_for_project(issue) - return super if columns_list.empty? + columns_list = get_fields_for_project issue + # no field defined, then use render from core redmine (or whatever by other plugins loaded before this) + return super if columns_list.count.zero? # Retrieve minimum width settings for columns min_width_setting = RedmineIssueViewColumns.setting(:columns_min_width) @@ -19,7 +19,7 @@ def render_descendants_tree(issue) # Retrieve sorting settings and determine if sorting by directory/file model is enabled sort_dir_file_model = RedmineIssueViewColumns.setting(:sort_dir_file_model) collapsed_ids = issue.collapsed_ids.to_s.split.map(&:to_i) - field_values = +"" + field_values = +'' s = table_start_for_relations(columns_list) manage_relations = User.current.allowed_to?(:manage_subtasks, issue.project) rendered_issues = Set.new @@ -45,51 +45,51 @@ def render_descendants_tree(issue) def render_issue_row(child, level, hidden = false, columns_list, min_widths, manage_relations, collapsed_ids, issue) # Construct the row classes with context menu and alternating row colors tr_classes = +"hascontextmenu #{child.css_classes}" - tr_classes << " #{cycle("odd", "even")}" unless hidden + tr_classes << " #{cycle('odd', 'even')}" unless hidden tr_classes << " idnt-#{level}" if level.positive? # Generate buttons for deleting if the user has the right permissions buttons = if manage_relations link_to l(:label_delete_link_to_subtask), issue_path(id: child.id, - issue: { parent_issue_id: "" }, + issue: { parent_issue_id: '' }, back_url: issue_path(issue.id), - no_flash: "1"), + no_flash: '1'), method: :put, data: { confirm: l(:text_are_you_sure) }, title: l(:label_delete_link_to_subtask), - class: "icon-only icon-link-break" + class: 'icon-only icon-link-break' else - "".html_safe + ''.html_safe end buttons << link_to_context_menu # Build the content for each table cell - field_content = content_tag("td", check_box_tag("ids[]", child.id, false, id: nil), class: "checkbox") + field_content = content_tag('td', check_box_tag('ids[]', child.id, false, id: nil), class: 'checkbox') if child.descendants.any? # Generate toggle icon for expanding/collapsing subissues - icon_class = collapsed_ids.include?(child.id) ? "icon icon-toggle-plus" : "icon icon-toggle-minus" - expand_icon = content_tag("span", "", class: icon_class, onclick: "collapseExpand(this)") + icon_class = collapsed_ids.include?(child.id) ? 'icon icon-toggle-plus' : 'icon icon-toggle-minus' + expand_icon = content_tag('span', '', class: icon_class, onclick: 'collapseExpand(this)') subject_content = "#{expand_icon} #{link_to_issue(child, project: (issue.project_id != child.project_id))}".html_safe else subject_content = link_to_issue(child, project: (issue.project_id != child.project_id)) end - field_content << content_tag("td", subject_content, class: "subject") + field_content << content_tag('td', subject_content, class: 'subject') # Add columns with their respective content and minimum width style columns_list.each do |column| column_name = column.caption # Convert symbol to string - min_width_style = min_widths[column_name].present? ? "min-width: #{min_widths[column_name]};" : "" - field_content << content_tag("td", column_content(column, child), class: column.css_classes.to_s, style: min_width_style) + min_width_style = min_widths[column_name].present? ? 'min-width: #{min_widths[column_name]};' : '' + field_content << content_tag('td', column_content(column, child), class: column.css_classes.to_s, style: min_width_style) end - field_content << content_tag("td", buttons, class: "buttons") + field_content << content_tag('td', buttons, class: 'buttons') # Apply style to hide the row if hidden is true - row_style = hidden ? "display: none;" : "" - content_tag("tr", field_content, class: tr_classes, id: "issue-#{child.id}", style: row_style).html_safe + row_style = hidden ? 'display: none;' : '' + content_tag('tr', field_content, class: tr_classes, id: "issue-#{child.id}", style: row_style).html_safe end def render_issues_dir_file_model(issue, render_issue_row, collapsed_ids, rendered_issues, columns_list, field_values) @@ -251,7 +251,7 @@ def render_issue_relations(issue, relations) other_issue = relation.other_issue issue next if other_issue.closed? && !issue_columns_with_closed_issues? - tr_classes = "hascontextmenu #{other_issue.css_classes} #{cycle "odd", "even"} #{relation.css_classes_for other_issue}" + tr_classes = "hascontextmenu #{other_issue.css_classes} #{cycle 'odd', 'even'} #{relation.css_classes_for other_issue}" buttons = if manage_relations link_to l(:label_relation_delete), relation_path(relation), @@ -259,24 +259,24 @@ def render_issue_relations(issue, relations) method: :delete, data: { confirm: l(:text_are_you_sure) }, title: l(:label_relation_delete), - class: "icon-only icon-link-break" + class: 'icon-only icon-link-break' else - "".html_safe + ''.html_safe end buttons << link_to_context_menu subject_content = relation.to_s(@issue) { |other| link_to_issue other, project: Setting.cross_project_issue_relations? }.html_safe - field_content = content_tag("td", check_box_tag("ids[]", other_issue.id, false, id: nil), class: "checkbox") + - content_tag("td", subject_content, class: "subject") + field_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| - field_content << content_tag("td", column_content(column, other_issue), class: column.css_classes.to_s) + field_content << content_tag('td', column_content(column, other_issue), class: column.css_classes.to_s) end - field_content << content_tag("td", buttons, class: "buttons") + field_content << content_tag('td', buttons, class: 'buttons') - s << content_tag("tr", field_content, + s << content_tag('tr', field_content, id: "relation-#{relation.id}", class: tr_classes) end @@ -295,7 +295,7 @@ def issue_columns_with_closed_issues? issue_scope = RedmineIssueViewColumns.setting :issue_scope return true if issue_scope_with_closed? issue_scope - @issue_columns_with_closed_issues = if issue_scope == "without_closed_by_default" + @issue_columns_with_closed_issues = if issue_scope == 'without_closed_by_default' RedminePluginKit.true? params[:with_closed_issues] else RedminePluginKit.false? params[:without_closed_issues] @@ -303,8 +303,8 @@ def issue_columns_with_closed_issues? end def link_to_closed_issues(issue, issue_scope) - css_class = "closed-issue-switcher" - if issue_scope == "without_closed_by_default" + css_class = 'closed-issue-switcher' + if issue_scope == 'without_closed_by_default' if issue_columns_with_closed_issues? link_to l(:label_hide_closed_issues), issue_path(issue), class: "#{css_class} hide-switch" else @@ -322,18 +322,18 @@ def link_to_closed_issues(issue, issue_scope) def table_start_for_relations(columns_list) s = +'
' - s << content_tag("th", l(:field_subject), class: "subject") + s << content_tag('th', l(:field_subject), class: 'subject') columns_list.each do |column| - s << content_tag("th", column.caption, class: column.name) + s << content_tag('th', column.caption, class: column.name) end - s << content_tag("th", "", class: "buttons") - s << "" + s << content_tag('th', '', class: 'buttons') + s << '' s end def table_end_for_relations - "
" + '' end def get_fields_for_project(issue) diff --git a/app/helpers/issue_view_columns_projects_helper.rb b/app/helpers/issue_view_columns_projects_helper.rb index 0773053..8df51a9 100644 --- a/app/helpers/issue_view_columns_projects_helper.rb +++ b/app/helpers/issue_view_columns_projects_helper.rb @@ -5,9 +5,9 @@ def project_settings_tabs tabs = super if User.current.allowed_to? :manage_issue_view_columns, @project - tabs << { name: "issue_view_columns", + tabs << { name: 'issue_view_columns', action: :issue_view_columns, - partial: "projects/settings/issue_view_columns", + partial: 'projects/settings/issue_view_columns', label: :issue_view_columns_settings } end @@ -16,7 +16,7 @@ def project_settings_tabs def build_query_for_project @selected_columns = IssueViewColumn.where(project_id: @project).sorted.pluck(:name) - @selected_columns = ["#"] if @selected_columns.count.zero? + @selected_columns = ['#'] if @selected_columns.count.zero? @query = IssueQuery.new column_names: @selected_columns @query.project = @project @query diff --git a/app/views/collapse_expand/_collapse_expand.js.erb b/app/views/collapse_expand/_collapse_expand.js.erb index 0d358f7..e246d16 100644 --- a/app/views/collapse_expand/_collapse_expand.js.erb +++ b/app/views/collapse_expand/_collapse_expand.js.erb @@ -1,4 +1,4 @@ - \ No newline at end of file diff --git a/assets/stylesheets/issue_view_columns.css b/assets/stylesheets/issue_view_columns.css index 7521f3f..f823af3 100644 --- a/assets/stylesheets/issue_view_columns.css +++ b/assets/stylesheets/issue_view_columns.css @@ -35,3 +35,19 @@ padding: 5px !important; } } + +#sorting_criteria_box { + display: flex; + justify-content: space-around; + align-items: center; +} + +.capitalize-text { + text-transform: capitalize; +} + +#input_header { + display: flex; + justify-content: space-around; + align-items: center; +} \ No newline at end of file diff --git a/config/locales/de.yml b/config/locales/de.yml index b96b265..f84a1bc 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -14,14 +14,4 @@ de: info_issue_view_columns_without_columns: Wenn keine Spalten festgelegt werden, wird die Standard-Darstellung von Redmine verwendet. info_issue_view_columns_project_settings: Durch Aktivierung des Projekt-Modul "Spalten in Ticketansicht" kann man individuell für das Projekt Spalten für Untertickets und Ticketbeziehungen festlegen (falls diese vom der systemweiten Einstellung abweichen sollen). Wenn die systemweite Einstellung verwendet werden soll, muss das Projekt Module deaktiviert werden. - label_issue_view_column: Ticketspalte - label_sort_dir_file_model: Sortierung nach Ordner-/Dateimodell - info_sort_dir_file_model: Aktivieren Sie diese Option, um Probleme nach dem Ordner-/Dateimodell zu sortieren. - label_sorting_columns: Sortierkriterien und Spalten-Mindestbreite festlegen - info_columns_sorting: "Geben Sie die Sortierreihenfolge für jede Spalte an, in der Reihenfolge der Relevanz. Beispiel: Aktualisierte:DESC, Erstellt:ASC" - label_columns_min_width: Mindestbreite für Spalten festlegen - info_columns_min_width: "Geben Sie die Mindestbreite für jede Spalte an. Beispiel: Projekt:200px, Status:30vw" - access_denied: "Zugriff verweigert" - invalid_json_format: "Ungültiges JSON-Format" - update_successful: "Aktualisierung erfolgreich" - update_failed: "Aktualisierung fehlgeschlagen" + label_issue_view_column: Ticketspalte \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index d5d4312..342bb78 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -24,4 +24,5 @@ en: access_denied: "Access denied" invalid_json_format: "Invalid JSON format" update_successful: "Update successful" - update_failed: "Update failed" \ No newline at end of file + update_failed: "Update failed" + label_sorting_priority_columns: Column priority in sorting \ No newline at end of file diff --git a/config/settings.yml b/config/settings.yml index 69ae812..1725072 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -3,4 +3,6 @@ sort_dir_file_model: '0' issue_scope: all issue_list_defaults: - column_names: \ No newline at end of file + column_names: +columns_sorting: '' +columns_min_width: '' \ No newline at end of file From 21a00621f6145ab152615fb2515710a68879806e Mon Sep 17 00:00:00 2001 From: Andrei Munteanu Date: Thu, 1 Aug 2024 16:00:38 +0300 Subject: [PATCH 19/28] check for querycustomfield --- app/helpers/issue_view_columns_issues_helper.rb | 4 ++-- test/functional/issues_controller_test.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/helpers/issue_view_columns_issues_helper.rb b/app/helpers/issue_view_columns_issues_helper.rb index 9bcd4b5..a5787ee 100644 --- a/app/helpers/issue_view_columns_issues_helper.rb +++ b/app/helpers/issue_view_columns_issues_helper.rb @@ -147,8 +147,8 @@ def comparison_lambda(sorting_criteria) column_name = criterion[:column_name] direction = criterion[:direction] == "ASC" ? 1 : -1 - a_value = column_name.start_with?("cf_") ? a.custom_field_value(column_name.sub(/^cf_/, "")) : get_nested_attribute_value(a, column_name) rescue nil - b_value = column_name.start_with?("cf_") ? b.custom_field_value(column_name.sub(/^cf_/, "")) : get_nested_attribute_value(b, column_name) rescue nil + a_value = column_name.is_a?(QueryCustomFieldColumn) ? a.custom_field_value(column_name.sub(/^cf_/, "")) : get_nested_attribute_value(a, column_name) rescue nil + b_value = column_name.is_a?(QueryCustomFieldColumn) ? b.custom_field_value(column_name.sub(/^cf_/, "")) : get_nested_attribute_value(b, column_name) rescue nil comparison = if a_value.nil? && b_value.nil? 0 diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb index 17276b2..b2033b3 100644 --- a/test/functional/issues_controller_test.rb +++ b/test/functional/issues_controller_test.rb @@ -19,7 +19,7 @@ class IssuesControllerTest < RedmineIssueViewColumns::ControllerTest def setup prepare_tests - @global_settings = { "column_names" => %w[created_on updated_on]} + @global_settings = { 'column_names' => %w[created_on updated_on]} end def test_show_author_column_for_related_issues_with_project_setting From 0e2d15a2150d3cd83d4cc24c38afb9fe27644898 Mon Sep 17 00:00:00 2001 From: Andrei Munteanu Date: Thu, 1 Aug 2024 16:14:35 +0300 Subject: [PATCH 20/28] used get_issue_field_value --- .../issue_view_columns_issues_helper.rb | 61 ++++++++++--------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/app/helpers/issue_view_columns_issues_helper.rb b/app/helpers/issue_view_columns_issues_helper.rb index a5787ee..3aa84b9 100644 --- a/app/helpers/issue_view_columns_issues_helper.rb +++ b/app/helpers/issue_view_columns_issues_helper.rb @@ -128,10 +128,11 @@ def sort_issues(issues, columns_list) columns_sorting_setting = RedmineIssueViewColumns.setting(:columns_sorting) return issues unless columns_sorting_setting.present? - # Build sorting criteria as an array of hashes with keys :column_name and :direction + # Build sorting criteria as an array of hashes with keys :column and :direction sorting_criteria = columns_sorting_setting.split(",").map do |column_setting| column_name, direction = column_setting.split(":").map(&:strip) - { column_name: column_name, direction: direction } + column = columns_list.find { |col| col.name.to_s == column_name } + { column: column, direction: direction } end # Use the extracted comparison lambda @@ -144,34 +145,34 @@ def sort_issues(issues, columns_list) def comparison_lambda(sorting_criteria) lambda do |a, b| sorting_criteria.each do |criterion| - column_name = criterion[:column_name] + column = criterion[:column] direction = criterion[:direction] == "ASC" ? 1 : -1 - a_value = column_name.is_a?(QueryCustomFieldColumn) ? a.custom_field_value(column_name.sub(/^cf_/, "")) : get_nested_attribute_value(a, column_name) rescue nil - b_value = column_name.is_a?(QueryCustomFieldColumn) ? b.custom_field_value(column_name.sub(/^cf_/, "")) : get_nested_attribute_value(b, column_name) rescue nil + a_value = get_issue_field_value(a, column) + b_value = get_issue_field_value(b, column) comparison = if a_value.nil? && b_value.nil? - 0 - elsif a_value.nil? - -1 - elsif b_value.nil? - 1 + 0 + elsif a_value.nil? + -1 + elsif b_value.nil? + 1 + else + case a_value + when Numeric + a_value <=> b_value + when String + a_value.to_s <=> b_value.to_s + when Enumerable + a_value.length <=> b_value.length + when User + (a_value.firstname + a_value.lastname) <=> (b_value.firstname + b_value.lastname) + when ActiveRecord::Base + a_value.respond_to?(:name) ? a_value.name <=> b_value.name : a_value.id <=> b_value.id else - case a_value - when Numeric - a_value <=> b_value - when String - a_value.to_s <=> b_value.to_s - when Enumerable - a_value.length <=> b_value.length - when User - (a_value.firstname + a_value.lastname) <=> (b_value.firstname + b_value.lastname) - when ActiveRecord::Base - a_value.respond_to?(:name) ? a_value.name <=> b_value.name : a_value.id <=> b_value.id - else - a_value.to_s <=> b_value.to_s - end + a_value.to_s <=> b_value.to_s end + end # If comparison is not zero, return it adjusted by direction return comparison * direction if comparison != 0 @@ -180,11 +181,13 @@ def comparison_lambda(sorting_criteria) end end - # Retrieves a nested attribute value from an object based on a dot-separated attribute path ( used for parent.subject ) - def get_nested_attribute_value(object, attribute_path) - attribute_parts = attribute_path.split(".") - attribute_parts.inject(object) do |current_object, method| - current_object.public_send(method) if current_object + def get_issue_field_value(issue, column) + if column.is_a?(QueryCustomFieldColumn) + return issue.custom_field_value(column.custom_field[:id]) + elsif issue.has_attribute?(column.name) + return issue[column.name] + else + return issue.public_send(column.name) end end From dc263016452315458d66e3cd366896c9249e5c02 Mon Sep 17 00:00:00 2001 From: Andrei Munteanu Date: Thu, 1 Aug 2024 17:09:20 +0300 Subject: [PATCH 21/28] reverted using get_issue_field_value --- .../issue_view_columns_issues_helper.rb | 61 +++++++++---------- 1 file changed, 29 insertions(+), 32 deletions(-) diff --git a/app/helpers/issue_view_columns_issues_helper.rb b/app/helpers/issue_view_columns_issues_helper.rb index 3aa84b9..9bcd4b5 100644 --- a/app/helpers/issue_view_columns_issues_helper.rb +++ b/app/helpers/issue_view_columns_issues_helper.rb @@ -128,11 +128,10 @@ def sort_issues(issues, columns_list) columns_sorting_setting = RedmineIssueViewColumns.setting(:columns_sorting) return issues unless columns_sorting_setting.present? - # Build sorting criteria as an array of hashes with keys :column and :direction + # Build sorting criteria as an array of hashes with keys :column_name and :direction sorting_criteria = columns_sorting_setting.split(",").map do |column_setting| column_name, direction = column_setting.split(":").map(&:strip) - column = columns_list.find { |col| col.name.to_s == column_name } - { column: column, direction: direction } + { column_name: column_name, direction: direction } end # Use the extracted comparison lambda @@ -145,34 +144,34 @@ def sort_issues(issues, columns_list) def comparison_lambda(sorting_criteria) lambda do |a, b| sorting_criteria.each do |criterion| - column = criterion[:column] + column_name = criterion[:column_name] direction = criterion[:direction] == "ASC" ? 1 : -1 - a_value = get_issue_field_value(a, column) - b_value = get_issue_field_value(b, column) + a_value = column_name.start_with?("cf_") ? a.custom_field_value(column_name.sub(/^cf_/, "")) : get_nested_attribute_value(a, column_name) rescue nil + b_value = column_name.start_with?("cf_") ? b.custom_field_value(column_name.sub(/^cf_/, "")) : get_nested_attribute_value(b, column_name) rescue nil comparison = if a_value.nil? && b_value.nil? - 0 - elsif a_value.nil? - -1 - elsif b_value.nil? - 1 - else - case a_value - when Numeric - a_value <=> b_value - when String - a_value.to_s <=> b_value.to_s - when Enumerable - a_value.length <=> b_value.length - when User - (a_value.firstname + a_value.lastname) <=> (b_value.firstname + b_value.lastname) - when ActiveRecord::Base - a_value.respond_to?(:name) ? a_value.name <=> b_value.name : a_value.id <=> b_value.id + 0 + elsif a_value.nil? + -1 + elsif b_value.nil? + 1 else - a_value.to_s <=> b_value.to_s + case a_value + when Numeric + a_value <=> b_value + when String + a_value.to_s <=> b_value.to_s + when Enumerable + a_value.length <=> b_value.length + when User + (a_value.firstname + a_value.lastname) <=> (b_value.firstname + b_value.lastname) + when ActiveRecord::Base + a_value.respond_to?(:name) ? a_value.name <=> b_value.name : a_value.id <=> b_value.id + else + a_value.to_s <=> b_value.to_s + end end - end # If comparison is not zero, return it adjusted by direction return comparison * direction if comparison != 0 @@ -181,13 +180,11 @@ def comparison_lambda(sorting_criteria) end end - def get_issue_field_value(issue, column) - if column.is_a?(QueryCustomFieldColumn) - return issue.custom_field_value(column.custom_field[:id]) - elsif issue.has_attribute?(column.name) - return issue[column.name] - else - return issue.public_send(column.name) + # Retrieves a nested attribute value from an object based on a dot-separated attribute path ( used for parent.subject ) + def get_nested_attribute_value(object, attribute_path) + attribute_parts = attribute_path.split(".") + attribute_parts.inject(object) do |current_object, method| + current_object.public_send(method) if current_object end end From 9077a3b2b93a5a64129e23f7914e75eeb95c3324 Mon Sep 17 00:00:00 2001 From: Andrei Munteanu Date: Thu, 1 Aug 2024 18:19:19 +0300 Subject: [PATCH 22/28] all tests working --- test/functional/issues_controller_test.rb | 58 +++++++++++------------ 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb index b2033b3..66999ed 100644 --- a/test/functional/issues_controller_test.rb +++ b/test/functional/issues_controller_test.rb @@ -19,7 +19,7 @@ class IssuesControllerTest < RedmineIssueViewColumns::ControllerTest def setup prepare_tests - @global_settings = { 'column_names' => %w[created_on updated_on]} + @global_settings = { "column_names" => %w[created_on updated_on] } end def test_show_author_column_for_related_issues_with_project_setting @@ -28,7 +28,7 @@ def test_show_author_column_for_related_issues_with_project_setting get :show, params: { id: issue.id } assert_response :success - assert_select '#relations td.author' + assert_select "#relations td.author" end def test_show_default_columns_for_related_issues_without_global_setting @@ -37,14 +37,14 @@ def test_show_default_columns_for_related_issues_without_global_setting IssueRelation.create! issue_from: related_issue, issue_to: issue, - relation_type: 'relates' + relation_type: "relates" @request.session[:user_id] = 1 - with_plugin_settings 'redmine_issue_view_columns', issue_list_defaults: {} do + with_plugin_settings "redmine_issue_view_columns", issue_list_defaults: {} do get :show, params: { id: issue.id } assert_response :success - assert_select '#relations td.due_date' + assert_select "#relations td.due_date" end end @@ -54,16 +54,16 @@ def test_show_custom_columns_for_related_issues_with_global_setting IssueRelation.create! issue_from: related_issue, issue_to: issue, - relation_type: 'relates' + relation_type: "relates" @request.session[:user_id] = 1 - with_plugin_settings 'redmine_issue_view_columns', + with_plugin_settings "redmine_issue_view_columns", issue_list_defaults: @global_settings do get :show, params: { id: issue.id } assert_response :success - assert_select '#relations td.created_on' - assert_select '#relations td.updated_on' + assert_select "#relations td.created_on" + assert_select "#relations td.updated_on" end end @@ -74,7 +74,7 @@ def test_show_author_column_for_subtasks_with_project_setting get :show, params: { id: issue.id } assert_response :success - assert_select '#issue_tree td.author' + assert_select "#issue_tree td.author" end def test_show_default_columns_for_subtasks_without_global_setting @@ -82,12 +82,12 @@ def test_show_default_columns_for_subtasks_without_global_setting Issue.generate! project_id: 2, parent_issue_id: issue.id @request.session[:user_id] = 1 - with_plugin_settings 'redmine_issue_view_columns', + with_plugin_settings "redmine_issue_view_columns", issue_list_defaults: {} do get :show, params: { id: issue.id } assert_response :success - assert_select '#issue_tree td.due_date' + assert_select "#issue_tree td.due_date" end end @@ -96,13 +96,13 @@ def test_show_custom_columns_for_subtasks_with_global_setting Issue.generate! project_id: 2, parent_issue_id: issue.id @request.session[:user_id] = 1 - with_plugin_settings 'redmine_issue_view_columns', + with_plugin_settings "redmine_issue_view_columns", issue_list_defaults: @global_settings do get :show, params: { id: issue.id } assert_response :success - assert_select '#issue_tree td.created_on' - assert_select '#issue_tree td.updated_on' + assert_select "#issue_tree td.created_on" + assert_select "#issue_tree td.updated_on" end end @@ -112,16 +112,16 @@ def test_show_without_closed_relations related_issue = Issue.generate! project_id: 2, status_id: 1 open_relation = IssueRelation.create! issue_from: related_issue, issue_to: issue, - relation_type: 'relates' + relation_type: "relates" closed_issue = Issue.generate! project_id: 2, status_id: 5 closed_relation = IssueRelation.create! issue_from: closed_issue, issue_to: issue, - relation_type: 'relates' + relation_type: "relates" @request.session[:user_id] = 1 - with_plugin_settings 'redmine_issue_view_columns', - issue_scope: 'without_closed_by_default', + with_plugin_settings "redmine_issue_view_columns", + issue_scope: "without_closed_by_default", issue_list_defaults: @global_settings do get :show, params: { id: issue.id } @@ -138,8 +138,8 @@ def test_show_without_closed_subtasks closed_issue = Issue.generate! project_id: 1, parent_issue_id: issue.id, status_id: 5 @request.session[:user_id] = 1 - with_plugin_settings 'redmine_issue_view_columns', - issue_scope: 'without_closed_by_default', + with_plugin_settings "redmine_issue_view_columns", + issue_scope: "without_closed_by_default", issue_list_defaults: @global_settings do get :show, params: { id: issue.id } @@ -159,7 +159,7 @@ def test_render_issue_tree_dir_file_model @request.session[:user_id] = 1 with_plugin_settings "redmine_issue_view_columns", - sort_dir_file_model: '1' do + sort_dir_file_model: "1" do get :show, params: { id: parent_issue.id } assert_response :success @@ -186,7 +186,7 @@ def test_render_issue_tree_default @request.session[:user_id] = 1 with_plugin_settings "redmine_issue_view_columns", - sort_dir_file_model: '0' do + sort_dir_file_model: "0" do get :show, params: { id: parent_issue.id } assert_response :success @@ -211,14 +211,14 @@ def test_min_width_setting_applies @request.session[:user_id] = 1 with_plugin_settings "redmine_issue_view_columns", - columns_min_width: 'Status:300px' do + columns_min_width: "status:300px" do get :show, params: { id: issue.id } assert_response :success - assert_select "td.status", true, "Expected 'Status' column to be present" + assert_select "th.status", true, "Expected 'Status' column to be present" - style = css_select("td.status").first['style'] + style = css_select("th.status").first["style"] # Ensure the style attribute contains the min-width property assert_match(/min-width:\s*300px/, style, "Expected 'Status' column to have min-width of 300px") @@ -234,12 +234,12 @@ def test_sorting_criteria_and_order_for_columns @request.session[:user_id] = 1 with_plugin_settings "redmine_issue_view_columns", - columns_sorting: 'Status:ASC,Author:DESC' do + columns_sorting: "status:ASC,author:DESC" do get :show, params: { id: parent_issue.id } assert_response :success - issue_rows = css_select('#issue_tree tr').map(&:to_html) + issue_rows = css_select("#issue_tree tr").map(&:to_html) issue_ids = issue_rows.map { |row| row[/id="issue-(\d+)"/, 1].to_i } @@ -266,7 +266,7 @@ def test_collapsed_issue_is_not_displayed grandchild_issue_row = css_select("tr#issue-#{grandchild_issue.id}").first - style = grandchild_issue_row['style'] + style = grandchild_issue_row["style"] # Check if the style attribute of children of issues included in collapsed_ids includes 'display: none' assert_match(/display:\s*none/, style, "Expected grandchild issue to have display: none") From d184270b8252dee3e6b98a434d69bf0b6ea993c0 Mon Sep 17 00:00:00 2001 From: Andrei Munteanu Date: Fri, 2 Aug 2024 17:49:12 +0300 Subject: [PATCH 23/28] Fixed css names , endlines and internationalized h3s --- .../_issue_view_columns_settings.html.slim | 2 +- .../_min_width_sort_criteria.html.erb | 12 +++-- assets/javascripts/issue_view_columns.js | 2 +- assets/stylesheets/issue_view_columns.css | 8 ++-- config/locales/de.yml | 3 +- config/locales/en.yml | 9 +++- config/settings.yml | 2 +- test/functional/issues_controller_test.rb | 44 +++++++++---------- 8 files changed, 47 insertions(+), 35 deletions(-) diff --git a/app/views/settings/_issue_view_columns_settings.html.slim b/app/views/settings/_issue_view_columns_settings.html.slim index effd9dd..e33543a 100644 --- a/app/views/settings/_issue_view_columns_settings.html.slim +++ b/app/views/settings/_issue_view_columns_settings.html.slim @@ -30,4 +30,4 @@ br br -= render 'min_width_sort_criteria' \ No newline at end of file += render 'min_width_sort_criteria' diff --git a/app/views/settings/_min_width_sort_criteria.html.erb b/app/views/settings/_min_width_sort_criteria.html.erb index e5d8b20..1e86094 100644 --- a/app/views/settings/_min_width_sort_criteria.html.erb +++ b/app/views/settings/_min_width_sort_criteria.html.erb @@ -14,7 +14,7 @@ <% sorting_columns = columns_sorting.split(",").map { |criterion| criterion.split(":").first.strip } %> <% column_names = column_names = sorting_columns + (column_names - sorting_columns) %> -
+
<%= l :label_sorting_columns %> <% selected_tag_id = "sorting_columns" %> <% tag_name = "columns[]" %> @@ -33,11 +33,15 @@ id: selected_tag_id, multiple: true, size: 12, - class: "capitalize-text" %> + class: "capitalize-text_box_issue_view_columns_settings" %>
-

Column name

Sorting order

Min width

+
+

<%= l('column_name_setting_label') %>

+

<%= l('sorting_order_setting_label') %>

+

<%= l('min_width_setting_label') %>

+
<% column_names.each do |column| %>
@@ -159,4 +163,4 @@ } }); } - \ No newline at end of file + diff --git a/assets/javascripts/issue_view_columns.js b/assets/javascripts/issue_view_columns.js index f40684e..a8b3c2b 100644 --- a/assets/javascripts/issue_view_columns.js +++ b/assets/javascripts/issue_view_columns.js @@ -4,4 +4,4 @@ $(function() { $('#available_settings_issue_list_defaults_column_names option[value="subject"]').remove(); $('#tab-content-issue_view_columns #available_c option[value="tracker"]').remove(); $('#tab-content-issue_view_columns #available_c option[value="subject"]').remove(); -}); \ No newline at end of file +}); diff --git a/assets/stylesheets/issue_view_columns.css b/assets/stylesheets/issue_view_columns.css index f823af3..b65daf6 100644 --- a/assets/stylesheets/issue_view_columns.css +++ b/assets/stylesheets/issue_view_columns.css @@ -36,18 +36,18 @@ } } -#sorting_criteria_box { +#sorting_criteria_box_issue_view_columns_settings { display: flex; justify-content: space-around; align-items: center; } -.capitalize-text { +.capitalize-text_box_issue_view_columns_settings { text-transform: capitalize; } -#input_header { +#input_header_box_issue_view_columns_settings { display: flex; justify-content: space-around; align-items: center; -} \ No newline at end of file +} diff --git a/config/locales/de.yml b/config/locales/de.yml index f84a1bc..4259a4f 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -14,4 +14,5 @@ de: info_issue_view_columns_without_columns: Wenn keine Spalten festgelegt werden, wird die Standard-Darstellung von Redmine verwendet. info_issue_view_columns_project_settings: Durch Aktivierung des Projekt-Modul "Spalten in Ticketansicht" kann man individuell für das Projekt Spalten für Untertickets und Ticketbeziehungen festlegen (falls diese vom der systemweiten Einstellung abweichen sollen). Wenn die systemweite Einstellung verwendet werden soll, muss das Projekt Module deaktiviert werden. - label_issue_view_column: Ticketspalte \ No newline at end of file + label_issue_view_column: Ticketspalte + \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index 342bb78..7dc164c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -25,4 +25,11 @@ en: invalid_json_format: "Invalid JSON format" update_successful: "Update successful" update_failed: "Update failed" - label_sorting_priority_columns: Column priority in sorting \ No newline at end of file + label_sorting_priority_columns: Column priority in sorting + column_name: "Column name" + sorting_order: "Sorting order" + min_width: "Min width" + column_name_setting_label: "Column Name" + sorting_order_setting_label: "Sorting Order" + min_width_setting_label: "Minimum Width" + \ No newline at end of file diff --git a/config/settings.yml b/config/settings.yml index 1725072..85eecef 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -5,4 +5,4 @@ issue_scope: all issue_list_defaults: column_names: columns_sorting: '' -columns_min_width: '' \ No newline at end of file +columns_min_width: '' diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb index 66999ed..43a0d85 100644 --- a/test/functional/issues_controller_test.rb +++ b/test/functional/issues_controller_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.expand_path "../../test_helper", __FILE__ +require File.expand_path '../../test_helper', __FILE__ class IssuesControllerTest < RedmineIssueViewColumns::ControllerTest fixtures :users, :email_addresses, :roles, @@ -19,7 +19,7 @@ class IssuesControllerTest < RedmineIssueViewColumns::ControllerTest def setup prepare_tests - @global_settings = { "column_names" => %w[created_on updated_on] } + @global_settings = { 'column_names' => %w[created_on updated_on] } end def test_show_author_column_for_related_issues_with_project_setting @@ -28,7 +28,7 @@ def test_show_author_column_for_related_issues_with_project_setting get :show, params: { id: issue.id } assert_response :success - assert_select "#relations td.author" + assert_select '#relations td.author' end def test_show_default_columns_for_related_issues_without_global_setting @@ -37,14 +37,14 @@ def test_show_default_columns_for_related_issues_without_global_setting IssueRelation.create! issue_from: related_issue, issue_to: issue, - relation_type: "relates" + relation_type: 'relates' @request.session[:user_id] = 1 - with_plugin_settings "redmine_issue_view_columns", issue_list_defaults: {} do + with_plugin_settings 'redmine_issue_view_columns', issue_list_defaults: {} do get :show, params: { id: issue.id } assert_response :success - assert_select "#relations td.due_date" + assert_select '#relations td.due_date' end end @@ -54,16 +54,16 @@ def test_show_custom_columns_for_related_issues_with_global_setting IssueRelation.create! issue_from: related_issue, issue_to: issue, - relation_type: "relates" + relation_type: 'relates' @request.session[:user_id] = 1 - with_plugin_settings "redmine_issue_view_columns", + with_plugin_settings 'redmine_issue_view_columns', issue_list_defaults: @global_settings do get :show, params: { id: issue.id } assert_response :success - assert_select "#relations td.created_on" - assert_select "#relations td.updated_on" + assert_select '#relations td.created_on' + assert_select '#relations td.updated_on' end end @@ -74,7 +74,7 @@ def test_show_author_column_for_subtasks_with_project_setting get :show, params: { id: issue.id } assert_response :success - assert_select "#issue_tree td.author" + assert_select '#issue_tree td.author' end def test_show_default_columns_for_subtasks_without_global_setting @@ -82,12 +82,12 @@ def test_show_default_columns_for_subtasks_without_global_setting Issue.generate! project_id: 2, parent_issue_id: issue.id @request.session[:user_id] = 1 - with_plugin_settings "redmine_issue_view_columns", + with_plugin_settings 'redmine_issue_view_columns', issue_list_defaults: {} do get :show, params: { id: issue.id } assert_response :success - assert_select "#issue_tree td.due_date" + assert_select '#issue_tree td.due_date' end end @@ -96,13 +96,13 @@ def test_show_custom_columns_for_subtasks_with_global_setting Issue.generate! project_id: 2, parent_issue_id: issue.id @request.session[:user_id] = 1 - with_plugin_settings "redmine_issue_view_columns", + with_plugin_settings 'redmine_issue_view_columns', issue_list_defaults: @global_settings do get :show, params: { id: issue.id } assert_response :success - assert_select "#issue_tree td.created_on" - assert_select "#issue_tree td.updated_on" + assert_select '#issue_tree td.created_on' + assert_select '#issue_tree td.updated_on' end end @@ -112,16 +112,16 @@ def test_show_without_closed_relations related_issue = Issue.generate! project_id: 2, status_id: 1 open_relation = IssueRelation.create! issue_from: related_issue, issue_to: issue, - relation_type: "relates" + relation_type: 'relates' closed_issue = Issue.generate! project_id: 2, status_id: 5 closed_relation = IssueRelation.create! issue_from: closed_issue, issue_to: issue, - relation_type: "relates" + relation_type: 'relates' @request.session[:user_id] = 1 - with_plugin_settings "redmine_issue_view_columns", - issue_scope: "without_closed_by_default", + with_plugin_settings 'redmine_issue_view_columns', + issue_scope: 'without_closed_by_default', issue_list_defaults: @global_settings do get :show, params: { id: issue.id } @@ -138,8 +138,8 @@ def test_show_without_closed_subtasks closed_issue = Issue.generate! project_id: 1, parent_issue_id: issue.id, status_id: 5 @request.session[:user_id] = 1 - with_plugin_settings "redmine_issue_view_columns", - issue_scope: "without_closed_by_default", + with_plugin_settings 'redmine_issue_view_columns', + issue_scope: 'without_closed_by_default', issue_list_defaults: @global_settings do get :show, params: { id: issue.id } From d014200df3574b07a8f5676687bb5c30366b902d Mon Sep 17 00:00:00 2001 From: Andrei Munteanu Date: Fri, 2 Aug 2024 17:51:03 +0300 Subject: [PATCH 24/28] Fixed css names , endlines and internationalized h3s --- config/locales/de.yml | 3 +-- config/locales/en.yml | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/config/locales/de.yml b/config/locales/de.yml index 4259a4f..f84a1bc 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -14,5 +14,4 @@ de: info_issue_view_columns_without_columns: Wenn keine Spalten festgelegt werden, wird die Standard-Darstellung von Redmine verwendet. info_issue_view_columns_project_settings: Durch Aktivierung des Projekt-Modul "Spalten in Ticketansicht" kann man individuell für das Projekt Spalten für Untertickets und Ticketbeziehungen festlegen (falls diese vom der systemweiten Einstellung abweichen sollen). Wenn die systemweite Einstellung verwendet werden soll, muss das Projekt Module deaktiviert werden. - label_issue_view_column: Ticketspalte - \ No newline at end of file + label_issue_view_column: Ticketspalte \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index 7dc164c..0937a5c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -31,5 +31,4 @@ en: min_width: "Min width" column_name_setting_label: "Column Name" sorting_order_setting_label: "Sorting Order" - min_width_setting_label: "Minimum Width" - \ No newline at end of file + min_width_setting_label: "Minimum Width" \ No newline at end of file From 1253d8eff7c3d94e6603d87848250fd4b6aa76f8 Mon Sep 17 00:00:00 2001 From: Andrei Munteanu Date: Fri, 2 Aug 2024 17:53:53 +0300 Subject: [PATCH 25/28] Fixed css names , endlines and internationalized h3s --- config/locales/de.yml | 2 +- config/locales/en.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/locales/de.yml b/config/locales/de.yml index f84a1bc..80e463e 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -14,4 +14,4 @@ de: info_issue_view_columns_without_columns: Wenn keine Spalten festgelegt werden, wird die Standard-Darstellung von Redmine verwendet. info_issue_view_columns_project_settings: Durch Aktivierung des Projekt-Modul "Spalten in Ticketansicht" kann man individuell für das Projekt Spalten für Untertickets und Ticketbeziehungen festlegen (falls diese vom der systemweiten Einstellung abweichen sollen). Wenn die systemweite Einstellung verwendet werden soll, muss das Projekt Module deaktiviert werden. - label_issue_view_column: Ticketspalte \ No newline at end of file + label_issue_view_column: Ticketspalte diff --git a/config/locales/en.yml b/config/locales/en.yml index 0937a5c..bbbce09 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -31,4 +31,4 @@ en: min_width: "Min width" column_name_setting_label: "Column Name" sorting_order_setting_label: "Sorting Order" - min_width_setting_label: "Minimum Width" \ No newline at end of file + min_width_setting_label: "Minimum Width" From eeb4c5cfe26689ecf7f50c7847c6bb8b8d2c787d Mon Sep 17 00:00:00 2001 From: Andrei Munteanu Date: Fri, 2 Aug 2024 18:01:50 +0300 Subject: [PATCH 26/28] Internationalized options --- app/views/settings/_min_width_sort_criteria.html.erb | 6 +++--- config/locales/en.yml | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/views/settings/_min_width_sort_criteria.html.erb b/app/views/settings/_min_width_sort_criteria.html.erb index 1e86094..d23d3f1 100644 --- a/app/views/settings/_min_width_sort_criteria.html.erb +++ b/app/views/settings/_min_width_sort_criteria.html.erb @@ -46,9 +46,9 @@ <% column_names.each do |column| %>
diff --git a/config/locales/en.yml b/config/locales/en.yml index bbbce09..03b6482 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -32,3 +32,6 @@ en: column_name_setting_label: "Column Name" sorting_order_setting_label: "Sorting Order" min_width_setting_label: "Minimum Width" + no_sort_sorting_option: "No Sort" + ascending_sorting_option: "Ascending" + descending_sorting_option: "Descending" From e8dff63536b465aff5d409e73486855d750f39be Mon Sep 17 00:00:00 2001 From: Andrei Munteanu Date: Fri, 9 Aug 2024 16:07:57 +0300 Subject: [PATCH 27/28] modified typo --- app/views/settings/_min_width_sort_criteria.html.erb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/settings/_min_width_sort_criteria.html.erb b/app/views/settings/_min_width_sort_criteria.html.erb index d23d3f1..1d61780 100644 --- a/app/views/settings/_min_width_sort_criteria.html.erb +++ b/app/views/settings/_min_width_sort_criteria.html.erb @@ -20,10 +20,10 @@ <% tag_name = "columns[]" %>
- - - - + + + +
@@ -122,7 +122,7 @@ applyButton.addEventListener('click', updateHiddenFields); }); - function moveOptionss(direction) { + function moveColumnPriority(direction) { const selectElement = document.getElementById('sorting_columns'); const selectedOptions = Array.from(selectElement.selectedOptions); const selectedIndices = selectedOptions.map(option => Array.from(selectElement.options).indexOf(option)); From 06ee2d7f64691135ab0d3ee86dbad81fd0d4f017 Mon Sep 17 00:00:00 2001 From: Andrei Munteanu Date: Tue, 10 Sep 2024 16:08:06 +0300 Subject: [PATCH 28/28] 35305-stop-showing-+/--if-all-subtasks-areclosed --- .../issue_view_columns_issues_helper.rb | 95 ++++++++++--------- 1 file changed, 51 insertions(+), 44 deletions(-) diff --git a/app/helpers/issue_view_columns_issues_helper.rb b/app/helpers/issue_view_columns_issues_helper.rb index 9bcd4b5..fcf2b2a 100644 --- a/app/helpers/issue_view_columns_issues_helper.rb +++ b/app/helpers/issue_view_columns_issues_helper.rb @@ -9,7 +9,7 @@ def render_descendants_tree(issue) # Retrieve sorting settings and determine if sorting by directory/file model is enabled sort_dir_file_model = RedmineIssueViewColumns.setting(:sort_dir_file_model) collapsed_ids = issue.collapsed_ids.to_s.split.map(&:to_i) - field_values = +'' + field_values = +"" s = table_start_for_relations columns_list manage_relations = User.current.allowed_to? :manage_subtasks, issue.project rendered_issues = Set.new @@ -28,49 +28,56 @@ def render_descendants_tree(issue) def render_issue_row(child, level, hidden = false, columns_list, manage_relations, collapsed_ids, issue) # Construct the row classes with context menu and alternating row colors tr_classes = +"hascontextmenu #{child.css_classes}" - tr_classes << " #{cycle('odd', 'even')}" unless hidden + tr_classes << " #{cycle("odd", "even")}" unless hidden tr_classes << " idnt-#{level}" if level.positive? # Generate buttons for deleting if the user has the right permissions buttons = if manage_relations link_to l(:label_delete_link_to_subtask), issue_path(id: child.id, - issue: { parent_issue_id: '' }, + issue: { parent_issue_id: "" }, back_url: issue_path(issue.id), - no_flash: '1'), + no_flash: "1"), method: :put, data: { confirm: l(:text_are_you_sure) }, title: l(:label_delete_link_to_subtask), - class: 'icon-only icon-link-break' + class: "icon-only icon-link-break" else - ''.html_safe + "".html_safe end buttons << link_to_context_menu # Build the content for each table cell - field_content = content_tag('td', check_box_tag('ids[]', child.id, false, id: nil), class: 'checkbox') + field_content = content_tag("td", check_box_tag("ids[]", child.id, false, id: nil), class: "checkbox") - if child.descendants.any? + # If all children are closed and hidden, do not show the expand/collapse button + with_closed_issues = (params[:with_closed_issues] == "true") + status_column = columns_list.find { |column| column.instance_variable_get(:@name) == :status } + all_descendants_closed = child.descendants.all? do |descendant| + column_content(status_column, descendant) == "Closed" + end + + if child.descendants.any? && (!all_descendants_closed || with_closed_issues) # Generate toggle icon for expanding/collapsing subissues - icon_class = collapsed_ids.include?(child.id) ? 'icon icon-toggle-plus' : 'icon icon-toggle-minus' - expand_icon = content_tag('span', '', class: icon_class, onclick: 'collapseExpand(this)') + icon_class = collapsed_ids.include?(child.id) ? "icon icon-toggle-plus" : "icon icon-toggle-minus" + expand_icon = content_tag("span", "", class: icon_class, onclick: "collapseExpand(this)") subject_content = "#{expand_icon} #{link_to_issue(child, project: (issue.project_id != child.project_id))}".html_safe else subject_content = link_to_issue(child, project: (issue.project_id != child.project_id)) end - field_content << content_tag('td', subject_content, class: 'subject') + field_content << content_tag("td", subject_content, class: "subject") # Add columns with their respective content columns_list.each do |column| - field_content << content_tag('td', column_content(column, child), class: column.css_classes.to_s) + field_content << content_tag("td", column_content(column, child), class: column.css_classes.to_s) end - field_content << content_tag('td', buttons, class: 'buttons') + field_content << content_tag("td", buttons, class: "buttons") # Apply style to hide the row if hidden is true - row_style = hidden ? 'display: none;' : '' - content_tag('tr', field_content, class: tr_classes, id: "issue-#{child.id}", style: row_style).html_safe + row_style = hidden ? "display: none;" : "" + content_tag("tr", field_content, class: tr_classes, id: "issue-#{child.id}", style: row_style).html_safe end def render_issues(issue, render_issue_row, collapsed_ids, columns_list, field_values, sort_dir_file_model, rendered_issues = Set.new) @@ -199,32 +206,32 @@ def render_issue_relations(issue, relations) other_issue = relation.other_issue issue next if other_issue.closed? && !issue_columns_with_closed_issues? - tr_classes = "hascontextmenu #{other_issue.css_classes} #{cycle 'odd', 'even'} #{relation.css_classes_for other_issue}" + tr_classes = "hascontextmenu #{other_issue.css_classes} #{cycle "odd", "even"} #{relation.css_classes_for other_issue}" buttons = if manage_relations - link_to l(:label_relation_delete), - relation_path(relation), - remote: true, - method: :delete, - data: { confirm: l(:text_are_you_sure) }, - title: l(:label_relation_delete), - class: 'icon-only icon-link-break' - else - ''.html_safe - end + link_to l(:label_relation_delete), + relation_path(relation), + remote: true, + method: :delete, + data: { confirm: l(:text_are_you_sure) }, + title: l(:label_relation_delete), + class: "icon-only icon-link-break" + else + "".html_safe + end buttons << link_to_context_menu subject_content = relation.to_s(@issue) { |other| link_to_issue other, project: Setting.cross_project_issue_relations? }.html_safe - field_content = content_tag('td', check_box_tag('ids[]', other_issue.id, false, id: nil), class: 'checkbox') + - content_tag('td', subject_content, class: 'subject') + field_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| - field_content << content_tag('td', column_content(column, other_issue), class: column.css_classes.to_s) + field_content << content_tag("td", column_content(column, other_issue), class: column.css_classes.to_s) end - field_content << content_tag('td', buttons, class: 'buttons') + field_content << content_tag("td", buttons, class: "buttons") - s << content_tag('tr', field_content, + s << content_tag("tr", field_content, id: "relation-#{relation.id}", class: tr_classes) end @@ -243,16 +250,16 @@ def issue_columns_with_closed_issues? issue_scope = RedmineIssueViewColumns.setting :issue_scope return true if issue_scope_with_closed? issue_scope - @issue_columns_with_closed_issues = if issue_scope == 'without_closed_by_default' - RedminePluginKit.true? params[:with_closed_issues] - else - RedminePluginKit.false? params[:without_closed_issues] - end + @issue_columns_with_closed_issues = if issue_scope == "without_closed_by_default" + RedminePluginKit.true? params[:with_closed_issues] + else + RedminePluginKit.false? params[:without_closed_issues] + end end def link_to_closed_issues(issue, issue_scope) - css_class = 'closed-issue-switcher' - if issue_scope == 'without_closed_by_default' + css_class = "closed-issue-switcher" + if issue_scope == "without_closed_by_default" if issue_columns_with_closed_issues? link_to l(:label_hide_closed_issues), issue_path(issue), class: "#{css_class} hide-switch" else @@ -280,19 +287,19 @@ def table_start_for_relations(columns_list) s = +'
' - s << content_tag('th', l(:field_subject), class: 'subject', style: min_widths['Subject'].present? ? "min-width: #{min_widths['Subject']};" : '') + s << content_tag("th", l(:field_subject), class: "subject", style: min_widths["Subject"].present? ? "min-width: #{min_widths["Subject"]};" : "") columns_list.each do |column| - min_width_style = min_widths[column.name.to_s].present? ? "min-width: #{min_widths[column.name.to_s]};" : '' - s << content_tag('th', column.caption, class: column.name, style: min_width_style) + min_width_style = min_widths[column.name.to_s].present? ? "min-width: #{min_widths[column.name.to_s]};" : "" + s << content_tag("th", column.caption, class: column.name, style: min_width_style) end - s << content_tag('th', '', class: 'buttons') - s << '' + s << content_tag("th", "", class: "buttons") + s << "" s end def table_end_for_relations - '
' + "
" end def get_fields_for_project(issue)