]> source.dussan.org Git - redmine.git/commitdiff
Allow collapse/expand in gantt chart (#6417).
authorGo MAEDA <maeda@farend.jp>
Tue, 5 Mar 2019 14:52:09 +0000 (14:52 +0000)
committerGo MAEDA <maeda@farend.jp>
Tue, 5 Mar 2019 14:52:09 +0000 (14:52 +0000)
Patch by Yuichi HARADA.

git-svn-id: http://svn.redmine.org/redmine/trunk@17929 e93f8b46-1217-0410-a6f0-8f06a7374b81

app/views/gantts/show.html.erb
lib/redmine/helpers/gantt.rb
public/javascripts/gantt.js
public/stylesheets/application.css
test/unit/lib/redmine/helpers/gantt_test.rb

index 90f6a85fa65db9b9f47cbab4a75aabb3a57c55ee..a51abdcde436e9222b2a90fed13dc968a7fb1694 100644 (file)
     resizableSubjectColumn();
     $("#draw_relations").change(drawGanttHandler);
     $("#draw_progress_line").change(drawGanttHandler);
+    $('div.gantt_subjects .expander').on('click', ganttEntryClick);
   });
   $(window).resize(function() {
     drawGanttHandler();
index 312a3133a42c755d87060e872aacf3d22715ab84..d5f85ee6fe74e8360b973d75817866ca088092ab 100644 (file)
@@ -698,21 +698,43 @@ module Redmine
       end
 
       def html_subject(params, subject, object)
-        style = "position: absolute;top:#{params[:top]}px;left:#{params[:indent]}px;"
-        style << "width:#{params[:subject_width] - params[:indent]}px;" if params[:subject_width]
         content = html_subject_content(object) || subject
-        tag_options = {:style => style}
+        tag_options = {}
         case object
         when Issue
           tag_options[:id] = "issue-#{object.id}"
           tag_options[:class] = "issue-subject hascontextmenu"
           tag_options[:title] = object.subject
+          children = object.children & project_issues(object.project)
+          has_children = children.present? && (children.collect(&:fixed_version).uniq & [object.fixed_version]).present?
         when Version
           tag_options[:id] = "version-#{object.id}"
           tag_options[:class] = "version-name"
+          has_children = object.fixed_issues.exists?
         when Project
           tag_options[:class] = "project-name"
+          has_children = object.issues.exists? || object.versions.exists?
+        end
+        if object
+          tag_options[:data] = {
+            :collapse_expand => {
+              :top_increment => params[:top_increment],
+              :obj_id => "#{object.class}-#{object.id}".downcase,
+            },
+          }
+        end
+        if has_children
+          content = view.content_tag(:span, nil, :class => :expander) + content
+          tag_options[:class] << ' open'
+        else
+          if params[:indent]
+            params = params.dup
+            params[:indent] += 12
+          end
         end
+        style = "position: absolute;top:#{params[:top]}px;left:#{params[:indent]}px;"
+        style << "width:#{params[:subject_width] - params[:indent]}px;" if params[:subject_width]
+        tag_options[:style] = style
         output = view.content_tag(:div, content, tag_options)
         @subjects << output
         output
@@ -751,6 +773,8 @@ module Redmine
 
       def html_task(params, coords, markers, label, object)
         output = ''
+        data_options = {}
+        data_options[:collapse_expand] = "#{object.class}-#{object.id}".downcase if object
 
         css = "task " + case object
           when Project
@@ -774,13 +798,15 @@ module Redmine
           html_id = "task-todo-version-#{object.id}" if object.is_a?(Version)
           content_opt = {:style => style,
                          :class => "#{css} task_todo",
-                         :id => html_id}
+                         :id => html_id,
+                         :data => {}}
           if object.is_a?(Issue)
             rels = issue_relations(object)
             if rels.present?
               content_opt[:data] = {"rels" => rels.to_json}
             end
           end
+          content_opt[:data].merge!(data_options)
           output << view.content_tag(:div, '&nbsp;'.html_safe, content_opt)
           if coords[:bar_late_end]
             width = coords[:bar_late_end] - coords[:bar_start] - 2
@@ -790,7 +816,8 @@ module Redmine
             style << "width:#{width}px;"
             output << view.content_tag(:div, '&nbsp;'.html_safe,
                                        :style => style,
-                                       :class => "#{css} task_late")
+                                       :class => "#{css} task_late",
+                                       :data => data_options)
           end
           if coords[:bar_progress_end]
             width = coords[:bar_progress_end] - coords[:bar_start] - 2
