diff --git a/app/models/issue_query.rb b/app/models/issue_query.rb index 7e8c2e6a8b..d469baf315 100644 --- a/app/models/issue_query.rb +++ b/app/models/issue_query.rb @@ -75,10 +75,20 @@ class IssueQuery < Query options[:draw_progress_line] = (arg == '1' ? '1' : nil) end + def draw_selected_columns + r = options[:draw_selected_columns] + r == '1' + end + + def draw_selected_columns=(arg) + options[:draw_selected_columns] = (arg == '1' ? '1' : nil) + end + def build_from_params(params, defaults={}) super self.draw_relations = params[:draw_relations] || (params[:query] && params[:query][:draw_relations]) || options[:draw_relations] self.draw_progress_line = params[:draw_progress_line] || (params[:query] && params[:query][:draw_progress_line]) || options[:draw_progress_line] + self.draw_selected_columns = params[:draw_selected_columns] || (params[:query] && params[:query][:draw_selected_columns]) || options[:draw_progress_line] self end diff --git a/app/views/gantts/show.html.erb b/app/views/gantts/show.html.erb index a51abdcde4..9866b811b2 100644 --- a/app/views/gantts/show.html.erb +++ b/app/views/gantts/show.html.erb @@ -24,6 +24,20 @@ <%= l(:label_options) %>
+ + +
+
+ + <%= l(:field_column_names) %> + + + <%= render_query_columns_selection(@query) %> +
+
@@ -125,16 +139,16 @@

<%= l(:notice_gantt_chart_truncated, :max => @gantt.max_rows) %>

