Project

General

Profile

Feature #29482 » 0005-Option-to-switch-between-table-list-and-board-list.patch

Marius BĂLTEANU, 2019-09-28 18:45

View differences:

app/controllers/projects_controller.rb
34 34
  helper :issues
35 35
  helper :queries
36 36
  include QueriesHelper
37
  helper :projects_queries
38
  include ProjectsQueriesHelper
37 39
  helper :repositories
38 40
  helper :members
39 41
  helper :trackers
......
50 52

  
51 53
    respond_to do |format|
52 54
      format.html {
53
        @projects = scope.to_a
55
        @entry_count = scope.count
56
        @entry_pages = Paginator.new @entry_count, per_page_option, params['page']
57
        @entries = scope.offset(@entry_pages.offset).limit(@entry_pages.per_page).to_a
54 58
      }
55 59
      format.api  {
56 60
        @offset, @limit = api_offset_and_limit
......
61 65
        projects = scope.reorder(:created_on => :desc).limit(Setting.feeds_limit.to_i).to_a
62 66
        render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
63 67
      }
68
      format.csv {
69
        # Export all entries
70
        @entries = scope.to_a
71
        send_data(query_to_csv(@entries, @query, params), :type => 'text/csv; header=present', :filename => 'projects.csv')
72
      }
64 73
    end
65 74
  end
66 75

  
app/helpers/projects_helper.rb
158 158
    url = bookmark_project_url(project)
159 159
    link_to text, url, remote: true, method: method, class: css
160 160
  end
161

  
162
  def grouped_project_list(projects, query, &block)
163
    ancestors = []
164
    grouped_query_results(projects, query) do |project, group_name, group_count, group_totals|
165
      ancestors.pop while ancestors.any? && !project.is_descendant_of?(ancestors.last)
166
      yield project, ancestors.size, group_name, group_count, group_totals
167
      ancestors << project unless project.leaf?
168
    end
169
  end
161 170
end
app/helpers/projects_queries_helper.rb
1
# frozen_string_literal: true
2

  
3
# Redmine - project management software
4
# Copyright (C) 2006-2017  Jean-Philippe Lang
5
#
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of the GNU General Public License
8
# as published by the Free Software Foundation; either version 2
9
# of the License, or (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19
module ProjectsQueriesHelper
20
  include ApplicationHelper
21

  
22
  def column_value(column, item, value)
23
    if item.is_a?(Project)
24
      case column.name
25
      when :name
26
        link_to_project(item) + (content_tag('span', '', :class => 'icon icon-user my-project', :title => l(:label_my_projects)) if User.current.member_of?(item))
27
      when :short_description
28
        item.description? ? content_tag('div', textilizable(item, :short_description), :class => "wiki") : ''
29
      when :homepage
30
        item.homepage? ? content_tag('div', textilizable(item, :homepage), :class => "wiki") : ''
31
      when :status
32
        get_project_status_label[column.value_object(item)]
33
      when :parent_id
34
        link_to_project(item.parent) unless item.parent.nil?
35
      else
36
        super
37
      end
38
    end
39
  end
40

  
41
  def csv_content(column, item)
42
    if item.is_a?(Project)
43
      case column.name
44
      when :status
45
        get_project_status_label[column.value_object(item)]
46
      when :parent_id
47
        return item.parent.name unless item.parent.nil?
48
      end
49
    end
50
    super
51
  end
52

  
53
  private
54

  
55
  def get_project_status_label
56
    {
57
      Project::STATUS_ACTIVE => l(:project_status_active),
58
      Project::STATUS_CLOSED => l(:project_status_closed)
59
    }
60
  end
61
end
app/helpers/queries_helper.rb
120 120
    render :partial => 'queries/columns', :locals => {:query => query, :tag_name => tag_name}
121 121
  end
122 122

  
123
  def available_display_types_tags(query)
124
    available_display_types = []
125
    query.available_display_types.each do |t|
126
      available_display_types << [l(:"label_display_type_#{t}"), t]
127
    end
128
    select_tag('display_type', options_for_select(available_display_types, @query.display_type), :id => 'display_type')
129
  end