@@ -803,7 +830,8 @@ module Redmine
             output << view.content_tag(:div, '&nbsp;'.html_safe,
                                        :style => style,
                                        :class => "#{css} task_done",
-                                       :id => html_id)
+                                       :id => html_id,
+                                       :data => data_options)
           end
         end
         # Renders the markers
@@ -815,7 +843,8 @@ module Redmine
             style << "width:15px;"
             output << view.content_tag(:div, '&nbsp;'.html_safe,
                                        :style => style,
-                                       :class => "#{css} marker starting")
+                                       :class => "#{css} marker starting",
+                                       :data => data_options)
           end
           if coords[:end]
             style = ""
@@ -824,7 +853,8 @@ module Redmine
             style << "width:15px;"
             output << view.content_tag(:div, '&nbsp;'.html_safe,
                                        :style => style,
-                                       :class => "#{css} marker ending")
+                                       :class => "#{css} marker ending",
+                                       :data => data_options)
           end
         end
         # Renders the label on the right
@@ -835,7 +865,8 @@ module Redmine
           style << "width:15px;"
           output << view.content_tag(:div, label,
                                      :style => style,
-                                     :class => "#{css} label")
+                                     :class => "#{css} label",
+                                     :data => data_options)
         end
         # Renders the tooltip
         if object.is_a?(Issue) && coords[:bar_start] && coords[:bar_end]
@@ -851,7 +882,8 @@ module Redmine
           style << "height:12px;"
           output << view.content_tag(:div, s.html_safe,
                                      :style => style,
-                                     :class => "tooltip hascontextmenu")
+                                     :class => "tooltip hascontextmenu",
+                                     :data => data_options)
         end
         @lines << output
         output
