Browse Source

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
tags/4.1.0
Jean-Philippe Lang 4 years ago
parent
commit
45f039187d

+ 10
- 1
app/controllers/projects_controller.rb View File

@@ -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


+ 9
- 0
app/helpers/projects_helper.rb View File

@@ -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

+ 61
- 0
app/helpers/projects_queries_helper.rb View File

@@ -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

+ 8
- 0
app/helpers/queries_helper.rb View File

@@ -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

+ 27
- 2
app/models/project_query.rb View File

@@ -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

+ 16
- 0
app/models/query.rb View File

@@ -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)

+ 3
- 0
app/views/projects/_board.html.erb View File

@@ -0,0 +1,3 @@
<div id="projects-index">
<%= render_project_hierarchy(@entries) %>
</div>

+ 35
- 0
app/views/projects/_list.html.erb View File

@@ -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);">&nbsp;</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>

+ 3
- 4
app/views/projects/index.html.erb View File

@@ -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 %>


+ 19
- 2
app/views/queries/_form.html.erb View File

@@ -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 %>

+ 23
- 2
app/views/queries/_query_form.html.erb View File

@@ -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 %>

+ 3
- 0
config/locales/en.yml View File

@@ -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

+ 13
- 22
public/stylesheets/application.css View File

@@ -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;}

+ 53
- 0
test/functional/projects_controller_test.rb View File

@@ -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',

+ 11
- 0
test/unit/project_query_test.rb View File

@@ -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

Loading…
Cancel
Save