<% end %> - +
- - +<% @query.columns.each do |column| %> + <% next if Redmine::Helpers::Gantt::UNAVAILABLE_COLUMNS.include?(column.name) %> + +<% end %>
+ <% style = "" style += "position:relative;" style += "height: #{t_height + 24}px;" - style += "width: #{subject_width + 1}px;" + style += "width: #{subject_width}px;" %> - <%= content_tag(:div, :style => style, :class => "gantt_subjects_container") do %> + <%= content_tag(:div, :style => style, :class => "gantt_subjects_container #{'draw_selected_columns' if @query.draw_selected_columns}") do %> <% style = "" style += "width: #{subject_width}px;" @@ -146,7 +160,6 @@ style = "" style += "width: #{subject_width}px;" style += "height: #{t_height}px;" - style += 'border-left: 1px solid #c0c0c0;' style += 'overflow: hidden;' %> <%= content_tag(:div, "", :style => style, :class => "gantt_hdr") %> @@ -157,7 +170,30 @@ <% end %> <% end %> + <% + style = "position: relative;" + style += "height: #{t_height + 24}px;" + %> + <%= content_tag(:div, :style => style, :class => "gantt_#{column.name}_container gantt_selected_column_container") do %> + <% + style = "height: #{t_height}px;" + style += 'overflow: hidden;' + %> + <%= content_tag(:div, '', :style => style, :class => "gantt_hdr") %> + <% + style = "height: #{headers_height}px;" + style += 'background: #eee;' + %> + <%= content_tag(:div, content_tag(:p, column.caption, :class => 'gantt_hdr_selected_column_name'), :style => style, :class => "gantt_hdr") %> + <%= content_tag(:div, :class => "gantt_#{column.name} gantt_selected_column_content") do %> + <%= @gantt.selected_column_content({:column => column, :top => headers_height + 8, :zoom => zoom, :g_width => g_width}).html_safe %> + <% end %> + <% end %> +
<% @@ -371,10 +407,11 @@ <%= javascript_tag do %> var issue_relation_type = <%= raw Redmine::Helpers::Gantt::DRAW_TYPES.to_json %>; $(function() { + disable_unavailable_columns('<%= Redmine::Helpers::Gantt::UNAVAILABLE_COLUMNS.map(&:to_s).join(',') %>'.split(',')); drawGanttHandler(); resizableSubjectColumn(); - $("#draw_relations").change(drawGanttHandler); - $("#draw_progress_line").change(drawGanttHandler); + drawSelectedColumns(); + $("#draw_relations, #draw_progress_line, #draw_selected_columns").change(drawGanttHandler); $('div.gantt_subjects .expander').on('click', ganttEntryClick); }); $(window).resize(function() { diff --git a/app/views/queries/_form.html.erb b/app/views/queries/_form.html.erb index 7f79906efb..6e25710ca1 100644 --- a/app/views/queries/_form.html.erb +++ b/app/views/queries/_form.html.erb @@ -22,12 +22,12 @@

<%= check_box_tag 'query_is_for_all', 1, @query.project.nil?, :class => (User.current.admin? ? '' : 'disable-unless-private') %>

-<% unless params[:gantt] %>
<%= l(:label_options) %>

<%= check_box_tag 'default_columns', 1, @query.has_default_columns?, :id => 'query_default_columns', :data => {:disables => "#columns, .block_columns input"} %>

+<% unless params[:gantt] %>

<%= select 'query', 'group_by', @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, :include_blank => true %>

@@ -36,17 +36,17 @@

<%= available_totalable_columns_tags(@query) %>

-
<% else %> -
<%= l(:label_options) %>

<%= hidden_field_tag 'query[draw_relations]', '0' %> <%= hidden_field_tag 'query[draw_progress_line]', '0' %> + <%= hidden_field_tag 'query[draw_selected_columns]', '0' %> +

-
<% end %> +
<%= l(:label_filter_plural) %> @@ -72,12 +72,10 @@
<% end %> -<% unless params[:gantt] %> <%= content_tag 'fieldset', :id => 'columns' do %> <%= l(:field_column_names) %> <%= render_query_columns_selection(query) %> <% end %> -<% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 44e451fd29..ec14ce1b8d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -981,6 +981,7 @@ en: label_cross_project_hierarchy: With project hierarchy label_cross_project_system: With all projects label_gantt_progress_line: Progress line + label_gantt_selected_columns: Selected columns label_visibility_private: to me only label_visibility_roles: to these roles only label_visibility_public: to any users diff --git a/lib/redmine/helpers/gantt.rb b/lib/redmine/helpers/gantt.rb index ac2b34e3d0..64745ef033 100644 --- a/lib/redmine/helpers/gantt.rb +++ b/lib/redmine/helpers/gantt.rb @@ -34,6 +34,8 @@ module Redmine IssueRelation::TYPE_PRECEDES => { :landscape_margin => 20, :color => '#628FEA' } }.freeze + UNAVAILABLE_COLUMNS = [:id, :subject] + # Some utility methods for the PDF export # @private class PDF @@ -78,6 +80,7 @@ module Redmine @date_to = (@date_from >> @months) - 1 @subjects = +'' @lines = +'' + @columns ||= {} @number_of_rows = nil @truncated = false if options.has_key?(:max_rows) @@ -137,6 +140,12 @@ module Redmine @lines end + # Renders the selected column of the Gantt chart, the right side of subjects. + def selected_column_content(options={}) + render(options.merge(:only => :selected_columns)) unless @columns.has_key?(options[:column].name) + @columns[options[:column].name] + end + # Returns issues that will be rendered def issues @issues ||= @query.issues( @@ -198,8 +207,9 @@ module Redmine :indent_increment => 20, :render => :subject, :format => :html}.merge(options) indent = options[:indent] || 4 - @subjects = +'' unless options[:only] == :lines - @lines = +'' unless options[:only] == :subjects + @subjects = +'' unless options[:only] == :lines || options[:only] == :selected_columns + @lines = +'' unless options[:only] == :subjects || options[:only] == :selected_columns + @columns[options[:column].name] = +'' if options[:only] == :selected_columns && @columns.has_key?(options[:column]) == false @number_of_rows = 0 begin Project.project_tree(projects) do |project, level| @@ -209,8 +219,8 @@ module Redmine rescue MaxLinesLimitReached @truncated = true end - @subjects_rendered = true unless options[:only] == :lines - @lines_rendered = true unless options[:only] == :subjects + @subjects_rendered = true unless options[:only] == :lines || options[:only] == :selected_columns + @lines_rendered = true unless options[:only] == :subjects || options[:only] == :selected_columns render_end(options) end @@ -256,8 +266,9 @@ module Redmine def render_object_row(object, options) class_name = object.class.name.downcase - send("subject_for_#{class_name}", object, options) unless options[:only] == :lines - send("line_for_#{class_name}", object, options) unless options[:only] == :subjects + send("subject_for_#{class_name}", object, options) unless options[:only] == :lines || options[:only] == :selected_columns + send("line_for_#{class_name}", object, options) unless options[:only] == :subjects || options[:only] == :selected_columns + column_content_for_issue(object, options) if options[:only] == :selected_columns && options[:column].present? && object.is_a?(Issue) options[:top] += options[:top_increment] @number_of_rows += 1 if @max_rows && @number_of_rows >= @max_rows @@ -325,6 +336,17 @@ module Redmine end end + def column_content_for_issue(issue, options) + if options[:format] == :html + data_options = {} + data_options[:collapse_expand] = "issue-#{issue.id}" + style = "position: absolute;top: #{options[:top]}px; font-size: 0.8em;" + content = view.content_tag(:div, view.column_content(options[:column], issue), :style => style, :class => "issue_#{options[:column].name}", :id => "#{options[:column].name}_issue_#{issue.id}", :data => data_options) + @columns[options[:column].name] << content if @columns.has_key?(options[:column].name) + content + end + end + def subject(label, options, object=nil) send "#{options[:format]}_subject", options, label, object end diff --git a/public/javascripts/gantt.js b/public/javascripts/gantt.js index 0241e6f3ec..0e9b6d79a7 100644 --- a/public/javascripts/gantt.js +++ b/public/javascripts/gantt.js @@ -163,6 +163,41 @@ function drawGanttProgressLines() { } } +function drawSelectedColumns(){ + if ($("#draw_selected_columns").prop('checked')) { + if(isMobile()) { + $('td.gantt_selected_column').each(function(i) { + $(this).hide(); + }); + }else{ + $('.gantt_subjects_container').addClass('draw_selected_columns'); + $('td.gantt_selected_column').each(function() { + $(this).show(); + var column_name = $(this).attr('id'); + var alsoResizeElements = '.gantt_' + column_name + '_container, .gantt_' + column_name + '_container > .gantt_hdr'; + $(this).resizable({ + alsoResize: alsoResizeElements, + minWidth: 20, + handles: "e", + create: function() { + $(".ui-resizable-e").css("cursor","ew-resize"); + }, + stop: function () { + $(alsoResizeElements).width($(this).width()); + } + }).on('resize', function (e) { + e.stopPropagation(); + }); + }); + } + }else{ + $('td.gantt_selected_column').each(function (i) { + $(this).hide(); + $('.gantt_subjects_container').removeClass('draw_selected_columns'); + }); + } +} + function drawGanttHandler() { var folder = document.getElementById('gantt_draw_area'); if(draw_gantt != null) @@ -170,10 +205,12 @@ function drawGanttHandler() { else draw_gantt = Raphael(folder); setDrawArea(); + drawSelectedColumns(); if ($("#draw_progress_line").prop('checked')) try{drawGanttProgressLines();}catch(e){} if ($("#draw_relations").prop('checked')) drawRelations(); + $('#content').addClass('gantt_content'); } function resizableSubjectColumn(){ @@ -184,9 +221,11 @@ function resizableSubjectColumn(){ alsoResize: '.gantt_subjects_container, .gantt_subjects_container>.gantt_hdr, .project-name, .issue-subject, .version-name', minWidth: 100, handles: 'e', - containment: '#content', create: function( event, ui ) { $('.ui-resizable-e').css('cursor','ew-resize'); + }, + stop: function () { + $('.gantt_subjects_container, .gantt_subjects_container>.gantt_hdr').width($(this).width()); } }).on('resize', function (e) { e.stopPropagation(); @@ -224,8 +263,8 @@ ganttEntryClick = function(e){ var new_top_val = parseInt(el.css('top')) + total_height * (target_shown ? -1 : 1); el.css('top', new_top_val); - $('#gantt_area form > div[data-collapse-expand="' + json.obj_id + '"]').each(function(_, task){ - $(task).css('top', new_top_val); + $('#gantt_area form > div[data-collapse-expand="' + json.obj_id + '"], td.gantt_selected_column div[data-collapse-expand="' + json.obj_id + '"]').each(function(_, el){ + $(el).css('top', new_top_val); }); return true; } @@ -237,13 +276,20 @@ ganttEntryClick = function(e){ total_height = 0; } if(is_shown == target_shown){ - $('#gantt_area form > div[data-collapse-expand="' + json.obj_id + '"]').each(function(_, task){ + $('#gantt_area form > div[data-collapse-expand="' + json.obj_id + '"]').each(function(_, task) { var el_task = $(task); if(!is_shown) el_task.css('top', target_top + total_height); if(!el_task.hasClass('tooltip')) el_task.toggle(!is_shown); }); + $('td.gantt_selected_column div[data-collapse-expand="' + json.obj_id + '"]' + ).each(function (_, attr) { + var el_attr = $(attr); + if (!is_shown) + el_attr.css('top', target_top + total_height); + el_attr.toggle(!is_shown); + }); if(!is_shown) el.css('top', target_top + total_height); iconChange(el); @@ -253,3 +299,9 @@ ganttEntryClick = function(e){ }); drawGanttHandler(); }; + +function disable_unavailable_columns(unavailable_columns) { + $.each(unavailable_columns, function (index, value) { + $('#available_c, #selected_c').children("[value='" + value + "']").prop('disabled', true); + }); +} diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 09bcd5b7e7..20af147829 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -1238,26 +1238,69 @@ div.wiki img {vertical-align:middle; max-width:100%;} #my-page .list th.checkbox, #my-page .list td.checkbox {display:none;} /***** Gantt chart *****/ +div.gantt_content { + overflow: scroll; +} +table.gantt-table { + width: 100%; + border-collapse: collapse; +} +table.gantt-table td { + padding: 0px; +} .gantt_hdr { position:absolute; top:0; height:16px; border-top: 1px solid #c0c0c0; border-bottom: 1px solid #c0c0c0; - border-right: 1px solid #c0c0c0; + border-left: 1px solid #c0c0c0; text-align: center; overflow: hidden; } +#gantt_area .gantt_hdr { + border-left: 0px; + border-right: 1px solid #c0c0c0; +} +.gantt_subjects_container:not(.draw_selected_columns) .gantt_hdr, +.last_gantt_selected_column .gantt_hdr { + z-index: 10; + border-right: 1px solid #c0c0c0; +} +.gantt_subjects_container .gantt_subjects * { + z-index: 10; +} .gantt_hdr.nwday {background-color:#f1f1f1; color:#999;} - -.gantt_subjects { font-size: 0.8em; } -.gantt_subjects div { line-height:16px;height:16px;overflow:hidden;white-space:nowrap;text-overflow: ellipsis; } +.gantt_subjects, .gantt_selected_column_content.gantt_hdr { font-size: 0.8em; } +.gantt_subjects div, .gantt_selected_column_content div { + line-height: 16px; + height: 16px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden !important; + width: 100%; +} .gantt_subjects div.issue-subject:hover { background-color:#ffffdd; } - +.gantt_selected_column_content { padding-left: 3px; padding-right: 3px;} .gantt_subjects .issue-subject img.icon-gravatar { margin: 2px 5px 0px 2px; } +.gantt_hdr_selected_column_name { + position: absolute; + top: 50%; + width:100%; + transform: translateY(-50%); + -webkit- transform: translateY(-50%); + font-size: 0.8em; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + +} +td.gantt_selected_column, td.gantt_selected_column .gantt_hdr,.gantt_selected_column_container { + width: 50px; +} .task { position: absolute; diff --git a/public/stylesheets/responsive.css b/public/stylesheets/responsive.css index a2193864cc..6bec535605 100644 --- a/public/stylesheets/responsive.css +++ b/public/stylesheets/responsive.css @@ -711,7 +711,6 @@ .gantt_subjects_column .gantt_hdr { width: 100% !important; - border-right: 1px solid #c0c0c0; /* [2] */ right: 0 !important; /* [2] */ } diff --git a/test/functional/queries_controller_test.rb b/test/functional/queries_controller_test.rb index 22fb7bae9b..8654c5d2ea 100644 --- a/test/functional/queries_controller_test.rb +++ b/test/functional/queries_controller_test.rb @@ -295,7 +295,8 @@ class QueriesControllerTest < Redmine::ControllerTest :query => { :name => "test_create_from_gantt", :draw_relations => '1', - :draw_progress_line => '1' + :draw_progress_line => '1', + :draw_selected_columns => '1' } } assert_response 302 @@ -304,6 +305,7 @@ class QueriesControllerTest < Redmine::ControllerTest assert_redirected_to "/issues/gantt?query_id=#{query.id}" assert_equal true, query.draw_relations assert_equal true, query.draw_progress_line + assert_equal true, query.draw_selected_columns end def test_create_project_query_from_gantt @@ -321,7 +323,8 @@ class QueriesControllerTest < Redmine::ControllerTest :query => { :name => "test_create_from_gantt", :draw_relations => '0', - :draw_progress_line => '0' + :draw_progress_line => '0', + :draw_selected_columns => '0' } } assert_response 302 @@ -330,6 +333,7 @@ class QueriesControllerTest < Redmine::ControllerTest assert_redirected_to "/projects/ecookbook/issues/gantt?query_id=#{query.id}" assert_equal false, query.draw_relations assert_equal false, query.draw_progress_line + assert_equal false, query.draw_selected_columns end def test_create_project_public_query_should_force_private_without_manage_public_queries_permission diff --git a/test/unit/lib/redmine/helpers/gantt_test.rb b/test/unit/lib/redmine/helpers/gantt_test.rb index 36a9fb54e0..7e409c4dfa 100644 --- a/test/unit/lib/redmine/helpers/gantt_test.rb +++ b/test/unit/lib/redmine/helpers/gantt_test.rb @@ -25,6 +25,7 @@ class Redmine::Helpers::GanttHelperTest < Redmine::HelperTest include ProjectsHelper include IssuesHelper + include QueriesHelper include ERB::Util include Rails.application.routes.url_helpers @@ -241,6 +242,17 @@ class Redmine::Helpers::GanttHelperTest < Redmine::HelperTest assert_select "div.tooltip", /#{@issue.subject}/ end + test "#selected_column_content" do + create_gantt + issue = Issue.generate! + @gantt.query.column_names = [:assigned_to] + issue.update(:assigned_to_id => issue.assignable_users.first.id) + @project.issues << issue + # :column => assigned_to + @output_buffer = @gantt.selected_column_content({ :column => @gantt.query.columns.last }) + assert_select "div.issue_assigned_to#assigned_to_issue_#{issue.id}" + end + test "#subject_for_project" do create_gantt @output_buffer = @gantt.subject_for_project(@project, :format => :html) @@ -437,6 +449,20 @@ class Redmine::Helpers::GanttHelperTest < Redmine::HelperTest assert_select "div.label", :text => 'line' end + test "#column_content_for_issue" do + create_gantt + @gantt.query.column_names = [:assigned_to] + issue = Issue.generate! + issue.update(:assigned_to_id => issue.assignable_users.first.id) + @project.issues << issue + # :column => assigned_to + options = { :column => @gantt.query.columns.last, :top => 64, :format => :html } + @output_buffer = @gantt.column_content_for_issue(issue, options) + + assert_select "div.issue_assigned_to#assigned_to_issue_#{issue.id}" + assert_includes @output_buffer, column_content(options[:column], issue) + end + def test_sort_issues_no_date project = Project.generate! issue1 = Issue.generate!(:subject => "test", :project => project)