130

  
123 131
  def grouped_query_results(items, query, &block)
124 132
    result_count_by_group = query.result_count_by_group
125 133
    previous_group, first = false, true
app/models/project_query.rb
22 22
  self.queried_class = Project
23 23
  self.view_permission = :search_project
24 24

  
25
  self.available_columns = []
25
  self.available_columns = [
26
    QueryColumn.new(:name, :sortable => "#{Project.table_name}.name"),
27
    QueryColumn.new(:status, :sortable => "#{Project.table_name}.status"),
28
    QueryColumn.new(:short_description, :sortable => "#{Project.table_name}.description", :caption => :field_description),
29
    QueryColumn.new(:homepage, :sortable => "#{Project.table_name}.homepage"),
30
    QueryColumn.new(:identifier, :sortable => "#{Project.table_name}.identifier"),
31
    QueryColumn.new(:parent_id, :sortable => "#{Project.table_name}.lft ASC", :default_order => 'desc', :caption => :field_parent),
32
    QueryColumn.new(:is_public, :sortable => "#{Project.table_name}.is_public", :groupable => true),
33
    QueryColumn.new(:created_on, :sortable => "#{Project.table_name}.created_on", :default_order => 'desc')
34
  ]
26 35

  
27 36
  def initialize(attributes=nil, *args)
28 37
    super attributes
......
48 57
  end
49 58

  
50 59
  def available_columns
51
    []
60
    return @available_columns if @available_columns
61
    @available_columns = self.class.available_columns.dup
62
    @available_columns += ProjectCustomField.visible.
63
                            map {|cf| QueryAssociationCustomFieldColumn.new(:project, cf) }
64
    @available_columns
65
  end
66

  
67
  def available_display_types
68
    ['board', 'list']
69
  end
70

  
71
  def default_columns_names
72
    @default_columns_names ||= [:name, :identifier, :short_description]
73
  end
74

  
75
  def default_sort_criteria
76
    [[]]
52 77
  end
53 78

  
54 79
  def base_scope
app/models/query.rb
408 408
    self.column_names = params[:c] || query_params[:column_names] || self.column_names
409 409
    self.totalable_names = params[:t] || query_params[:totalable_names] || self.totalable_names
410 410
    self.sort_criteria = params[:sort] || query_params[:sort_criteria] || self.sort_criteria
411
    self.display_type = params[:display_type] || query_params[:display_type] || self.display_type
411 412
    self
412 413
  end
413 414

  
......
982 983
    end
983 984
  end
984 985

  
986
  def display_type
987
    options[:display_type] || self.available_display_types.first
988
  end
989

  
990
  def display_type=(type)
991
    unless type || self.available_display_types.include?(type)
992
      type = self.available_display_types.first
993
    end
994
    options[:display_type] = type
995
  end
996

  
997
  def available_display_types
998
    ['list']
999
  end
1000

  
985 1001
  private
986 1002

  
987 1003
  def grouped_query(&block)
app/views/projects/_board.html.erb
1
<div id="projects-index">
2
  <%= render_project_hierarchy(@entries) %>
3
</div>
app/views/projects/_list.html.erb
1
<div class="autoscroll">
2
<table class="list projects odd-even <%= @query.css_classes %>">
3
<thead>
4
  <tr>
5
    <% @query.inline_columns.each do |column| %>
6
      <%= column_header(@query, column) %>
7
    <% end %>
8
  </tr>
9
</thead>
10
<tbody>
11
<% grouped_project_list(entries, @query) do |entry, level, group_name, group_count, group_totals| -%>
12
  <% if group_name %>
13
    <% reset_cycle %>
14
    <tr class="group open">
15
      <td colspan="<%= @query.inline_columns.size %>">
16
        <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
17
        <span class="name"><%= group_name %></span>
18
        <% if group_count %>
19
        <span class="count"><%= group_count %></span>
20
        <% end %>
21
        <span class="totals"><%= group_totals %></span>
22
        <%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}",
23
                             "toggleAllRowGroups(this)", :class => 'toggle-all') %>
24
      </td>
25
    </tr>
26
  <% end %>
