Feature #2024 » 0001-Moving-gantt-bar.patch
| app/controllers/gantts_controller.rb | ||
|---|---|---|
| 19 | 19 | |
| 20 | 20 | class GanttsController < ApplicationController | 
| 21 | 21 | menu_item :gantt | 
| 22 | before_action :find_optional_project | |
| 22 |   before_action :find_optional_project, :only => [:show] | |
| 23 | 23 | |
| 24 | 24 | rescue_from Query::StatementInvalid, :with => :query_statement_invalid | 
| 25 | 25 | |
| ... | ... | |
| 55 | 55 | end | 
| 56 | 56 | end | 
| 57 | 57 | end | 
| 58 | ||
| 59 | def change_duration | |
| 60 | return render_error(:status => :unprocessable_entity) unless request.xhr? | |
| 61 | ||
| 62 | @obj = Issue.find(params[:id]) | |
| 63 | raise Unauthorized unless @obj.visible? | |
| 64 | ||
| 65 | ActiveRecord::Base.transaction do | |
| 66 | @obj.init_journal(User.current) | |
| 67 | @obj.safe_attributes = params[:change_duration] | |
| 68 | if !@obj.save | |
| 69 | render_403(:message => @obj.errors.full_messages.join) | |
| 70 | raise ActiveRecord::Rollback | |
| 71 | end | |
| 72 | end | |
| 73 | retrieve_query | |
| 74 | rescue ActiveRecord::StaleObjectError | |
| 75 | render_403(:message => :notice_issue_update_conflict) | |
| 76 | rescue ActiveRecord::RecordNotFound | |
| 77 | render_404 | |
| 78 | end | |
| 58 | 79 | end | 
| app/views/gantts/change_duration.js.erb | ||
|---|---|---|
| 1 | <% | |
| 2 | @draw_objs = [] | |
| 3 | ||
| 4 | def select_precedes(issue) | |
| 5 | issue.relations_from.where(:relation_type => IssueRelation::TYPE_PRECEDES).map(&:issue_to).each do |follows| | |
| 6 | next if @draw_objs.include?(follows) | |
| 7 | ||
| 8 | while follows do | |
| 9 | @draw_objs.concat [follows, follows.fixed_version, follows.project] | |
| 10 | select_precedes(follows) | |
| 11 | follows.children.each do |child| | |
| 12 | @draw_objs.concat [child, child.fixed_version, child.project] | |
| 13 | select_precedes(child) | |
| 14 | end | |
| 15 | follows = follows.parent | |
| 16 | end | |
| 17 | end | |
| 18 | end | |
| 19 | ||
| 20 | issue = @obj | |
| 21 | while issue do | |
| 22 | @draw_objs.concat [issue, issue.fixed_version, issue.project] | |
| 23 | select_precedes(issue) | |
| 24 | issue = issue.parent | |
| 25 | end | |
| 26 | @draw_objs = @draw_objs.compact.uniq | |
| 27 | @draw_objs.reject!{|obj| ![Project, Version, Issue].include?(obj.class)} | |
| 28 | -%> | |
| 29 | var elm; | |
| 30 | <% | |
| 31 | gantt = Redmine::Helpers::Gantt.new(params) | |
| 32 | gantt.view = self | |
| 33 | gantt.query = @query | |
| 34 | ||
| 35 | @draw_objs.each do |obj| | |
| 36 | gantt.instance_variable_set(:@number_of_rows, 0) | |
| 37 | gantt.instance_variable_set(:@lines, '') | |
| 38 | gantt.render_object_row( | |
| 39 | obj, | |
| 40 |     {format: :html, only: :lines, zoom: 2 ** gantt.zoom, top: 0, top_increment: 20} | |
| 41 | ) | |
| 42 | todo_content = Nokogiri::HTML.parse(gantt.instance_variable_get(:@lines)) | |
| 43 | todo_content = todo_content.xpath( | |
| 44 | "//div[contains(@class,'task') and contains(@class,'line')]/*" | |
| 45 |   ).to_s.tr("\n",'').gsub(/'/, "\\\\'") | |
| 46 | ||
| 47 | klass_name = obj.class.name.underscore | |
| 48 |   elm_todo = "[id=task-todo-#{klass_name}-#{obj.id}]" | |
| 49 | css_subject = 'span:not(.expander)' | |
| 50 |   elm_subject = raw("[id=#{klass_name}-#{obj.id}] > #{css_subject}") | |
| 51 | ||
| 52 | subject_content = Nokogiri::HTML.parse(gantt.__send__(:html_subject_content, obj)) | |
| 53 |   subject_content = subject_content.css(css_subject).to_s.tr("\n",'').gsub(/'/, "\\\\'") | |
| 54 | -%> | |
| 55 | if($('<%= elm_subject %>').length){ | |
| 56 |   $('<%= elm_todo %>').parent().html('<%= raw(todo_content) %>'); | |
| 57 |   $('<%= elm_subject %>').replaceWith('<%= raw(subject_content) %>'); | |
| 58 | <% | |
| 59 | case obj | |
| 60 | when Issue | |
| 61 | @query.columns.each do |column| | |
| 62 | -%> | |
| 63 |   elm = $('div.gantt_selected_column_content #<%= column.name %>_issue_<%= obj.id %>'); | |
| 64 |   if(elm.length){ | |
| 65 |     elm.html('<%= escape_javascript(column_content(column, obj)) %>'); | |
| 66 | } | |
| 67 | <% | |
| 68 | end | |
| 69 | end | |
| 70 | -%> | |
| 71 | } | |
| 72 | <% | |
| 73 | end | |
| 74 | -%> | |
| config/routes.rb | ||
|---|---|---|
| 60 | 60 | |
| 61 | 61 | get '/projects/:project_id/issues/gantt', :to => 'gantts#show', :as => 'project_gantt' | 
| 62 | 62 | get '/issues/gantt', :to => 'gantts#show' | 
| 63 | put '/gantt/:id/change_duration', :to => 'gantts#change_duration', :as => 'gantt_change_duration' | |
| 63 | 64 | |
| 64 | 65 | get '/projects/:project_id/issues/calendar', :to => 'calendars#show', :as => 'project_calendar' | 
| 65 | 66 | get '/issues/calendar', :to => 'calendars#show' | 
| lib/redmine/helpers/gantt.rb | ||
|---|---|---|
| 771 | 771 | tag_options[:class] = "version-name" | 
| 772 | 772 | has_children = object.fixed_issues.exists? | 
| 773 | 773 | when Project | 
| 774 |           tag_options[:id] = "project-#{object.id}" | |
| 774 | 775 | tag_options[:class] = "project-name" | 
| 775 | 776 | has_children = object.issues.exists? || object.versions.exists? | 
| 776 | 777 | end | 
| ... | ... | |
| 849 | 850 | end | 
| 850 | 851 | # Renders the task bar, with progress and late | 
| 851 | 852 | if coords[:bar_start] && coords[:bar_end] | 
| 852 |           width = coords[:bar_end] - coords[:bar_start] - 2 | |
| 853 | width = coords[:bar_end] - coords[:bar_start] | |
| 853 | 854 | style = +"" | 
| 854 |           style << "top:#{params[:top]}px;" | |
| 855 | 855 |           style << "left:#{coords[:bar_start]}px;" | 
| 856 | 856 |           style << "width:#{width}px;" | 
| 857 |           html_id = "task-todo-issue-#{object.id}" if object.is_a?(Issue) | |
| 858 |           html_id = "task-todo-version-#{object.id}" if object.is_a?(Version) | |
| 857 | html_id = | |
| 858 | case object | |
| 859 | when Project | |
| 860 |               "task-todo-project-#{object.id}" | |
| 861 | when Version | |
| 862 |               "task-todo-version-#{object.id}" | |
| 863 | when Issue | |
| 864 |               "task-todo-issue-#{object.id}" | |
| 865 | end | |
| 859 | 866 |           content_opt = {:style => style, | 
| 860 |                          :class => "#{css} task_todo", | |
| 867 | :class => "task_todo", | |
| 861 | 868 | :id => html_id, | 
| 862 | 869 |                          :data => {}} | 
| 863 | 870 | if object.is_a?(Issue) | 
| ... | ... | |
| 865 | 872 | if rels.present? | 
| 866 | 873 |               content_opt[:data] = {"rels" => rels.to_json} | 
| 867 | 874 | end | 
| 875 |             content_opt[:data].merge!({ | |
| 876 | :url_change_duration => Rails.application.routes.url_helpers.gantt_change_duration_path( | |
| 877 | object | |
| 878 | ), | |
| 879 |               :object => { | |
| 880 | :start_date => object.start_date, | |
| 881 | :due_date => object.due_date, | |
| 882 | :lock_version => object.lock_version, | |
| 883 | }.to_json, | |
| 884 | }) | |
| 868 | 885 | end | 
| 869 | 886 | content_opt[:data].merge!(data_options) | 
| 870 |           output << view.content_tag(:div, ' '.html_safe, content_opt) | |
| 887 |           bar_contents = [] | |
| 871 | 888 | if coords[:bar_late_end] | 
| 872 |             width = coords[:bar_late_end] - coords[:bar_start] - 2 | |
| 889 | width = coords[:bar_late_end] - coords[:bar_start] | |
| 873 | 890 | style = +"" | 
| 874 |             style << "top:#{params[:top]}px;" | |
| 875 |             style << "left:#{coords[:bar_start]}px;" | |
| 876 | 891 |             style << "width:#{width}px;" | 
| 877 |             output << view.content_tag(:div, ' '.html_safe, | |
| 878 | :style => style, | |
| 879 |                                        :class => "#{css} task_late", | |
| 880 | :data => data_options) | |
| 892 |             bar_contents << view.content_tag(:div, ' '.html_safe, | |
| 893 |                                              :style => style, | |
| 894 |                                              :class => "task_late", | |
| 895 |                                              :data => data_options) | |
| 881 | 896 | end | 
| 882 | 897 | if coords[:bar_progress_end] | 
| 883 |             width = coords[:bar_progress_end] - coords[:bar_start] - 2 | |
| 898 | width = coords[:bar_progress_end] - coords[:bar_start] | |
| 884 | 899 | style = +"" | 
| 885 |             style << "top:#{params[:top]}px;" | |
| 886 |             style << "left:#{coords[:bar_start]}px;" | |
| 887 | 900 |             style << "width:#{width}px;" | 
| 888 | 901 |             html_id = "task-done-issue-#{object.id}" if object.is_a?(Issue) | 
| 889 | 902 |             html_id = "task-done-version-#{object.id}" if object.is_a?(Version) | 
| 890 | output << view.content_tag(:div, ' '.html_safe, | |
| 903 | bar_contents << view.content_tag(:div, ' '.html_safe, | |
| 904 | :style => style, | |
| 905 | :class => "task_done", | |
| 906 | :id => html_id, | |
| 907 | :data => data_options) | |
| 908 | end | |
| 909 | ||
| 910 | # Renders the tooltip | |
| 911 | if object.is_a?(Issue) | |
| 912 | s = view.content_tag(:span, | |
| 913 | view.render_issue_tooltip(object).html_safe, | |
| 914 | :class => "tip") | |
| 915 | s += view.content_tag(:input, nil, :type => 'checkbox', :name => 'ids[]', :value => object.id, :style => 'display:none;', :class => 'toggle-selection') | |
| 916 | style = +"" | |
| 917 |             style << "width:#{coords[:bar_end] - coords[:bar_start]}px;" | |
| 918 | style << "height:12px;" | |
| 919 | bar_contents << view.content_tag(:div, s.html_safe, | |
| 920 | :style => style, | |
| 921 | :class => "tooltip hascontextmenu", | |
| 922 | :data => data_options) | |
| 923 | end | |
| 924 | ||
| 925 | # Renders the label on the right | |
| 926 | if label | |
| 927 | style = +"" | |
| 928 | style << "top:0px;" | |
| 929 |             style << "left:#{coords[:bar_end] - coords[:bar_start] + 8}px;" | |
| 930 | bar_contents << view.content_tag(:div, label, | |
| 931 | :style => style, | |
| 932 | :class => "label", | |
| 933 | :data => data_options) | |
| 934 | end | |
| 935 | ||
| 936 | bar_contents = bar_contents.join.presence | |
| 937 | output << view.content_tag(:div, (bar_contents || ' ').html_safe, content_opt) | |
| 938 | else | |
| 939 | # Renders the label on the right | |
| 940 | if label | |
| 941 | style = +"" | |
| 942 | style << "top:1px;" | |
| 943 |             style << "left:#{(coords[:bar_end] || 0) + 8}px;" | |
| 944 | output << view.content_tag(:div, label, | |
| 891 | 945 | :style => style, | 
| 892 |                                        :class => "#{css} task_done", | |
| 893 | :id => html_id, | |
| 946 | :class => "label", | |
| 894 | 947 | :data => data_options) | 
| 895 | 948 | end | 
| 896 | 949 | end | 
| ... | ... | |
| 898 | 951 | if markers | 
| 899 | 952 | if coords[:start] | 
| 900 | 953 | style = +"" | 
| 901 |             style << "top:#{params[:top]}px;" | |
| 902 | 954 |             style << "left:#{coords[:start]}px;" | 
| 903 | style << "width:15px;" | |
| 904 | 955 | output << view.content_tag(:div, ' '.html_safe, | 
| 905 | 956 | :style => style, | 
| 906 |                                        :class => "#{css} marker starting", | |
| 957 | :class => "marker starting", | |
| 907 | 958 | :data => data_options) | 
| 908 | 959 | end | 
| 909 | 960 | if coords[:end] | 
| 910 | 961 | style = +"" | 
| 911 |             style << "top:#{params[:top]}px;" | |
| 912 | 962 |             style << "left:#{coords[:end]}px;" | 
| 913 | style << "width:15px;" | |
| 914 | 963 | output << view.content_tag(:div, ' '.html_safe, | 
| 915 | 964 | :style => style, | 
| 916 |                                        :class => "#{css} marker ending", | |
| 965 | :class => "marker ending", | |
| 917 | 966 | :data => data_options) | 
| 918 | 967 | end | 
| 919 | 968 | end | 
| 920 | # Renders the label on the right | |
| 921 | if label | |
| 922 | style = +"" | |
| 923 |           style << "top:#{params[:top]}px;" | |
| 924 |           style << "left:#{(coords[:bar_end] || 0) + 8}px;" | |
| 925 | style << "width:15px;" | |
| 926 | output << view.content_tag(:div, label, | |
| 927 | :style => style, | |
| 928 |                                      :class => "#{css} label", | |
| 929 | :data => data_options) | |
| 930 | end | |
| 931 | # Renders the tooltip | |
| 932 | if object.is_a?(Issue) && coords[:bar_start] && coords[:bar_end] | |
| 933 | s = view.content_tag(:span, | |
| 934 | view.render_issue_tooltip(object).html_safe, | |
| 935 | :class => "tip") | |
| 936 | s += view.content_tag(:input, nil, :type => 'checkbox', :name => 'ids[]', | |
| 937 | :value => object.id, :style => 'display:none;', | |
| 938 | :class => 'toggle-selection') | |
| 939 | style = +"" | |
| 940 | style << "position: absolute;" | |
| 941 |           style << "top:#{params[:top]}px;" | |
| 942 |           style << "left:#{coords[:bar_start]}px;" | |
| 943 |           style << "width:#{coords[:bar_end] - coords[:bar_start]}px;" | |
| 944 | style << "height:12px;" | |
| 945 | output << view.content_tag(:div, s.html_safe, | |
| 946 | :style => style, | |
| 947 | :class => "tooltip hascontextmenu", | |
| 948 | :data => data_options) | |
| 949 | end | |
| 969 | output = view.content_tag(:div, output.html_safe, | |
| 970 |           :class => "#{css} line", | |
| 971 |           :style => "top:#{params[:top]}px;width:#{params[:g_width] - 1}px;", | |
| 972 | :data => data_options | |
| 973 | ) | |
| 950 | 974 | @lines << output | 
| 951 | 975 | output | 
| 952 | 976 | end | 
| public/javascripts/gantt.js | ||
|---|---|---|
| 4 | 4 | var draw_gantt = null; | 
| 5 | 5 | var draw_top; | 
| 6 | 6 | var draw_right; | 
| 7 | var draw_left; | |
| 8 | 7 | |
| 9 | 8 | var rels_stroke_width = 2; | 
| 10 | 9 | |
| 11 | 10 | function setDrawArea() { | 
| 12 |   draw_top   = $("#gantt_draw_area").position().top; | |
| 11 |   draw_top   = $("#gantt_draw_area").offset().top; | |
| 13 | 12 |   draw_right = $("#gantt_draw_area").width(); | 
| 14 |   draw_left  = $("#gantt_area").scrollLeft(); | |
| 15 | 13 | } | 
| 16 | 14 | |
| 17 | 15 | function getRelationsArray() { | 
| ... | ... | |
| 42 | 40 | return; | 
| 43 | 41 | } | 
| 44 | 42 | var issue_height = issue_from.height(); | 
| 45 |     var issue_from_top   = issue_from.position().top  + (issue_height / 2) - draw_top; | |
| 43 |     var issue_from_top   = issue_from.offset().top  + (issue_height / 2) - draw_top; | |
| 46 | 44 | var issue_from_right = issue_from.position().left + issue_from.width(); | 
| 47 |     var issue_to_top   = issue_to.position().top  + (issue_height / 2) - draw_top; | |
| 45 |     var issue_to_top   = issue_to.offset().top  + (issue_height / 2) - draw_top; | |
| 48 | 46 | var issue_to_left = issue_to.position().left; | 
| 49 | 47 | var color = issue_relation_type[element_issue["rel_type"]]["color"]; | 
| 50 | 48 | var landscape_margin = issue_relation_type[element_issue["rel_type"]]["landscape_margin"]; | 
| 51 | 49 | var issue_from_right_rel = issue_from_right + landscape_margin; | 
| 52 | 50 | var issue_to_left_rel = issue_to_left - landscape_margin; | 
| 53 |     draw_gantt.path(["M", issue_from_right + draw_left,     issue_from_top, | |
| 54 |                      "L", issue_from_right_rel + draw_left, issue_from_top]) | |
| 51 | draw_gantt.path(["M", issue_from_right, issue_from_top, | |
| 52 | "L", issue_from_right_rel, issue_from_top]) | |
| 55 | 53 |                    .attr({stroke: color, | 
| 56 | 54 | "stroke-width": rels_stroke_width | 
| 57 | 55 | }); | 
| 58 | 56 |     if (issue_from_right_rel < issue_to_left_rel) { | 
| 59 |       draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_from_top, | |
| 60 |                        "L", issue_from_right_rel + draw_left, issue_to_top]) | |
| 57 | draw_gantt.path(["M", issue_from_right_rel, issue_from_top, | |
| 58 | "L", issue_from_right_rel, issue_to_top]) | |
| 61 | 59 |                      .attr({stroke: color, | 
| 62 | 60 | "stroke-width": rels_stroke_width | 
| 63 | 61 | }); | 
| 64 |       draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_to_top, | |
| 65 |                        "L", issue_to_left + draw_left,        issue_to_top]) | |
| 62 | draw_gantt.path(["M", issue_from_right_rel, issue_to_top, | |
| 63 | "L", issue_to_left, issue_to_top]) | |
| 66 | 64 |                      .attr({stroke: color, | 
| 67 | 65 | "stroke-width": rels_stroke_width | 
| 68 | 66 | }); | 
| ... | ... | |
| 70 | 68 | var issue_middle_top = issue_to_top + | 
| 71 | 69 | (issue_height * | 
| 72 | 70 | ((issue_from_top > issue_to_top) ? 1 : -1)); | 
| 73 |       draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_from_top, | |
| 74 |                        "L", issue_from_right_rel + draw_left, issue_middle_top]) | |
| 71 | draw_gantt.path(["M", issue_from_right_rel, issue_from_top, | |
| 72 | "L", issue_from_right_rel, issue_middle_top]) | |
| 75 | 73 |                      .attr({stroke: color, | 
| 76 | 74 | "stroke-width": rels_stroke_width | 
| 77 | 75 | }); | 
| 78 |       draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_middle_top, | |
| 79 |                        "L", issue_to_left_rel + draw_left,    issue_middle_top]) | |
| 76 | draw_gantt.path(["M", issue_from_right_rel, issue_middle_top, | |
| 77 | "L", issue_to_left_rel, issue_middle_top]) | |
| 80 | 78 |                      .attr({stroke: color, | 
| 81 | 79 | "stroke-width": rels_stroke_width | 
| 82 | 80 | }); | 
| 83 |       draw_gantt.path(["M", issue_to_left_rel + draw_left, issue_middle_top, | |
| 84 |                        "L", issue_to_left_rel + draw_left, issue_to_top]) | |
| 81 | draw_gantt.path(["M", issue_to_left_rel, issue_middle_top, | |
| 82 | "L", issue_to_left_rel, issue_to_top]) | |
| 85 | 83 |                      .attr({stroke: color, | 
| 86 | 84 | "stroke-width": rels_stroke_width | 
| 87 | 85 | }); | 
| 88 |       draw_gantt.path(["M", issue_to_left_rel + draw_left, issue_to_top, | |
| 89 |                        "L", issue_to_left + draw_left,     issue_to_top]) | |
| 86 | draw_gantt.path(["M", issue_to_left_rel, issue_to_top, | |
| 87 | "L", issue_to_left, issue_to_top]) | |
| 90 | 88 |                      .attr({stroke: color, | 
| 91 | 89 | "stroke-width": rels_stroke_width | 
| 92 | 90 | }); | 
| 93 | 91 | } | 
| 94 |     draw_gantt.path(["M", issue_to_left + draw_left, issue_to_top, | |
| 92 | draw_gantt.path(["M", issue_to_left, issue_to_top, | |
| 95 | 93 | "l", -4 * rels_stroke_width, -2 * rels_stroke_width, | 
| 96 | 94 | "l", 0, 4 * rels_stroke_width, "z"]) | 
| 97 | 95 |                    .attr({stroke: "none", | 
| ... | ... | |
| 104 | 102 | |
| 105 | 103 | function getProgressLinesArray() { | 
| 106 | 104 | var arr = new Array(); | 
| 107 |   var today_left = $('#today_line').position().left; | |
| 105 |   var today_left = $('#today_line').position().left + $("#gantt_area").scrollLeft(); | |
| 108 | 106 |   arr.push({left: today_left, top: 0}); | 
| 109 | 107 |   $.each($('div.issue-subject, div.version-name'), function(index, element) { | 
| 110 | 108 |     if(!$(element).is(':visible')) return true; | 
| 111 |     var t = $(element).position().top - draw_top ; | |
| 109 |     var t = $(element).offset().top - draw_top ; | |
| 112 | 110 | var h = ($(element).height() / 9); | 
| 113 | 111 | var element_top_upper = t - h; | 
| 114 | 112 | var element_top_center = t + (h * 3); | 
| ... | ... | |
| 125 | 123 |         arr.push({left: draw_right, top: element_top_upper, is_right_edge: true}); | 
| 126 | 124 |         arr.push({left: draw_right, top: element_top_lower, is_right_edge: true, none_stroke: true}); | 
| 127 | 125 |       } else if (issue_done.length > 0) { | 
| 128 | var done_left = issue_done.first().position().left + | |
| 129 | issue_done.first().width(); | |
| 126 | var done_left = today_left; | |
| 127 |         var issue_todo = $("#task-todo-" + $(element).attr("id")); | |
| 128 |         if (issue_todo.length > 0){ | |
| 129 | done_left = issue_todo.first().position().left; | |
| 130 | } | |
| 130 | 131 |         arr.push({left: done_left, top: element_top_center}); | 
| 131 | 132 |       } else if (is_behind_start) { | 
| 132 | 133 |         arr.push({left: 0 , top: element_top_upper, is_left_edge: true}); | 
| ... | ... | |
| 145 | 146 | } | 
| 146 | 147 | |
| 147 | 148 | function drawGanttProgressLines() { | 
| 149 |   if(!$("#today_line").length) return; | |
| 148 | 150 | var arr = getProgressLinesArray(); | 
| 149 | 151 |   var color = $("#today_line") | 
| 150 | 152 |                     .css("border-left-color"); | 
| ... | ... | |
| 154 | 156 |         (!("is_right_edge" in arr[i - 1] && "is_right_edge" in arr[i]) && | 
| 155 | 157 |          !("is_left_edge"  in arr[i - 1] && "is_left_edge"  in arr[i])) | 
| 156 | 158 |         ) { | 
| 157 |       var x1 = (arr[i - 1].left == 0) ? 0 : arr[i - 1].left + draw_left; | |
| 158 |       var x2 = (arr[i].left == 0)     ? 0 : arr[i].left     + draw_left; | |
| 159 | var x1 = (arr[i - 1].left == 0) ? 0 : arr[i - 1].left; | |
| 160 | var x2 = (arr[i].left == 0) ? 0 : arr[i].left; | |
| 159 | 161 | draw_gantt.path(["M", x1, arr[i - 1].top, | 
| 160 | 162 | "L", x2, arr[i].top]) | 
| 161 | 163 |                    .attr({stroke: color, "stroke-width": 2}); | 
| ... | ... | |
| 301 | 303 |     $('#available_c, #selected_c').children("[value='" + value + "']").prop('disabled', true); | 
| 302 | 304 | }); | 
| 303 | 305 | } | 
| 306 | ||
| 307 | initGanttDnD = function(){ | |
| 308 | var grid_x = 0; | |
| 309 |   if($('#zoom').length){ | |
| 310 |     switch(parseInt($('#zoom').val())){ | |
| 311 | case 4: | |
| 312 | grid_x = 16; | |
| 313 | break; | |
| 314 | case 3: | |
| 315 | grid_x = 8; | |
| 316 | break; | |
| 317 | } | |
| 318 | } | |
| 319 |   if(grid_x > 0){ | |
| 320 |     $('.leaf .task_todo').draggable({ | |
| 321 | containment: 'parent', | |
| 322 | axis: 'x', | |
| 323 | grid: [grid_x, 0], | |
| 324 | opacity: 0.5, | |
| 325 | cursor: 'move', | |
| 326 | revertDuration: 100, | |
| 327 |       start: function (event, ui) { | |
| 328 | var helper = ui.helper[0]; | |
| 329 | helper.startLeft = ui.position.left; | |
| 330 | }, | |
| 331 | }); | |
| 332 | ||
| 333 |     $('.task.line').droppable({ | |
| 334 | accept: '.leaf .task_todo', | |
| 335 |       drop: function (event, ui) { | |
| 336 | var target = $(ui.draggable); | |
| 337 |         var url = target.attr('data-url-change-duration'); | |
| 338 |         var object = JSON.parse(target.attr('data-object')); | |
| 339 | var startLeft = target[0].startLeft; | |
| 340 | var relative_days = Math.floor((ui.position.left - startLeft) / grid_x); | |
| 341 |         if(relative_days == 0){ | |
| 342 | return; | |
| 343 | } | |
| 344 | var start_date = new Date(object.start_date); | |
| 345 | start_date.setDate(start_date.getDate() + relative_days); | |
| 346 | start_date = | |
| 347 | [ | |
| 348 | start_date.getFullYear(), | |
| 349 |             ('0' + (start_date.getMonth() + 1)).slice(-2), | |
| 350 |             ('0' + start_date.getDate()).slice(-2), | |
| 351 |           ].join('-'); | |
| 352 | var due_date = null; | |
| 353 |         if(object.due_date != null){ | |
| 354 | due_date = new Date(object.due_date); | |
| 355 | due_date.setDate(due_date.getDate() + relative_days); | |
| 356 | due_date = | |
| 357 | [ | |
| 358 | due_date.getFullYear(), | |
| 359 |               ('0' + (due_date.getMonth() + 1)).slice(-2), | |
| 360 |               ('0' + due_date.getDate()).slice(-2), | |
| 361 |             ].join('-'); | |
| 362 | } | |
| 363 | ||
| 364 |         $('#selected_c option:not(:disabled)').prop('selected', true); | |
| 365 |         var form = $('#query_form').serializeArray(); | |
| 366 |         var json_param = {}; | |
| 367 |         form.forEach(function(data){ | |
| 368 | var key = data.name; | |
| 369 | var value = data.value; | |
| 370 |           if(/\[\]$/.test(key)){ | |
| 371 |             if(!json_param.hasOwnProperty(key)){ | |
| 372 | json_param[key] = []; | |
| 373 | } | |
| 374 | json_param[key].push(value); | |
| 375 | } | |
| 376 |           else{ | |
| 377 | json_param[key] = value; | |
| 378 | } | |
| 379 | }); | |
| 380 |         $('#selected_c option:not(:disabled)').prop('selected', false); | |
| 381 |         Object.assign(json_param, { | |
| 382 |           change_duration: { | |
| 383 | start_date: start_date, | |
| 384 | due_date: due_date, | |
| 385 | lock_version: object.lock_version, | |
| 386 | }, | |
| 387 | }); | |
| 388 | ||
| 389 |         $.ajax({ | |
| 390 | type: 'PUT', | |
| 391 | url: url, | |
| 392 | data: json_param, | |
| 393 |         }).done(function(data){ | |
| 394 | drawGanttHandler(); | |
| 395 | initGanttDnD(); | |
| 396 |         }).fail(function(jqXHR){ | |
| 397 |           var contents = $('<div>' + jqXHR.responseText + '</div>'); | |
| 398 |           var error_message = contents.find('p#errorExplanation'); | |
| 399 |           if(error_message.length){ | |
| 400 |             $('div#content h2:first-of-type').after(error_message); | |
| 401 |             $('p#errorExplanation').hide('fade', {}, 3000, function(){ | |
| 402 | $(this).remove(); | |
| 403 | }); | |
| 404 | } | |
| 405 |           ui.draggable.animate({'left': ui.draggable[0].startLeft}, 'fast'); | |
| 406 | }); | |
| 407 | } | |
| 408 | }); | |
| 409 | } | |
| 410 | }; | |
| 411 | ||
| 412 | $(document).ready(initGanttDnD); | |
| public/stylesheets/application.css | ||
|---|---|---|
| 1410 | 1410 | |
| 1411 | 1411 | .task { | 
| 1412 | 1412 | position: absolute; | 
| 1413 |   height:8px; | |
| 1413 |   height:10px; | |
| 1414 | 1414 | font-size:0.8em; | 
| 1415 | 1415 | color:#888; | 
| 1416 | 1416 | padding:0; | 
| ... | ... | |
| 1419 | 1419 | white-space:nowrap; | 
| 1420 | 1420 | } | 
| 1421 | 1421 | |
| 1422 | .task.label {width:100%;} | |
| 1423 | .task.label.project, .task.label.version { font-weight: bold; } | |
| 1422 | .task.line { left: 0; } | |
| 1423 | .task div.tooltip:hover span.tip { font-size: inherit; } | |
| 1424 | .task .task_todo .label { font-size: inherit; } | |
| 1425 | .task.project .task_todo .label { margin-top: -4px; } | |
| 1426 | .task.version .task_todo .label { margin-top: -3px; } | |
| 1424 | 1427 | |
| 1425 | .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; } | |
| 1426 | .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; } | |
| 1427 | .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; } | |
| 1428 | .task .label { position: absolute; width: auto; } | |
| 1429 | .task.project .label, .task.version .label { font-weight: bold; } | |
| 1428 | 1430 | |
| 1429 | .task_todo.parent { background: #888; border: 1px solid #888; height: 3px;} | |
| 1430 | .task_late.parent, .task_done.parent { height: 3px;} | |
| 1431 | .task.parent.marker.starting  { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; left: 0px; top: -1px;} | |
| 1432 | .task.parent.marker.ending { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; right: 0px; top: -1px;} | |
| 1431 | .task_late { position: absolute; height: inherit; background:#f66; } | |
| 1432 | .task_done { position: absolute; height: inherit; background:#00c600; } | |
| 1433 | .task_todo { position: absolute; height: inherit; background:#aaa; } | |
| 1433 | 1434 | |
| 1434 | .version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;} | |
| 1435 | .version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;} | |
| 1436 | .version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;} | |
| 1437 | .version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; } | |
| 1435 | .parent .task_todo { background: #888; height: 5px; } | |
| 1436 | .parent .task_late, .parent .task_done { height: 5px; } | |
| 1437 | .parent .marker { | |
| 1438 | background: #888; | |
| 1439 | display: inline-block; | |
| 1440 | position: absolute; | |
| 1441 | width: 8px; | |
| 1442 | height: 6px; | |
| 1443 | margin-left: -5px; | |
| 1444 | margin-bottom: -4px; | |
| 1445 | } | |
| 1446 | .parent .marker:after { | |
| 1447 | border-top: 3px solid #888; | |
| 1448 | border-left: 4px solid transparent; | |
| 1449 | border-right: 4px solid transparent; | |
| 1450 | content: ''; | |
| 1451 | height: 0; | |
| 1452 | left: 0; | |
| 1453 | position: absolute; | |
| 1454 | bottom: -3px; | |
| 1455 | width: 0; | |
| 1456 | } | |
| 1457 | ||
| 1458 | .version .task_late { background:#f66; height: 4px; } | |
| 1459 | .version .task_done { background:#00c600; height: 4px; } | |
| 1460 | .version .task_todo { background:#aaa; height: 4px; margin-top: 3px; } | |
| 1461 | .version .marker { | |
| 1462 | width: 0; | |
| 1463 | height: 0; | |
| 1464 | border: 5px solid transparent; | |
| 1465 | border-bottom-color: black; | |
| 1466 | position: absolute; | |
| 1467 | margin-top: -5px; | |
| 1468 | margin-left: -6px; | |
| 1469 | } | |
| 1470 | .version .marker:after { | |
| 1471 | content: ''; | |
| 1472 | position: absolute; | |
| 1473 | left: -5px; | |
| 1474 | top: 5px; | |
| 1475 | width: 0; | |
| 1476 | height: 0; | |
| 1477 | border: 5px solid transparent; | |
| 1478 | border-top-color: black; | |
| 1479 | } | |
| 1438 | 1480 | |
| 1439 | .project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;} | |
| 1440 | .project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;} | |
| 1441 | .project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;} | |
| 1442 | .project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; } | |
| 1481 | .project .task_late { background:#f66; height: 2px; } | |
| 1482 | .project .task_done { background:#00c600; height: 2px; } | |
| 1483 | .project .task_todo { background:#aaa; height: 2px; margin-top: 4px; } | |
| 1484 | .project .marker { | |
| 1485 | width: 0; | |
| 1486 | height: 0; | |
| 1487 | border: 5px solid transparent; | |
| 1488 | border-bottom-color: blue; | |
| 1489 | position: absolute; | |
| 1490 | margin-top: -5px; | |
| 1491 | margin-left: -6px; | |
| 1492 | } | |
| 1493 | .project .marker:after { | |
| 1494 | content: ''; | |
| 1495 | position: absolute; | |
| 1496 | left: -5px; | |
| 1497 | top: 5px; | |
| 1498 | width: 0; | |
| 1499 | height: 0; | |
| 1500 | border: 5px solid transparent; | |
| 1501 | border-top-color: blue; | |
| 1502 | } | |
| 1443 | 1503 | |
| 1444 | 1504 | .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;} | 
| 1445 | 1505 | .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;} | 
| test/integration/routing/gantts_test.rb | ||
|---|---|---|
| 26 | 26 | |
| 27 | 27 | should_route 'GET /projects/foo/issues/gantt' => 'gantts#show', :project_id => 'foo' | 
| 28 | 28 | should_route 'GET /projects/foo/issues/gantt.pdf' => 'gantts#show', :project_id => 'foo', :format => 'pdf' | 
| 29 | ||
| 30 | should_route 'PUT /gantt/123/change_duration' => 'gantts#change_duration', :id => '123' | |
| 29 | 31 | end | 
| 30 | 32 | end | 
| test/unit/lib/redmine/helpers/gantt_test.rb | ||
|---|---|---|
| 235 | 235 | @project.issues << @issue | 
| 236 | 236 | @output_buffer = @gantt.lines | 
| 237 | 237 | |
| 238 | assert_select "div.project.task_todo" | |
| 239 | assert_select "div.project.starting" | |
| 240 | assert_select "div.project.ending" | |
| 241 |     assert_select "div.label.project", /#{@project.name}/ | |
| 238 | assert_select "div.task.project" do | |
| 239 | assert_select "> div.task_todo" do | |
| 240 |         assert_select "> div.label", /#{@project.name}/ | |
| 241 | end | |
| 242 | assert_select "> div.starting" | |
| 243 | assert_select "> div.ending" | |
| 244 | end | |
| 242 | 245 | |
| 243 | assert_select "div.version.task_todo" | |
| 244 | assert_select "div.version.starting" | |
| 245 | assert_select "div.version.ending" | |
| 246 |     assert_select "div.label.version", /#{@version.name}/ | |
| 246 | assert_select "div.task.version" do | |
| 247 | assert_select "> div.task_todo" do | |
| 248 |         assert_select "div.label", /#{@version.name}/ | |
| 249 | end | |
| 250 | assert_select "> div.starting" | |
| 251 | assert_select "> div.ending" | |
| 252 | end | |
| 247 | 253 | |
| 248 | assert_select "div.task_todo" | |
| 249 |     assert_select "div.task.label", /#{@issue.done_ratio}/ | |
| 250 |     assert_select "div.tooltip", /#{@issue.subject}/ | |
| 254 | assert_select "div.task" do | |
| 255 | assert_select "> div.task_todo" do | |
| 256 |         assert_select "> div.label", /#{@issue.done_ratio}/ | |
| 257 |         assert_select "> div.tooltip", /#{@issue.subject}/ | |
| 258 | end | |
| 259 | end | |
| 251 | 260 | end | 
| 252 | 261 | |
| 253 | 262 | test "#selected_column_content" do | 
| ... | ... | |
| 331 | 340 | @project.stubs(:start_date).returns(today - 7) | 
| 332 | 341 | @project.stubs(:due_date).returns(today + 7) | 
| 333 | 342 | @output_buffer = @gantt.line_for_project(@project, :format => :html) | 
| 334 | assert_select "div.project.label", :text => @project.name | |
| 343 | assert_select "div.task.project > div.task_todo" do | |
| 344 | assert_select "> div.label", :text => @project.name | |
| 345 | end | |
| 335 | 346 | end | 
| 336 | 347 | |
| 337 | 348 | test "#line_for_version" do | 
| ... | ... | |
| 341 | 352 | version.stubs(:due_date).returns(today + 7) | 
| 342 | 353 | version.stubs(:visible_fixed_issues => stub(:completed_percent => 30)) | 
| 343 | 354 | @output_buffer = @gantt.line_for_version(version, :format => :html) | 
| 344 | assert_select "div.version.label", :text => /Foo/ | |
| 345 | assert_select "div.version.label", :text => /30%/ | |
| 355 | assert_select "div.task.version > div.task_todo" do | |
| 356 | assert_select "> div.label", :text => 'Foo 30%' | |
| 357 | end | |
| 346 | 358 | end | 
| 347 | 359 | |
| 348 | 360 | test "#line_for_issue" do | 
| 349 | 361 | create_gantt | 
| 350 | 362 | issue = Issue.generate!(:project => @project, :start_date => today - 7, :due_date => today + 7, :done_ratio => 30) | 
| 351 | 363 | @output_buffer = @gantt.line_for_issue(issue, :format => :html) | 
| 352 |     assert_select "div.task.label", :text => /#{issue.status.name}/ | |
| 353 | assert_select "div.task.label", :text => /30%/ | |
| 354 |     assert_select "div.tooltip", /#{issue.subject}/ | |
| 364 | assert_select "div.task_todo" do | |
| 365 |       assert_select "> div.label", :text => "#{issue.status.name} 30%" | |
| 366 |       assert_select "> div.tooltip", /#{issue.subject}/ | |
| 367 | end | |
| 355 | 368 | end | 
| 356 | 369 | |
| 357 | 370 | test "#line todo line should start from the starting point on the left" do | 
| ... | ... | |
| 365 | 378 | [gantt_start - 1, gantt_start].each do |start_date| | 
| 366 | 379 | @output_buffer = @gantt.line(start_date, gantt_start, 30, false, 'line', :format => :html, :zoom => 4) | 
| 367 | 380 | # the leftmost date (Date.today - 14 days) | 
| 368 | assert_select 'div.task_todo[style*="left:0px"]', 1, @output_buffer | |
| 369 | assert_select 'div.task_todo[style*="width:2px"]', 1, @output_buffer | |
| 381 | assert_select 'div.task_todo[style*="left:0px"][style*="width:4px"]', 1, @output_buffer | |
| 370 | 382 | end | 
| 371 | 383 | end | 
| 372 | 384 | |
| ... | ... | |
| 375 | 387 | [gantt_end, gantt_end + 1].each do |end_date| | 
| 376 | 388 | @output_buffer = @gantt.line(gantt_end, end_date, 30, false, 'line', :format => :html, :zoom => 4) | 
| 377 | 389 | # the rightmost date (Date.today + 14 days) | 
| 378 | assert_select 'div.task_todo[style*="left:112px"]', 1, @output_buffer | |
| 379 | assert_select 'div.task_todo[style*="width:2px"]', 1, @output_buffer | |
| 390 | assert_select 'div.task_todo[style*="left:112px"][style*="width:4px"]', 1, @output_buffer | |
| 380 | 391 | end | 
| 381 | 392 | end | 
| 382 | 393 | |
| 383 | 394 | test "#line todo line should be the total width" do | 
| 384 | 395 | create_gantt | 
| 385 | 396 | @output_buffer = @gantt.line(today - 7, today + 7, 30, false, 'line', :format => :html, :zoom => 4) | 
| 386 |     assert_select 'div.task_todo[style*="width:58px"]', 1 | |
| 397 |     assert_select 'div.task_todo[style*="width:60px"]', 1 | |
| 387 | 398 | end | 
| 388 | 399 | |
| 389 | 400 | test "#line late line should start from the starting point on the left" do | 
| 390 | 401 | create_gantt | 
| 391 | 402 | @output_buffer = @gantt.line(today - 7, today + 7, 30, false, 'line', :format => :html, :zoom => 4) | 
| 392 | assert_select 'div.task_late[style*="left:28px"]', 1 | |
| 403 | assert_select 'div.task_todo[style*="left:28px"]' do | |
| 404 | assert_select '> div.task_late', 1 | |
| 405 | end | |
| 393 | 406 | end | 
| 394 | 407 | |
| 395 | 408 | test "#line late line should be the total delayed width" do | 
| 396 | 409 | create_gantt | 
| 397 | 410 | @output_buffer = @gantt.line(today - 7, today + 7, 30, false, 'line', :format => :html, :zoom => 4) | 
| 398 |     assert_select 'div.task_late[style*="width:30px"]', 1 | |
| 411 |     assert_select 'div.task_late[style*="width:32px"]', 1 | |
| 399 | 412 | end | 
| 400 | 413 | |
| 401 | 414 | test "#line late line should be the same width as task_todo if start date and end date are the same day" do | 
| 402 | 415 | create_gantt | 
| 403 | 416 | @output_buffer = @gantt.line(today - 7, today - 7, 0, false, 'line', :format => :html, :zoom => 4) | 
| 404 | assert_select 'div.task_late[style*="width:2px"]', 1 | |
| 405 | assert_select 'div.task_todo[style*="width:2px"]', 1 | |
| 417 | assert_select 'div.task_todo[style*="width:4px"]' do | |
| 418 | assert_select '> div.task_late[style*="width:4px"]', 1 | |
| 419 | end | |
| 406 | 420 | end | 
| 407 | 421 | |
| 408 | 422 | test "#line late line should be the same width as task_todo if start date and today are the same day" do | 
| 409 | 423 | create_gantt | 
| 410 | 424 | @output_buffer = @gantt.line(today, today, 0, false, 'line', :format => :html, :zoom => 4) | 
| 411 | assert_select 'div.task_late[style*="width:2px"]', 1 | |
| 412 | assert_select 'div.task_todo[style*="width:2px"]', 1 | |
| 425 | assert_select 'div.task_todo[style*="width:4px"]' do | |
| 426 | assert_select '> div.task_late[style*="width:4px"]', 1 | |
| 427 | end | |
| 413 | 428 | end | 
| 414 | 429 | |
| 415 | 430 | test "#line done line should start from the starting point on the left" do | 
| 416 | 431 | create_gantt | 
| 417 | 432 | @output_buffer = @gantt.line(today - 7, today + 7, 30, false, 'line', :format => :html, :zoom => 4) | 
| 418 | assert_select 'div.task_done[style*="left:28px"]', 1 | |
| 433 | assert_select 'div.task_todo[style*="left:28px"]' do | |
| 434 | assert_select '> div.task_done', 1 | |
| 435 | end | |
| 419 | 436 | end | 
| 420 | 437 | |
| 421 | 438 | test "#line done line should be the width for the done ratio" do | 
| 422 | 439 | create_gantt | 
| 423 | 440 | @output_buffer = @gantt.line(today - 7, today + 7, 30, false, 'line', :format => :html, :zoom => 4) | 
| 424 |     # 15 days * 4 px * 30% - 2 px for borders = 16 px | |
| 425 |     assert_select 'div.task_done[style*="width:16px"]', 1 | |
| 441 |     # 15 days * 4 px * 30% = 18 px | |
| 442 |     assert_select 'div.task_done[style*="width:18px"]', 1 | |
| 426 | 443 | end | 
| 427 | 444 | |
| 428 | 445 | test "#line done line should be the total width for 100% done ratio" do | 
| 429 | 446 | create_gantt | 
| 430 | 447 | @output_buffer = @gantt.line(today - 7, today + 7, 100, false, 'line', :format => :html, :zoom => 4) | 
| 431 |     # 15 days * 4 px - 2 px for borders = 58 px | |
| 432 |     assert_select 'div.task_done[style*="width:58px"]', 1 | |
| 448 |     # 15 days * 4 px = 60 px | |
| 449 |     assert_select 'div.task_done[style*="width:60px"]', 1 | |
| 433 | 450 | end | 
| 434 | 451 | |
| 435 | 452 | test "#line done line should be the total width for 100% done ratio with same start and end dates" do | 
| 436 | 453 | create_gantt | 
| 437 | 454 | @output_buffer = @gantt.line(today + 7, today + 7, 100, false, 'line', :format => :html, :zoom => 4) | 
| 438 |     assert_select 'div.task_done[style*="width:2px"]', 1 | |
| 455 |     assert_select 'div.task_done[style*="width:4px"]', 1 | |
| 439 | 456 | end | 
| 440 | 457 | |
| 441 | 458 | test "#line done line should not be the total done width if the gantt starts after start date" do | 
| 442 | 459 | create_gantt | 
| 443 | 460 | @output_buffer = @gantt.line(today - 16, today - 2, 30, false, 'line', :format => :html, :zoom => 4) | 
| 444 | assert_select 'div.task_done[style*="left:0px"]', 1 | |
| 445 | assert_select 'div.task_done[style*="width:8px"]', 1 | |
| 461 | assert_select 'div.task_todo[style*="left:0px"]' do | |
| 462 | assert_select '> div.task_done[style*="width:10px"]', 1 | |
| 463 | end | |
| 446 | 464 | end | 
| 447 | 465 | |
| 448 | 466 | test "#line starting marker should appear at the start date" do |