index 2e71178b0f494c8cd9b9a45dc3ef204d5b1ec019..0241e6f3ec375865c439496bb697a942811461d8 100644 (file)
@@ -17,6 +17,7 @@ function setDrawArea() {
 function getRelationsArray() {
   var arr = new Array();
   $.each($('div.task_todo[data-rels]'), function(index_div, element) {
+    if(!$(element).is(':visible')) return true;
     var element_id = $(element).attr("id");
     if (element_id != null) {
       var issue_id = element_id.replace("task-todo-issue-", "");
@@ -106,6 +107,7 @@ function getProgressLinesArray() {
   var today_left = $('#today_line').position().left;
   arr.push({left: today_left, top: 0});
   $.each($('div.issue-subject, div.version-name'), function(index, element) {
+    if(!$(element).is(':visible')) return true;
     var t = $(element).position().top - draw_top ;
     var h = ($(element).height() / 9);
     var element_top_upper  = t - h;
@@ -169,7 +171,7 @@ function drawGanttHandler() {
     draw_gantt = Raphael(folder);
   setDrawArea();
   if ($("#draw_progress_line").prop('checked'))
-    drawGanttProgressLines();
+    try{drawGanttProgressLines();}catch(e){}
   if ($("#draw_relations").prop('checked'))
     drawRelations();
 }
@@ -195,3 +197,59 @@ function resizableSubjectColumn(){
     $('td.gantt_subjects_column').resizable('enable');
   };
 }
+
+ganttEntryClick = function(e){
+  var subject = $(e.target.parentElement);
+  var subject_left = parseInt(subject.css('left'));
+  var target_shown = null;
+  var target_top = 0;
+  var total_height = 0;
+  var out_of_hierarchy = false;
+  var iconChange = null;
+  if(subject.hasClass('open'))
+    iconChange = function(element){
+      $(element).removeClass('open');
+    };
+  else
+    iconChange = function(element){
+      $(element).addClass('open');
+    };
+  iconChange(subject);
+  subject.nextAll('div').each(function(_, element){
+    var el = $(element);
+    var json = el.data('collapse-expand');
+    if(out_of_hierarchy || parseInt(el.css('left')) <= subject_left){
+      out_of_hierarchy = true;
+      if(target_shown == null) return false;
+
+      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);
+      });
+      return true;
+    }
+
+    var is_shown = el.is(':visible');
+    if(target_shown == null){
+      target_shown = is_shown;
+      target_top = parseInt(el.css('top'));
+      total_height = 0;
+    }
+    if(is_shown == target_shown){
+      $('#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);
+      });
+      if(!is_shown)
+        el.css('top', target_top + total_height);
+      iconChange(el);
+      el.toggle(!is_shown);
+      total_height += parseInt(json.top_increment);
+    }
+  });
+  drawGanttHandler();
+};
index 3febc999a2969c92b84fa678d7118357ff5e5cbe..7dde1200796d9e5484494bf6cca3a2510adf5176 100644 (file)
@@ -291,8 +291,10 @@ tr.entry td.age { text-align: right; }
 tr.entry.file td.filename a { margin-left: 16px; }
 tr.entry.file td.filename_no_report a { margin-left: 16px; }
 
-tr span.expander {background: url(../images/arrow_right.png) no-repeat 2px 50%; padding-left: 8px; margin-left: 0; cursor: pointer;}
-tr.open span.expander {background-image: url(../images/arrow_down.png);}
+tr span.expander, .gantt_subjects div > span.expander {background: url(../images/arrow_right.png) no-repeat 2px 50%; padding-left: 8px; margin-left: 0; cursor: pointer;}
+tr.open span.expander, .gantt_subjects div.open > span.expander {background-image: url(../images/arrow_down.png);}
+.gantt_subjects div > span.expander {padding-left: 12px;}
+.gantt_subjects div > span .icon-gravatar {float: none;}
 
 tr.changeset { height: 20px }
 tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; }
index 351f9c03764ca5d665de65bdec9b51b748f3123e..0f4ca4e902b71f9d3677b2e12d9bf5301b505d9b 100644 (file)
@@ -152,7 +152,8 @@ class Redmine::Helpers::GanttHelperTest < Redmine::HelperTest
     setup_subjects
     @output_buffer = @gantt.subjects
     assert_select "div.issue-subject", /#{@issue.subject}/
-    assert_select 'div.issue-subject[style*="left:44px"]'
+    # subject 56px: 44px + 12px(collapse/expand icon's width)
+    assert_select 'div.issue-subject[style*="left:56px"]'
   end
 
   test "#subjects issue assigned to a shared version of another project should be rendered" do
@@ -200,9 +201,10 @@ class Redmine::Helpers::GanttHelperTest < Redmine::HelperTest
     assert_select 'div.issue-subject[style*="left:44px"]', /#{@issue.subject}/
     # children 64px
     assert_select 'div.issue-subject[style*="left:64px"]', /child1/
-    assert_select 'div.issue-subject[style*="left:64px"]', /child2/
-    # grandchild 84px
-    assert_select 'div.issue-subject[style*="left:84px"]', /grandchild/, @output_buffer
+    # children 76px: 64px + 12px(collapse/expand icon's width)
+    assert_select 'div.issue-subject[style*="left:76px"]', /child2/
+    # grandchild 96px: 84px + 12px(collapse/expand icon's width)
+    assert_select 'div.issue-subject[style*="left:96px"]', /grandchild/, @output_buffer
   end
 
   test "#lines" do
@@ -298,7 +300,8 @@ class Redmine::Helpers::GanttHelperTest < Redmine::HelperTest
   test "#subject should use the indent option to move the div to the right" do
     create_gantt
     @output_buffer = @gantt.subject('subject', :format => :html, :indent => 40)
-    assert_select 'div[style*="left:40"]'
+    # subject 52px: 40px(indent) + 12px(collapse/expand icon's width)
+    assert_select 'div[style*="left:52px"]'
   end
 
   test "#line_for_project" do