27
  <tr id="project-<%= entry.id %>" class="<%= cycle('odd', 'even') %> <%= entry.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>">
28
    <% @query.inline_columns.each do |column| %>
29
    <%= content_tag('td', column_content(column, entry), :class => column.css_classes) %>
30
    <% end %>
31
  </tr>
32
<% end -%>
33
</tbody>
34
</table>
35
</div>
app/views/projects/index.html.erb
11 11
<% end %>
12 12

  
13 13
<% if @query.valid? %>
14
  <% if @projects.empty? %>
14
  <% if @entries.empty? %>
15 15
    <p class="nodata"><%= l(:label_no_data) %></p>
16 16
  <% else %>
17
    <div id="projects-index">
18
      <%= render_project_hierarchy(@projects) %>
19
    </div>
17
    <%= render :partial => @query.display_type, :locals => { :entries => @entries }%>
18
    <span class="pagination"><%= pagination_links_full @entry_pages, @entry_count %></span>
20 19
  <% end %>
21 20
<% end %>
22 21

  
app/views/queries/_form.html.erb
29 29
<% end %>
30 30

  
31 31
<fieldset id="options"><legend><%= l(:label_options) %></legend>
32
<p><label for="query_default_columns"><%=l(:label_default_columns)%></label>
32
  <% if @query.available_display_types.size > 1 %>
33
  <p><label for='display_type'><%= l(:label_display_type) %></label>
34
    <%= available_display_types_tags(@query) %>
35
  </p>
36
<% end %>
37

  
38
<p id ="default_columns"><label for="query_default_columns"><%=l(:label_default_columns)%></label>
33 39
<%= check_box_tag 'default_columns', 1, @query.has_default_columns?, :id => 'query_default_columns',
34 40
      :data => {:disables => "#columns, .block_columns input"} %></p>
35 41

  
36 42
<% unless params[:gantt] %>
37
  <p><label for="query_group_by"><%= l(:field_group_by) %></label>
43
  <p id="group_by"><label id="group_by" for="query_group_by"><%= l(:field_group_by) %></label>
38 44
  <%= select 'query', 'group_by', @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, :include_blank => true %></p>
39 45

  
40 46
  <% unless @query.available_block_columns.empty? %>
......
99 105
    $("input.disable-unless-private").attr('disabled', !private_checked);
100 106
  }).trigger('change');
101 107
});
108

  
109
$(function ($) {
110
  $('#display_type').change(function (e) {
111
    var option = $(e.target).val()
112
    if (option == 'board') {
113
      $('fieldset#columns, fieldset#sort, p#default_columns, p#group_by').hide();
114
    } else {
115
      $('fieldset#columns, fieldset#sort, p#default_columns, p#group_by').show();
116
    }
117
  }).change()
118
});
102 119
<% end %>
app/views/queries/_query_form.html.erb
14 14
  <% if @query.available_columns.any? %>
15 15
    <fieldset id="options" class="collapsible collapsed">
16 16
      <legend onclick="toggleFieldset(this);" class="icon icon-collapsed"><%= l(:label_options) %></legend>
17
      <div style="display: none;">
18
        <table>
17
        <div class="hidden">
18
          <% if @query.available_display_types.size > 1 %>
19
          <div>
20
            <span class="field"><label for='display_type'><%= l(:label_display_type) %></label></span>
21
            <%= available_display_types_tags(@query) %>
22
          </div>
23
          <% end %>
24
          <table id="list" class="<%= 'hidden' if (@query.display_type == 'board') %>">
19 25
          <% if @query.available_columns.any? %>
20 26
            <tr>
21 27
              <td class="field"><%= l(:field_column_names) %></td>
......
65 71
</div>
66 72

  
67 73
<%= error_messages_for @query %>
74

  
75
<%= javascript_tag do %>
76
$(function ($) {
77
  $('#display_type').change(function (e) {
78
    var option = $(e.target).val()
79
    if (option == 'board') {
80
      $('table#list').hide();
81
    } else {
82
      $('table#list').show();
83
    }
84

  
85
  })
86
});
87

  
88
<% end %>
config/locales/en.yml
1071 1071
  label_password_char_class_lowercase: lowercase letters
