diff --git a/app/controllers/issue_view_columns_controller.rb b/app/controllers/issue_view_columns_controller.rb index e9eddf3..86f93d0 100644 --- a/app/controllers/issue_view_columns_controller.rb +++ b/app/controllers/issue_view_columns_controller.rb @@ -4,10 +4,33 @@ class IssueViewColumnsController < ApplicationController before_action :find_project_by_project_id before_action :authorize before_action :build_query_for_project + skip_before_action :find_project_by_project_id, :authorize, :build_query_for_project, only: :update_collapsed_ids include QueriesHelper include IssueViewColumnsProjectsHelper + def update_collapsed_ids + @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 + return + end + + 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] || [] diff --git a/app/helpers/issue_view_columns_issues_helper.rb b/app/helpers/issue_view_columns_issues_helper.rb index 5d3152d..fcf2b2a 100644 --- a/app/helpers/issue_view_columns_issues_helper.rb +++ b/app/helpers/issue_view_columns_issues_helper.rb @@ -6,49 +6,193 @@ def render_descendants_tree(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? - # continue here if there are fields defined - field_values = +'' + # 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 = +"" 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? + rendered_issues = Set.new - tr_classes = +"hascontextmenu #{child.css_classes} #{cycle 'odd', 'even'}" - tr_classes << " idnt idnt-#{level}" if level.positive? + render_issues(issue, ->(child, level, hidden) { + render_issue_row(child, level, hidden, columns_list, manage_relations, collapsed_ids, issue) + }, collapsed_ids, columns_list, field_values, sort_dir_file_model) - 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') + - content_tag('td', link_to_issue(child, project: (issue.project_id != child.project_id)), class: 'subject') + s.html_safe + end - columns_list.each do |column| - field_content << content_tag('td', column_content(column, child), class: column.css_classes.to_s) + 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 << " 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 + "".html_safe end + buttons << link_to_context_menu - field_content << content_tag('td', buttons, class: 'buttons') - field_values << content_tag('tr', field_content, - class: tr_classes, - id: "issue-#{child.id}").html_safe + # Build the content for each table cell + field_content = content_tag("td", check_box_tag("ids[]", child.id, false, id: nil), class: "checkbox") + + # 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 - s << field_values - s << table_end_for_relations + 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)") + 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 - s.html_safe + 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) + 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(issue, render_issue_row, collapsed_ids, columns_list, field_values, sort_dir_file_model, rendered_issues = Set.new) + render_issue_with_descendants = lambda do |parent, level, hidden = false| + issues_with_subissues = [] + issues_without_subissues = [] + issues = [] + + # 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) && sort_dir_file_model == "1") + + rendered_issues.add(child.id) + + child_hidden = hidden || collapsed_ids.include?(child.id) + + # Traverse sorted issues recursively + if sort_dir_file_model == "1" + 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) + issues_with_subissues.concat(subissues_with) + issues_with_subissues.concat(subissues_without) + else + issues_without_subissues << render_issue_row.call(child, level, child_hidden) if child.parent_id == parent.id + end + else + issues << render_issue_row.call(child, level, hidden) + subissues = render_issue_with_descendants.call(child, level + 1, child_hidden) + issues.concat(subissues) + end + end + + if sort_dir_file_model == "1" + return issues_with_subissues, issues_without_subissues + else + return issues + end + end + + if sort_dir_file_model == "1" + 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 + else + rendered_issues = render_issue_with_descendants.call(issue, 0, false) + field_values << rendered_issues.join("").html_safe + end + end + + 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 + sorting_criteria = columns_sorting_setting.split(",").map do |column_setting| + column_name, direction = column_setting.split(":").map(&:strip) + { column_name: column_name, direction: direction } + end + + # Use the extracted comparison lambda + sorted_issues = issues.to_a.sort(&comparison_lambda(sorting_criteria)) + + sorted_issues + end + + # 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 + + 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 + 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 + + # 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 @@ -62,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 @@ -106,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 @@ -131,20 +275,31 @@ def link_to_closed_issues(issue, issue_scope) private def table_start_for_relations(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 + s = +'
' - s << content_tag('th', l(:field_subject), class: '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| - s << content_tag('th', column.caption, class: column.name) + 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) 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..988f0f8 --- /dev/null +++ b/app/views/collapse_expand/_collapse_expand.js.erb @@ -0,0 +1,64 @@ + diff --git a/app/views/settings/_issue_view_columns_settings.html.slim b/app/views/settings/_issue_view_columns_settings.html.slim index eaa8466..e33543a 100644 --- a/app/views/settings/_issue_view_columns_settings.html.slim +++ b/app/views/settings/_issue_view_columns_settings.html.slim @@ -12,8 +12,22 @@ 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 + +br + += 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 new file mode 100644 index 0000000..1d61780 --- /dev/null +++ b/app/views/settings/_min_width_sort_criteria.html.erb @@ -0,0 +1,166 @@ +<% columns = IssueQuery.new.available_inline_columns %> +<% column_captions = columns.each_with_object({}) do |column, hash| + hash[column.name.to_s] = column.caption + end %> + +<% #Retrieve settings %> +<% settings = @settings || [] %> +<% column_settings = settings.find { |setting| setting[0] == "issue_list_defaults" } %> +<% column_names = column_settings&.dig(1, "column_names") || [] %> +<% columns_sorting = settings["columns_sorting"] || "" %> +<% columns_min_width = settings["columns_min_width"] || "" %> + +<% #Re-arrange column_names based on sorting order %> +<% 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[]" %> + +
+ + + + +
+ +
+

