diff options
author | Jean-Philippe Lang <jp_lang@yahoo.fr> | 2019-10-19 11:42:20 +0000 |
---|---|---|
committer | Jean-Philippe Lang <jp_lang@yahoo.fr> | 2019-10-19 11:42:20 +0000 |
commit | 45f039187df03e8925cb0d08ccc1838c9ea2d5c2 (patch) | |
tree | 3bbb8958c71b24e65123c0e8f29d6f15726424d5 | |
parent | ecd9d9ba2f43f2f5a88c1d34d34d07dae40dfe4e (diff) | |
download | redmine-45f039187df03e8925cb0d08ccc1838c9ea2d5c2.tar.gz redmine-45f039187df03e8925cb0d08ccc1838c9ea2d5c2.zip |
Option to switch between table list and board list (#29482).
Patch by Marius BALTEANU.
git-svn-id: http://svn.redmine.org/redmine/trunk@18765 e93f8b46-1217-0410-a6f0-8f06a7374b81
-rw-r--r-- | app/controllers/projects_controller.rb | 11 | ||||
-rw-r--r-- | app/helpers/projects_helper.rb | 9 | ||||
-rw-r--r-- | app/helpers/projects_queries_helper.rb | 61 | ||||
-rw-r--r-- | app/helpers/queries_helper.rb | 8 | ||||
-rw-r--r-- | app/models/project_query.rb | 29 | ||||
-rw-r--r-- | app/models/query.rb | 16 | ||||
-rw-r--r-- | app/views/projects/_board.html.erb | 3 | ||||
-rw-r--r-- | app/views/projects/_list.html.erb | 35 | ||||
-rw-r--r-- | app/views/projects/index.html.erb | 7 | ||||
-rw-r--r-- | app/views/queries/_form.html.erb | 21 | ||||
-rw-r--r-- | app/views/queries/_query_form.html.erb | 25 | ||||
-rw-r--r-- | config/locales/en.yml | 3 | ||||
-rw-r--r-- | public/stylesheets/application.css | 35 | ||||
-rw-r--r-- | test/functional/projects_controller_test.rb | 53 | ||||
-rw-r--r-- | test/unit/project_query_test.rb | 11 |
15 files changed, 294 insertions, 33 deletions
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index e7a79e495..b9bda6b43 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -34,6 +34,8 @@ class ProjectsController < ApplicationController helper :issues helper :queries include QueriesHelper + helper :projects_queries + include ProjectsQueriesHelper helper :repositories helper :members helper :trackers @@ -50,7 +52,9 @@ class ProjectsController < ApplicationController respond_to do |format| format.html { - @projects = scope.to_a + @entry_count = scope.count + @entry_pages = Paginator.new @entry_count, per_page_option, params['page'] + @entries = scope.offset(@entry_pages.offset).limit(@entry_pages.per_page).to_a } format.api { @offset, @limit = api_offset_and_limit @@ -61,6 +65,11 @@ class ProjectsController < ApplicationController projects = scope.reorder(:created_on => :desc).limit(Setting.feeds_limit.to_i).to_a render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}") } + format.csv { + # Export all entries + @entries = scope.to_a + send_data(query_to_csv(@entries, @query, params), :type => 'text/csv; header=present', :filename => 'projects.csv') + } end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index bdb1ac44c..335db8b0b 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -158,4 +158,13 @@ module ProjectsHelper url = bookmark_project_url(project) link_to text, url, remote: true, method: method, class: css end + + def grouped_project_list(projects, query, &block) + ancestors = [] + grouped_query_results(projects, query) do |project, group_name, group_count, group_totals| + ancestors.pop while ancestors.any? && !project.is_descendant_of?(ancestors.last) + yield project, ancestors.size, group_name, group_count, group_totals + ancestors << project unless project.leaf? + end + end end diff --git a/app/helpers/projects_queries_helper.rb b/app/helpers/projects_queries_helper.rb new file mode 100644 index 000000000..b5de291c0 --- /dev/null +++ b/app/helpers/projects_queries_helper.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +# Redmine - project management software +# Copyright (C) 2006-2017 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +module ProjectsQueriesHelper + include ApplicationHelper + + def column_value(column, item, value) + if item.is_a?(Project) + case column.name + when :name + link_to_project(item) + (content_tag('span', '', :class => 'icon icon-user my-project', :title => l(:label_my_projects)) if User.current.member_of?(item)) + when :short_description + item.description? ? content_tag('div', textilizable(item, :short_description), :class => "wiki") : '' + when :homepage + item.homepage? ? content_tag('div', textilizable(item, :homepage), :class => "wiki") : '' + when :status + get_project_status_label[column.value_object(item)] + when :parent_id + link_to_project(item.parent) unless item.parent.nil? + else + super + end + end + end + + def csv_content(column, item) + if item.is_a?(Project) + case column.name + when :status + get_project_status_label[column.value_object(item)] + when :parent_id + return item.parent.name unless item.parent.nil? + end + end + super + end + + private + + def get_project_status_label + { + Project::STATUS_ACTIVE => l(:project_status_active), + Project::STATUS_CLOSED => l(:project_status_closed) + } + end +end diff --git a/app/helpers/queries_helper.rb b/app/helpers/queries_helper.rb index fa03a76ef..9ed2dd21d 100644 --- a/app/helpers/queries_helper.rb +++ b/app/helpers/queries_helper.rb @@ -120,6 +120,14 @@ module QueriesHelper render :partial => 'queries/columns', :locals => {:query => query, :tag_name => tag_name} end + def available_display_types_tags(query) + available_display_types = [] + query.available_display_types.each do |t| + available_display_types << [l(:"label_display_type_#{t}"), t] + end + select_tag('display_type', options_for_select(available_display_types, @query.display_type), :id => 'display_type') + end + def grouped_query_results(items, query, &block) result_count_by_group = query.result_count_by_group previous_group, first = false, true diff --git a/app/models/project_query.rb b/app/models/project_query.rb index 2bc6b40e1..ecf3e687b 100644 --- a/app/models/project_query.rb +++ b/app/models/project_query.rb @@ -22,7 +22,16 @@ class ProjectQuery < Query self.queried_class = Project self.view_permission = :search_project - self.available_columns = [] + self.available_columns = [ + QueryColumn.new(:name, :sortable => "#{Project.table_name}.name"), + QueryColumn.new(:status, :sortable => "#{Project.table_name}.status"), + QueryColumn.new(:short_description, :sortable => "#{Project.table_name}.description", :caption => :field_description), + QueryColumn.new(:homepage, :sortable => "#{Project.table_name}.homepage"), + QueryColumn.new(:identifier, :sortable => "#{Project.table_name}.identifier"), + QueryColumn.new(:parent_id, :sortable => "#{Project.table_name}.lft ASC", :default_order => 'desc', :caption => :field_parent), + QueryColumn.new(:is_public, :sortable => "#{Project.table_name}.is_public", :groupable => true), + QueryColumn.new(:created_on, :sortable => "#{Project.table_name}.created_on", :default_order => 'desc') + ] def initialize(attributes=nil, *args) super attributes @@ -48,7 +57,23 @@ class ProjectQuery < Query end def available_columns - [] + return @available_columns if @available_columns + @available_columns = self.class.available_columns.dup + @available_columns += ProjectCustomField.visible. + map {|cf| QueryAssociationCustomFieldColumn.new(:project, cf) } + @available_columns + end + + def available_display_types + ['board', 'list'] + end + + def default_columns_names + @default_columns_names ||= [:name, :identifier, :short_description] + end + + def default_sort_criteria + [[]] end def base_scope diff --git a/app/models/query.rb b/app/models/query.rb index 7372f712e..19db899d8 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -409,6 +409,7 @@ class Query < ActiveRecord::Base self.column_names = params[:c] || query_params[:column_names] || self.column_names self.totalable_names = params[:t] || query_params[:totalable_names] || self.totalable_names self.sort_criteria = params[:sort] || query_params[:sort_criteria] || self.sort_criteria + self.display_type = params[:display_type] || query_params[:display_type] || self.display_type self end @@ -983,6 +984,21 @@ class Query < ActiveRecord::Base end end + def display_type + options[:display_type] || self.available_display_types.first + end + + def display_type=(type) + unless type || self.available_display_types.include?(type) + type = self.available_display_types.first + end + options[:display_type] = type + end + + def available_display_types + ['list'] + end + private def grouped_query(&block) diff --git a/app/views/projects/_board.html.erb b/app/views/projects/_board.html.erb new file mode 100644 index 000000000..7d63a533d --- /dev/null +++ b/app/views/projects/_board.html.erb @@ -0,0 +1,3 @@ +<div id="projects-index"> + <%= render_project_hierarchy(@entries) %> +</div> diff --git a/app/views/projects/_list.html.erb b/app/views/projects/_list.html.erb new file mode 100644 index 000000000..8a1efc204 --- /dev/null +++ b/app/views/projects/_list.html.erb @@ -0,0 +1,35 @@ +<div class="autoscroll"> +<table class="list projects odd-even <%= @query.css_classes %>"> +<thead> + <tr> + <% @query.inline_columns.each do |column| %> + <%= column_header(@query, column) %> + <% end %> + </tr> +</thead> +<tbody> +<% grouped_project_list(entries, @query) do |entry, level, group_name, group_count, group_totals| -%> + <% if group_name %> + <% reset_cycle %> + <tr class="group open"> + <td colspan="<%= @query.inline_columns.size %>"> + <span class="expander" onclick="toggleRowGroup(this);"> </span> + <span class="name"><%= group_name %></span> + <% if group_count %> + <span class="count"><%= group_count %></span> + <% end %> + <span class="totals"><%= group_totals %></span> + <%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}", + "toggleAllRowGroups(this)", :class => 'toggle-all') %> + </td> + </tr> + <% end %> + <tr id="project-<%= entry.id %>" class="<%= cycle('odd', 'even') %> <%= entry.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>"> + <% @query.inline_columns.each do |column| %> + <%= content_tag('td', column_content(column, entry), :class => column.css_classes) %> + <% end %> + </tr> +<% end -%> +</tbody> +</table> +</div> diff --git a/app/views/projects/index.html.erb b/app/views/projects/index.html.erb index 3c2543b43..067c9bdee 100644 --- a/app/views/projects/index.html.erb +++ b/app/views/projects/index.html.erb @@ -11,12 +11,11 @@ <% end %> <% if @query.valid? %> - <% if @projects.empty? %> + <% if @entries.empty? %> <p class="nodata"><%= l(:label_no_data) %></p> <% else %> - <div id="projects-index"> - <%= render_project_hierarchy(@projects) %> - </div> + <%= render :partial => @query.display_type, :locals => { :entries => @entries }%> + <span class="pagination"><%= pagination_links_full @entry_pages, @entry_count %></span> <% end %> <% end %> diff --git a/app/views/queries/_form.html.erb b/app/views/queries/_form.html.erb index a2dd9f589..12a1d0202 100644 --- a/app/views/queries/_form.html.erb +++ b/app/views/queries/_form.html.erb @@ -29,12 +29,18 @@ <% end %> <fieldset id="options"><legend><%= l(:label_options) %></legend> -<p><label for="query_default_columns"><%=l(:label_default_columns)%></label> + <% if @query.available_display_types.size > 1 %> + <p><label for='display_type'><%= l(:label_display_type) %></label> + <%= available_display_types_tags(@query) %> + </p> +<% end %> + +<p id ="default_columns"><label for="query_default_columns"><%=l(:label_default_columns)%></label> <%= check_box_tag 'default_columns', 1, @query.has_default_columns?, :id => 'query_default_columns', :data => {:disables => "#columns, .block_columns input"} %></p> <% unless params[:gantt] %> - <p><label for="query_group_by"><%= l(:field_group_by) %></label> + <p id="group_by"><label id="group_by" for="query_group_by"><%= l(:field_group_by) %></label> <%= select 'query', 'group_by', @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, :include_blank => true %></p> <% unless @query.available_block_columns.empty? %> @@ -99,4 +105,15 @@ $(document).ready(function(){ $("input.disable-unless-private").attr('disabled', !private_checked); }).trigger('change'); }); + +$(function ($) { + $('#display_type').change(function (e) { + var option = $(e.target).val() + if (option == 'board') { + $('fieldset#columns, fieldset#sort, p#default_columns, p#group_by').hide(); + } else { + $('fieldset#columns, fieldset#sort, p#default_columns, p#group_by').show(); + } + }).change() +}); <% end %> diff --git a/app/views/queries/_query_form.html.erb b/app/views/queries/_query_form.html.erb index 65bcc3eb5..62d156684 100644 --- a/app/views/queries/_query_form.html.erb +++ b/app/views/queries/_query_form.html.erb @@ -14,8 +14,14 @@ <% if @query.available_columns.any? %> <fieldset id="options" class="collapsible collapsed"> <legend onclick="toggleFieldset(this);" class="icon icon-collapsed"><%= l(:label_options) %></legend> - <div style="display: none;"> - <table> + <div class="hidden"> + <% if @query.available_display_types.size > 1 %> + <div> + <span class="field"><label for='display_type'><%= l(:label_display_type) %></label></span> + <%= available_display_types_tags(@query) %> + </div> + <% end %> + <table id="list" class="<%= 'hidden' if (@query.display_type == 'board') %>"> <% if @query.available_columns.any? %> <tr> <td class="field"><%= l(:field_column_names) %></td> @@ -65,3 +71,18 @@ </div> <%= error_messages_for @query %> + +<%= javascript_tag do %> +$(function ($) { + $('#display_type').change(function (e) { + var option = $(e.target).val() + if (option == 'board') { + $('table#list').hide(); + } else { + $('table#list').show(); + } + + }) +}); + +<% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 4c90deb9d..e5ae3220f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1072,6 +1072,9 @@ en: label_password_char_class_lowercase: lowercase letters label_password_char_class_digits: digits label_password_char_class_special_chars: special characters + label_display_type: Display results as + label_display_type_list: List + label_display_type_board: Board button_login: Login button_submit: Submit diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 7aa1bca54..0abd4fdae 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -130,6 +130,7 @@ div.modal .box p {margin: 0.3em 0;} .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; } .mobile-show {display: none;} +.hidden {display: none;} /***** Links *****/ a, a:link, a:visited{ color: #169; text-decoration: none; } @@ -235,7 +236,8 @@ table.list, .table-list { border: 1px solid #e4e4e4; width: 100%; margin-bottom: table.list th, .table-list-header { background-color:#EEEEEE; padding: 4px; white-space:nowrap; font-weight:bold; } table.list td {text-align:center; vertical-align:middle; padding-right:10px;} table.list td.id { width: 2%; text-align: center;} -table.list td.name, table.list td.description, table.list td.subject, table.list td.comments, table.list td.roles, table.list td.attachments, table.list td.text {text-align: left;} +table.list td.name, table.list td.description, table.list td.subject, table.list td.comments, table.list td.roles, table.list td.attachments, table.list td.text, table.list td.short_description {text-align: left;} + table.list td.attachments a {display:block;} table.list td.tick {width:15%} table.list td.checkbox { width: 15px; padding: 2px 0 0 0; } @@ -257,17 +259,6 @@ tr.project td.name a { white-space:nowrap; } tr.project.closed, tr.project.archived { color: #aaa; } tr.project.closed a, tr.project.archived a { color: #aaa; } -tr.project.idnt td.name span {background: url(../images/arrow_right.png) no-repeat 2px 50%; padding-left: 16px;} -tr.project.idnt-1 td.name {padding-left: 0.5em;} -tr.project.idnt-2 td.name {padding-left: 2em;} -tr.project.idnt-3 td.name {padding-left: 3.5em;} -tr.project.idnt-4 td.name {padding-left: 5em;} -tr.project.idnt-5 td.name {padding-left: 6.5em;} -tr.project.idnt-6 td.name {padding-left: 8em;} -tr.project.idnt-7 td.name {padding-left: 9.5em;} -tr.project.idnt-8 td.name {padding-left: 11em;} -tr.project.idnt-9 td.name {padding-left: 12.5em;} - tr.issue { text-align: center; white-space: nowrap; } tr.issue td.subject, tr.issue td.category, td.assigned_to, td.last_updated_by, tr.issue td.string, tr.issue td.text, tr.issue td.list, tr.issue td.relations, tr.issue td.parent { white-space: normal; } tr.issue td.relations { text-align: left; } @@ -277,16 +268,16 @@ table.issues td.block_column {color:#777; font-size:90%; padding:4px 4px 4px 24p table.issues td.block_column span {font-weight: bold; display: block; margin-bottom: 4px;} table.issues td.block_column pre {white-space:normal;} -tr.issue.idnt td.subject {background: url(../images/arrow_right.png) no-repeat 2px 50%;} -tr.issue.idnt-1 td.subject {padding-left: 24px; background-position: 8px 50%;} -tr.issue.idnt-2 td.subject {padding-left: 40px; background-position: 24px 50%;} -tr.issue.idnt-3 td.subject {padding-left: 56px; background-position: 40px 50%;} -tr.issue.idnt-4 td.subject {padding-left: 72px; background-position: 56px 50%;} -tr.issue.idnt-5 td.subject {padding-left: 88px; background-position: 72px 50%;} -tr.issue.idnt-6 td.subject {padding-left: 104px; background-position: 88px 50%;} -tr.issue.idnt-7 td.subject {padding-left: 120px; background-position: 104px 50%;} -tr.issue.idnt-8 td.subject {padding-left: 136px; background-position: 120px 50%;} -tr.issue.idnt-9 td.subject {padding-left: 152px; background-position: 136px 50%;} +tr.issue.idnt td.subject, tr.project.idnt td.name {background: url(../images/arrow_right.png) no-repeat 2px 50%;} +tr.issue.idnt-1 td.subject, tr.project.idnt-1 td.name {padding-left: 24px; background-position: 8px 50%;} +tr.issue.idnt-2 td.subject, tr.project.idnt-2 td.name {padding-left: 40px; background-position: 24px 50%;} +tr.issue.idnt-3 td.subject, tr.project.idnt-3 td.name {padding-left: 56px; background-position: 40px 50%;} +tr.issue.idnt-4 td.subject, tr.project.idnt-4 td.name {padding-left: 72px; background-position: 56px 50%;} +tr.issue.idnt-5 td.subject, tr.project.idnt-5 td.name {padding-left: 88px; background-position: 72px 50%;} +tr.issue.idnt-6 td.subject, tr.project.idnt-6 td.name {padding-left: 104px; background-position: 88px 50%;} +tr.issue.idnt-7 td.subject, tr.project.idnt-7 td.name {padding-left: 120px; background-position: 104px 50%;} +tr.issue.idnt-8 td.subject, tr.project.idnt-8 td.name {padding-left: 136px; background-position: 120px 50%;} +tr.issue.idnt-9 td.subject, tr.project.idnt-9 td.name {padding-left: 152px; background-position: 136px 50%;} table.issue-report {table-layout:fixed;} .issue-report-graph {width: 75%; margin: 2em 0;} diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb index 3f4dd6967..5e1efd240 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -94,6 +94,59 @@ class ProjectsControllerTest < Redmine::ControllerTest end end + def test_index_as_list_should_format_column_value + get :index, :params => { + :c => ['name', 'status', 'short_description', 'homepage', 'parent_id', 'identifier', 'is_public', 'created_on', 'project.cf_3'], + :display_type => 'list' + } + assert_response :success + + assert_select 'table.projects' do + assert_select 'tr[id=?]', 'project-1' do + assert_select 'td.name a[href=?]', '/projects/ecookbook', :text => 'eCookbook' + assert_select 'td.status', :text => 'active' + assert_select 'td.short_description', :text => 'Recipes management application' + assert_select 'td.homepage a.external', :text => 'http://ecookbook.somenet.foo/' + assert_select 'td.identifier', :text => 'ecookbook' + assert_select 'td.is_public', :text => 'Yes' + assert_select 'td.created_on', :text => '07/19/2006 05:13 PM' + assert_select 'td.project_cf_3.list', :text => 'Stable' + end + assert_select 'tr[id=?]', 'project-4' do + assert_select 'td.parent_id a[href=?]', '/projects/ecookbook', :text => 'eCookbook' + end + end + end + + def test_index_as_list_should_show_my_favourite_projects + @request.session[:user_id] = 1 + get :index, :params => { + :display_type => 'list' + } + + assert_response :success + assert_select 'tr[id=?] td.name span[class=?]', 'project-5', 'icon icon-user my-project' + end + + def test_index_as_list_should_indent_projects + @request.session[:user_id] = 1 + get :index, :params => { + :c => ['name', 'short_description'], + :sort => 'parent_id:desc,lft:desc', + :display_type => 'list' + } + assert_response :success + + child_level1 = css_select('tr#project-5').map {|e| e.attr('class')}.first.split(' ') + child_level2 = css_select('tr#project-6').map {|e| e.attr('class')}.first.split(' ') + + assert_include 'idnt', child_level1 + assert_include 'idnt-1', child_level1 + + assert_include 'idnt', child_level2 + assert_include 'idnt-2', child_level2 + end + def test_autocomplete_js get :autocomplete, :params => { :format => 'js', diff --git a/test/unit/project_query_test.rb b/test/unit/project_query_test.rb index f4182e560..43e95d308 100644 --- a/test/unit/project_query_test.rb +++ b/test/unit/project_query_test.rb @@ -44,6 +44,17 @@ class ProjectQueryTest < ActiveSupport::TestCase values = query.available_filters['status'][:values] assert_equal ['active', 'closed'], values.map(&:first) assert_equal ['1', '5'], values.map(&:second) + end + + def test_default_columns + q = ProjectQuery.new + assert q.columns.any? + assert q.inline_columns.any? + assert q.block_columns.empty? + end + def test_available_columns_should_include_project_custom_fields + query = ProjectQuery.new + assert_include :"project.cf_3", query.available_columns.map(&:name) end end |