1072 1072
  label_password_char_class_digits: digits
1073 1073
  label_password_char_class_special_chars: special characters
1074
  label_display_type: Display results as
1075
  label_display_type_list: List
1076
  label_display_type_board: Board  
1074 1077

  
1075 1078
  button_login: Login
1076 1079
  button_submit: Submit
public/stylesheets/application.css
130 130
.clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
131 131

  
132 132
.mobile-show {display: none;}
133
.hidden {display: none;}
133 134

  
134 135
/***** Links *****/
135 136
a, a:link, a:visited{ color: #169; text-decoration: none; }
......
235 236
table.list th, .table-list-header { background-color:#EEEEEE; padding: 4px; white-space:nowrap; font-weight:bold; }
236 237
table.list td {text-align:center; vertical-align:middle; padding-right:10px;}
237 238
table.list td.id { width: 2%; text-align: center;}
238
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;}
239
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;}
240

  
239 241
table.list td.attachments a {display:block;}
240 242
table.list td.tick {width:15%}
241 243
table.list td.checkbox { width: 15px; padding: 2px 0 0 0; }
......
257 259
tr.project.closed, tr.project.archived { color: #aaa; }
258 260
tr.project.closed a, tr.project.archived a { color: #aaa; }
259 261

  
260
tr.project.idnt td.name span {background: url(../images/arrow_right.png) no-repeat 2px 50%; padding-left: 16px;}
261
tr.project.idnt-1 td.name {padding-left: 0.5em;}
262
tr.project.idnt-2 td.name {padding-left: 2em;}
263
tr.project.idnt-3 td.name {padding-left: 3.5em;}
264
tr.project.idnt-4 td.name {padding-left: 5em;}
265
tr.project.idnt-5 td.name {padding-left: 6.5em;}
266
tr.project.idnt-6 td.name {padding-left: 8em;}
267
tr.project.idnt-7 td.name {padding-left: 9.5em;}
268
tr.project.idnt-8 td.name {padding-left: 11em;}
269
tr.project.idnt-9 td.name {padding-left: 12.5em;}
270

  
271 262
tr.issue { text-align: center; white-space: nowrap; }
272 263
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; }
273 264
tr.issue td.relations { text-align: left; }
......
277 268
table.issues td.block_column span {font-weight: bold; display: block; margin-bottom: 4px;}
278 269
table.issues td.block_column pre {white-space:normal;}
279 270

  
280
tr.issue.idnt td.subject {background: url(../images/arrow_right.png) no-repeat 2px 50%;}
281
tr.issue.idnt-1 td.subject {padding-left: 24px; background-position: 8px 50%;}
282
tr.issue.idnt-2 td.subject {padding-left: 40px; background-position: 24px 50%;}
283
tr.issue.idnt-3 td.subject {padding-left: 56px; background-position: 40px 50%;}
284
tr.issue.idnt-4 td.subject {padding-left: 72px; background-position: 56px 50%;}
285
tr.issue.idnt-5 td.subject {padding-left: 88px; background-position: 72px 50%;}
286
tr.issue.idnt-6 td.subject {padding-left: 104px; background-position: 88px 50%;}
287
tr.issue.idnt-7 td.subject {padding-left: 120px; background-position: 104px 50%;}
288
tr.issue.idnt-8 td.subject {padding-left: 136px; background-position: 120px 50%;}
289
tr.issue.idnt-9 td.subject {padding-left: 152px; background-position: 136px 50%;}
271
tr.issue.idnt td.subject, tr.project.idnt td.name {background: url(../images/arrow_right.png) no-repeat 2px 50%;}
272
tr.issue.idnt-1 td.subject, tr.project.idnt-1 td.name {padding-left: 24px; background-position: 8px 50%;}
273
tr.issue.idnt-2 td.subject, tr.project.idnt-2 td.name {padding-left: 40px; background-position: 24px 50%;}
274
tr.issue.idnt-3 td.subject, tr.project.idnt-3 td.name {padding-left: 56px; background-position: 40px 50%;}
275
tr.issue.idnt-4 td.subject, tr.project.idnt-4 td.name {padding-left: 72px; background-position: 56px 50%;}
276
tr.issue.idnt-5 td.subject, tr.project.idnt-5 td.name {padding-left: 88px; background-position: 72px 50%;}
277
tr.issue.idnt-6 td.subject, tr.project.idnt-6 td.name {padding-left: 104px; background-position: 88px 50%;}
278
tr.issue.idnt-7 td.subject, tr.project.idnt-7 td.name {padding-left: 120px; background-position: 104px 50%;}
279
tr.issue.idnt-8 td.subject, tr.project.idnt-8 td.name {padding-left: 136px; background-position: 120px 50%;}
280
tr.issue.idnt-9 td.subject, tr.project.idnt-9 td.name {padding-left: 152px; background-position: 136px 50%;}
290 281

  
291 282
table.issue-report {table-layout:fixed;}
292 283
.issue-report-graph {width: 75%; margin: 2em 0;}
test/functional/projects_controller_test.rb
94 94
    end
95 95
  end
96 96

  
97
  def test_index_as_list_should_format_column_value
98
    get :index, :params => {
99
      :c => ['name', 'status', 'short_description', 'homepage', 'parent_id', 'identifier', 'is_public', 'created_on', 'project.cf_3'],
100
      :display_type => 'list'
101
    }
102
    assert_response :success
103

  
104
    assert_select 'table.projects' do
105
      assert_select 'tr[id=?]', 'project-1' do
106
        assert_select 'td.name a[href=?]', '/projects/ecookbook', :text => 'eCookbook'
107
        assert_select 'td.status', :text => 'active'
108
        assert_select 'td.short_description', :text => 'Recipes management application'
109
        assert_select 'td.homepage a.external', :text => 'http://ecookbook.somenet.foo/'
110
        assert_select 'td.identifier', :text => 'ecookbook'
111
        assert_select 'td.is_public', :text => 'Yes'
112
        assert_select 'td.created_on', :text => '07/19/2006 05:13 PM'
113
        assert_select 'td.project_cf_3.list', :text => 'Stable'
114
      end
115
      assert_select 'tr[id=?]', 'project-4' do
116
        assert_select 'td.parent_id a[href=?]', '/projects/ecookbook', :text => 'eCookbook'
117
      end
118
    end
119
  end
120

  
121
  def test_index_as_list_should_show_my_favourite_projects
122
    @request.session[:user_id] = 1
123
    get :index, :params => {
124
      :display_type => 'list'
125
    }
126

  
127
    assert_response :success
128
    assert_select 'tr[id=?] td.name span[class=?]', 'project-5', 'icon icon-user my-project'
129
  end
130

  
131
  def test_index_as_list_should_indent_projects
132
    @request.session[:user_id] = 1
133
    get :index, :params => {
134
      :c => ['name', 'short_description'],
135
      :sort => 'parent_id:desc,lft:desc',
136
      :display_type => 'list'
137
    }
138
    assert_response :success
139

  
140
    child_level1 = css_select('tr#project-5').map {|e| e.attr('class')}.first.split(' ')
141
    child_level2 = css_select('tr#project-6').map {|e| e.attr('class')}.first.split(' ')
142

  
143
    assert_include 'idnt', child_level1
144
    assert_include 'idnt-1', child_level1
145

  
146
    assert_include 'idnt', child_level2
147
    assert_include 'idnt-2', child_level2
148
  end
149

  
97 150
  def test_autocomplete_js
98 151
    get :autocomplete, :params => {
99 152
        :format => 'js',
test/unit/project_query_test.rb
44 44
    values = query.available_filters['status'][:values]
45 45
    assert_equal ['active', 'closed'], values.map(&:first)
46 46
    assert_equal ['1', '5'], values.map(&:second)
47
  end
48

  
49
  def test_default_columns
50
    q = ProjectQuery.new
51
    assert q.columns.any?
52
    assert q.inline_columns.any?
53
    assert q.block_columns.empty?
54
  end
47 55

  
56
  def test_available_columns_should_include_project_custom_fields
57
    query = ProjectQuery.new
58
    assert_include :"project.cf_3", query.available_columns.map(&:name)
48 59
  end
49 60
end
(12-12/16)