<%= l(:label_sorting_priority_columns) %>

+ <%= select_tag tag_name, + options_for_select(column_names.map { |name| [column_captions[name], name] }), + id: selected_tag_id, + multiple: true, + size: 12, + class: "capitalize-text_box_issue_view_columns_settings" %> +
+ +
+
+

<%= l('column_name_setting_label') %>

+

<%= l('sorting_order_setting_label') %>

+

<%= l('min_width_setting_label') %>

+
+
+ <% column_names.each do |column| %> +
+ + + + +
+ <% end %> +
+
+
+ +<%= hidden_field_tag "settings[columns_sorting]", nil, id: "columns_sorting_field" %> +<%= hidden_field_tag "settings[columns_min_width]", nil, id: "columns_min_width_field" %> + + diff --git a/assets/stylesheets/issue_view_columns.css b/assets/stylesheets/issue_view_columns.css index 7521f3f..b65daf6 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_issue_view_columns_settings { + display: flex; + justify-content: space-around; + align-items: center; +} + +.capitalize-text_box_issue_view_columns_settings { + text-transform: capitalize; +} + +#input_header_box_issue_view_columns_settings { + display: flex; + justify-content: space-around; + align-items: center; +} diff --git a/config/locales/en.yml b/config/locales/en.yml index 36e56eb..03b6482 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -15,3 +15,23 @@ 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. + label_sorting_columns: Set sorting criteria and column min-width + info_columns_sorting: "Specify the sorting order for each column, in order of relevance. Example: Updated:DESC, Created:ASC" + label_columns_min_width: Set min-width property for columns + info_columns_min_width: "Specify the minimum width for each column. Example: Project:200px, Status:30vw" + access_denied: "Access denied" + invalid_json_format: "Invalid JSON format" + update_successful: "Update successful" + update_failed: "Update failed" + 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_sort_sorting_option: "No Sort" + ascending_sorting_option: "Ascending" + descending_sorting_option: "Descending" diff --git a/config/routes.rb b/config/routes.rb index da36f48..517b98c 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 :issue_view_columns, only: [] do + member do + patch 'update_collapsed_ids' + end + end end diff --git a/config/settings.yml b/config/settings.yml index aa43f00..85eecef 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -1,5 +1,8 @@ --- +sort_dir_file_model: '0' issue_scope: all issue_list_defaults: column_names: +columns_sorting: '' +columns_min_width: '' 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..6047609 --- /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, :string, limit: 1000 + 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 diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb index aa9a286..43a0d85 100644 --- a/test/functional/issues_controller_test.rb +++ b/test/functional/issues_controller_test.rb @@ -149,4 +149,126 @@ def test_show_without_closed_subtasks assert_select "tr#issue-#{closed_issue.id}", count: 0 end end + + def test_render_issue_tree_dir_file_model + parent_issue = Issue.generate!(project_id: 1) + child_issue1 = Issue.generate!(project_id: 1, parent_issue_id: parent_issue.id) + child_issue2 = Issue.generate!(project_id: 1, parent_issue_id: parent_issue.id) + grandchild_issue = Issue.generate!(project_id: 1, parent_issue_id: child_issue2.id) + + @request.session[:user_id] = 1 + + with_plugin_settings "redmine_issue_view_columns", + sort_dir_file_model: "1" do + get :show, params: { id: parent_issue.id } + + assert_response :success + + issue_rows = css_select("#issue_tree tr").map(&:to_html) + + # Extract the issue IDs from the rows + issue_ids = issue_rows.map { |row| row[/id="issue-(\d+)"/, 1].to_i } + + # Assert that child_issue2 appears before child_issue1 + assert issue_ids.index(child_issue2.id) < issue_ids.index(child_issue1.id), "Child issue 2 should appear before child issue 1" + + # Assert that grandchild_issue appears after child_issue2 + assert issue_ids.index(grandchild_issue.id) > issue_ids.index(child_issue2.id), "Grandchild issue should appear after child issue 2" + end + end + + def test_render_issue_tree_default + parent_issue = Issue.generate!(project_id: 1) + child_issue1 = Issue.generate!(project_id: 1, parent_issue_id: parent_issue.id) + child_issue2 = Issue.generate!(project_id: 1, parent_issue_id: parent_issue.id) + grandchild_issue = Issue.generate!(project_id: 1, parent_issue_id: child_issue2.id) + + @request.session[:user_id] = 1 + + with_plugin_settings "redmine_issue_view_columns", + sort_dir_file_model: "0" do + get :show, params: { id: parent_issue.id } + + assert_response :success + + issue_rows = css_select("#issue_tree tr").map(&:to_html) + + # Extract the issue IDs from the rows + issue_ids = issue_rows.map { |row| row[/id="issue-(\d+)"/, 1].to_i } + + # Assert that child_issue2 appears after child_issue1 + assert issue_ids.index(child_issue2.id) > issue_ids.index(child_issue1.id), "Child issue 2 should appear before child issue 1" + + # Assert that grandchild_issue appears after child_issue2 + assert issue_ids.index(grandchild_issue.id) > issue_ids.index(child_issue2.id), "Grandchild issue should appear after child issue 2" + end + end + + def test_min_width_setting_applies + issue = Issue.generate!(project_id: 1) + child_issue = Issue.generate!(project_id: 1, parent_issue_id: issue.id) + + @request.session[:user_id] = 1 + + with_plugin_settings "redmine_issue_view_columns", + columns_min_width: "status:300px" do + get :show, params: { id: issue.id } + + assert_response :success + + assert_select "th.status", true, "Expected 'Status' column to be present" + + 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") + end + end + + def test_sorting_criteria_and_order_for_columns + parent_issue = Issue.generate!(project_id: 1) + issue1 = Issue.generate!(project_id: 1, parent_issue_id: parent_issue.id, status_id: 1, author_id: 3) + issue2 = Issue.generate!(project_id: 1, parent_issue_id: parent_issue.id, status_id: 1, author_id: 2) + issue3 = Issue.generate!(project_id: 1, parent_issue_id: parent_issue.id, status_id: 2, author_id: 2) + + @request.session[:user_id] = 1 + + with_plugin_settings "redmine_issue_view_columns", + 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_ids = issue_rows.map { |row| row[/id="issue-(\d+)"/, 1].to_i } + + sorted_ids = [issue3.id, issue2.id, issue1.id] + + #Check that issues were sorted correctly by defined sorting criteria + assert_equal sorted_ids, issue_ids, "Issues are not sorted correctly by status and author" + end + end + + def test_collapsed_issue_is_not_displayed + parent_issue = Issue.generate!(project_id: 1) + child_issue = Issue.generate!(project_id: 1, parent_issue_id: parent_issue.id) + grandchild_issue = Issue.generate!(project_id: 1, parent_issue_id: child_issue.id) + + parent_issue.reload + parent_issue.update!(collapsed_ids: child_issue.id.to_s) + + @request.session[:user_id] = 1 + + get :show, params: { id: parent_issue.id } + + assert_response :success + + grandchild_issue_row = css_select("tr#issue-#{grandchild_issue.id}").first + + 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") + end end