Feature #2024 » 2024-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 = "#task-todo-#{klass_name}-#{obj.id}" |
|
49 |
css_subject = 'span.' << |
|
50 |
case obj |
|
51 |
when Project |
|
52 |
'icon-projects' |
|
53 |
when Version |
|
54 |
'icon-package' |
|
55 |
when Issue |
|
56 |
'icon-issue' |
|
57 |
end |
|
58 |
elm_subject = "##{klass_name}-#{obj.id} #{css_subject}" |
|
59 | ||
60 |
subject_content = Nokogiri::HTML.parse(gantt.__send__(:html_subject_content, obj)) |
|
61 |
subject_content = subject_content.css(css_subject).to_s.tr("\n",'').gsub(/'/, "\\\\'") |
|
62 |
-%> |
|
63 |
if($('<%= elm_subject %>').length){ |
|
64 |
$('<%= elm_todo %>').parent().html('<%= raw(todo_content) %>'); |
|
65 |
$('<%= elm_subject %>').replaceWith('<%= raw(subject_content) %>'); |
|
66 |
<% |
|
67 |
case obj |
|
68 |
when Issue |
|
69 |
@query.columns.each do |column| |
|
70 |
-%> |
|
71 |
elm = $('div.gantt_selected_column_content #<%= column.name %>_issue_<%= obj.id %>'); |
|
72 |
if(elm.length){ |
|
73 |
elm.html('<%= escape_javascript(column_content(column, obj)) %>'); |
|
74 |
} |
|
75 |
<% |
|
76 |
end |
|
77 |
end |
|
78 |
-%> |
|
79 |
} |
|
80 |
<% |
|
81 |
end |
|
82 |
-%> |
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 | ||
---|---|---|
750 | 750 |
tag_options[:class] = "version-name" |
751 | 751 |
has_children = object.fixed_issues.exists? |
752 | 752 |
when Project |
753 |
tag_options[:id] = "project-#{object.id}" |
|
753 | 754 |
tag_options[:class] = "project-name" |
754 | 755 |
has_children = object.issues.exists? || object.versions.exists? |
755 | 756 |
end |
... | ... | |
828 | 829 |
end |
829 | 830 |
# Renders the task bar, with progress and late |
830 | 831 |
if coords[:bar_start] && coords[:bar_end] |
831 |
width = coords[:bar_end] - coords[:bar_start] - 2
|
|
832 |
width = coords[:bar_end] - coords[:bar_start] |
|
832 | 833 |
style = +"" |
833 |
style << "top:#{params[:top]}px;" |
|
834 | 834 |
style << "left:#{coords[:bar_start]}px;" |
835 | 835 |
style << "width:#{width}px;" |
836 |
html_id = "task-todo-issue-#{object.id}" if object.is_a?(Issue) |
|
837 |
html_id = "task-todo-version-#{object.id}" if object.is_a?(Version) |
|
836 |
html_id = |
|
837 |
case object |
|
838 |
when Project |
|
839 |
"task-todo-project-#{object.id}" |
|
840 |
when Version |
|
841 |
"task-todo-version-#{object.id}" |
|
842 |
when Issue |
|
843 |
"task-todo-issue-#{object.id}" |
|
844 |
end |
|
838 | 845 |
content_opt = {:style => style, |
839 |
:class => "#{css} task_todo",
|
|
846 |
:class => "task_todo", |
|
840 | 847 |
:id => html_id, |
841 | 848 |
:data => {}} |
842 | 849 |
if object.is_a?(Issue) |
... | ... | |
844 | 851 |
if rels.present? |
845 | 852 |
content_opt[:data] = {"rels" => rels.to_json} |
846 | 853 |
end |
854 |
content_opt[:data].merge!({ |
|
855 |
:url_change_duration => Rails.application.routes.url_helpers.gantt_change_duration_path( |
|
856 |
object |
|
857 |
), |
|
858 |
:object => { |
|
859 |
:start_date => object.start_date, |
|
860 |
:due_date => object.due_date, |
|
861 |
:lock_version => object.lock_version, |
|
862 |
}.to_json, |
|
863 |
}) |
|
847 | 864 |
end |
848 | 865 |
content_opt[:data].merge!(data_options) |
849 |
output << view.content_tag(:div, ' '.html_safe, content_opt)
|
|
866 |
bar_contents = []
|
|
850 | 867 |
if coords[:bar_late_end] |
851 |
width = coords[:bar_late_end] - coords[:bar_start] - 2
|
|
868 |
width = coords[:bar_late_end] - coords[:bar_start] |
|
852 | 869 |
style = +"" |
853 |
style << "top:#{params[:top]}px;" |
|
854 |
style << "left:#{coords[:bar_start]}px;" |
|
855 | 870 |
style << "width:#{width}px;" |
856 |
output << view.content_tag(:div, ' '.html_safe,
|
|
857 |
:style => style, |
|
858 |
:class => "#{css} task_late",
|
|
859 |
:data => data_options) |
|
871 |
bar_contents << view.content_tag(:div, ' '.html_safe,
|
|
872 |
:style => style,
|
|
873 |
:class => "task_late",
|
|
874 |
:data => data_options)
|
|
860 | 875 |
end |
861 | 876 |
if coords[:bar_progress_end] |
862 |
width = coords[:bar_progress_end] - coords[:bar_start] - 2
|
|
877 |
width = coords[:bar_progress_end] - coords[:bar_start] |
|
863 | 878 |
style = +"" |
864 |
style << "top:#{params[:top]}px;" |
|
865 |
style << "left:#{coords[:bar_start]}px;" |
|
866 | 879 |
style << "width:#{width}px;" |
867 | 880 |
html_id = "task-done-issue-#{object.id}" if object.is_a?(Issue) |
868 | 881 |
html_id = "task-done-version-#{object.id}" if object.is_a?(Version) |
869 |
output << view.content_tag(:div, ' '.html_safe, |
|
882 |
bar_contents << view.content_tag(:div, ' '.html_safe, |
|
883 |
:style => style, |
|
884 |
:class => "task_done", |
|
885 |
:id => html_id, |
|
886 |
:data => data_options) |
|
887 |
end |
|
888 | ||
889 |
# Renders the tooltip |
|
890 |
if object.is_a?(Issue) |
|
891 |
s = view.content_tag(:span, |
|
892 |
view.render_issue_tooltip(object).html_safe, |
|
893 |
:class => "tip") |
|
894 |
s += view.content_tag(:input, nil, :type => 'checkbox', :name => 'ids[]', :value => object.id, :style => 'display:none;', :class => 'toggle-selection') |
|
895 |
style = +"" |
|
896 |
style << "width:#{coords[:bar_end] - coords[:bar_start]}px;" |
|
897 |
style << "height:12px;" |
|
898 |
bar_contents << view.content_tag(:div, s.html_safe, |
|
899 |
:style => style, |
|
900 |
:class => "tooltip hascontextmenu", |
|
901 |
:data => data_options) |
|
902 |
end |
|
903 | ||
904 |
# Renders the label on the right |
|
905 |
if label |
|
906 |
style = +"" |
|
907 |
style << "top:0px;" |
|
908 |
style << "left:#{coords[:bar_end] - coords[:bar_start] + 8}px;" |
|
909 |
bar_contents << view.content_tag(:div, label, |
|
910 |
:style => style, |
|
911 |
:class => "label", |
|
912 |
:data => data_options) |
|
913 |
end |
|
914 | ||
915 |
bar_contents = bar_contents.join.presence |
|
916 |
output << view.content_tag(:div, (bar_contents || ' ').html_safe, content_opt) |
|
917 |
else |
|
918 |
# Renders the label on the right |
|
919 |
if label |
|
920 |
style = +"" |
|
921 |
style << "top:1px;" |
|
922 |
style << "left:#{(coords[:bar_end] || 0) + 8}px;" |
|
923 |
output << view.content_tag(:div, label, |
|
870 | 924 |
:style => style, |
871 |
:class => "#{css} task_done", |
|
872 |
:id => html_id, |
|
925 |
:class => "label", |
|
873 | 926 |
:data => data_options) |
874 | 927 |
end |
875 | 928 |
end |
... | ... | |
877 | 930 |
if markers |
878 | 931 |
if coords[:start] |
879 | 932 |
style = +"" |
880 |
style << "top:#{params[:top]}px;" |
|
881 | 933 |
style << "left:#{coords[:start]}px;" |
882 |
style << "width:15px;" |
|
883 | 934 |
output << view.content_tag(:div, ' '.html_safe, |
884 | 935 |
:style => style, |
885 |
:class => "#{css} marker starting",
|
|
936 |
:class => "marker starting", |
|
886 | 937 |
:data => data_options) |
887 | 938 |
end |
888 | 939 |
if coords[:end] |
889 | 940 |
style = +"" |
890 |
style << "top:#{params[:top]}px;" |
|
891 | 941 |
style << "left:#{coords[:end]}px;" |
892 |
style << "width:15px;" |
|
893 | 942 |
output << view.content_tag(:div, ' '.html_safe, |
894 | 943 |
:style => style, |
895 |
:class => "#{css} marker ending",
|
|
944 |
:class => "marker ending", |
|
896 | 945 |
:data => data_options) |
897 | 946 |
end |
898 | 947 |
end |
899 |
# Renders the label on the right |
|
900 |
if label |
|
901 |
style = +"" |
|
902 |
style << "top:#{params[:top]}px;" |
|
903 |
style << "left:#{(coords[:bar_end] || 0) + 8}px;" |
|
904 |
style << "width:15px;" |
|
905 |
output << view.content_tag(:div, label, |
|
906 |
:style => style, |
|
907 |
:class => "#{css} label", |
|
908 |
:data => data_options) |
|
909 |
end |
|
910 |
# Renders the tooltip |
|
911 |
if object.is_a?(Issue) && coords[:bar_start] && coords[:bar_end] |
|
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 << "position: absolute;" |
|
918 |
style << "top:#{params[:top]}px;" |
|
919 |
style << "left:#{coords[:bar_start]}px;" |
|
920 |
style << "width:#{coords[:bar_end] - coords[:bar_start]}px;" |
|
921 |
style << "height:12px;" |
|
922 |
output << view.content_tag(:div, s.html_safe, |
|
923 |
:style => style, |
|
924 |
:class => "tooltip hascontextmenu", |
|
925 |
:data => data_options) |
|
926 |
end |
|
948 |
output = view.content_tag(:div, output.html_safe, |
|
949 |
:class => "#{css} line", |
|
950 |
:style => "top:#{params[:top]}px;width:#{params[:g_width] - 1}px;", |
|
951 |
:data => data_options |
|
952 |
) |
|
927 | 953 |
@lines << output |
928 | 954 |
output |
929 | 955 |
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 | ||
---|---|---|
1401 | 1401 | |
1402 | 1402 |
.task { |
1403 | 1403 |
position: absolute; |
1404 |
height:8px;
|
|
1404 |
height:10px;
|
|
1405 | 1405 |
font-size:0.8em; |
1406 | 1406 |
color:#888; |
1407 | 1407 |
padding:0; |
... | ... | |
1410 | 1410 |
white-space:nowrap; |
1411 | 1411 |
} |
1412 | 1412 | |
1413 |
.task.label {width:100%;} |
|
1414 |
.task.label.project, .task.label.version { font-weight: bold; } |
|
1413 |
.task.line { left: 0; } |
|
1414 |
.task div.tooltip:hover span.tip { font-size: inherit; } |
|
1415 |
.task .task_todo .label { font-size: inherit; } |
|
1416 |
.task.project .task_todo .label { margin-top: -4px; } |
|
1417 |
.task.version .task_todo .label { margin-top: -3px; } |
|
1415 | 1418 | |
1416 |
.task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; } |
|
1417 |
.task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; } |
|
1418 |
.task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; } |
|
1419 |
.task .label { position: absolute; width: auto; } |
|
1420 |
.task.project .label, .task.version .label { font-weight: bold; } |
|
1419 | 1421 | |
1420 |
.task_todo.parent { background: #888; border: 1px solid #888; height: 3px;} |
|
1421 |
.task_late.parent, .task_done.parent { height: 3px;} |
|
1422 |
.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;} |
|
1423 |
.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;} |
|
1422 |
.task_late { position: absolute; height: inherit; background:#f66; } |
|
1423 |
.task_done { position: absolute; height: inherit; background:#00c600; } |
|
1424 |
.task_todo { position: absolute; height: inherit; background:#aaa; } |
|
1424 | 1425 | |
1425 |
.version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;} |
|
1426 |
.version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;} |
|
1427 |
.version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;} |
|
1428 |
.version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; } |
|
1426 |
.parent .task_todo { background: #888; height: 5px; } |
|
1427 |
.parent .task_late, .parent .task_done { height: 5px; } |
|
1428 |
.parent .marker { |
|
1429 |
background: #888; |
|
1430 |
display: inline-block; |
|
1431 |
position: absolute; |
|
1432 |
width: 8px; |
|
1433 |
height: 6px; |
|
1434 |
margin-left: -5px; |
|
1435 |
margin-bottom: -4px; |
|
1436 |
} |
|
1437 |
.parent .marker:after { |
|
1438 |
border-top: 3px solid #888; |
|
1439 |
border-left: 4px solid transparent; |
|
1440 |
border-right: 4px solid transparent; |
|
1441 |
content: ''; |
|
1442 |
height: 0; |
|
1443 |
left: 0; |
|
1444 |
position: absolute; |
|
1445 |
bottom: -3px; |
|
1446 |
width: 0; |
|
1447 |
} |
|
1448 | ||
1449 |
.version .task_late { background:#f66; height: 4px; } |
|
1450 |
.version .task_done { background:#00c600; height: 4px; } |
|
1451 |
.version .task_todo { background:#aaa; height: 4px; margin-top: 3px; } |
|
1452 |
.version .marker { |
|
1453 |
width: 0; |
|
1454 |
height: 0; |
|
1455 |
border: 5px solid transparent; |
|
1456 |
border-bottom-color: black; |
|
1457 |
position: absolute; |
|
1458 |
margin-top: -5px; |
|
1459 |
margin-left: -6px; |
|
1460 |
} |
|
1461 |
.version .marker:after { |
|
1462 |
content: ''; |
|
1463 |
position: absolute; |
|
1464 |
left: -5px; |
|
1465 |
top: 5px; |
|
1466 |
width: 0; |
|
1467 |
height: 0; |
|
1468 |
border: 5px solid transparent; |
|
1469 |
border-top-color: black; |
|
1470 |
} |
|
1429 | 1471 | |
1430 |
.project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;} |
|
1431 |
.project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;} |
|
1432 |
.project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;} |
|
1433 |
.project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; } |
|
1472 |
.project .task_late { background:#f66; height: 2px; } |
|
1473 |
.project .task_done { background:#00c600; height: 2px; } |
|
1474 |
.project .task_todo { background:#aaa; height: 2px; margin-top: 4px; } |
|
1475 |
.project .marker { |
|
1476 |
width: 0; |
|
1477 |
height: 0; |
|
1478 |
border: 5px solid transparent; |
|
1479 |
border-bottom-color: blue; |
|
1480 |
position: absolute; |
|
1481 |
margin-top: -5px; |
|
1482 |
margin-left: -6px; |
|
1483 |
} |
|
1484 |
.project .marker:after { |
|
1485 |
content: ''; |
|
1486 |
position: absolute; |
|
1487 |
left: -5px; |
|
1488 |
top: 5px; |
|
1489 |
width: 0; |
|
1490 |
height: 0; |
|
1491 |
border: 5px solid transparent; |
|
1492 |
border-top-color: blue; |
|
1493 |
} |
|
1434 | 1494 | |
1435 | 1495 |
.version-behind-schedule a, .issue-behind-schedule a {color: #f66914;} |
1436 | 1496 |
.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 |