Feature #3436 » gantt-relations-r11060.diff
app/views/gantts/show.html.erb | ||
---|---|---|
1 |
<% content_for :header_tags do %> |
|
2 |
<%= javascript_include_tag 'raphael' %> |
|
3 |
<%= javascript_include_tag 'gantt' %> |
|
4 |
<% end %> |
|
5 |
<%= javascript_tag do %> |
|
6 |
$(document).ready(drawGanttHandler); |
|
7 |
$(window).resize(drawGanttHandler); |
|
8 |
<% end %> |
|
1 | 9 |
<% @gantt.view = self %> |
2 | 10 |
<h2><%= @query.new_record? ? l(:label_gantt) : h(@query.name) %></h2> |
3 | 11 | |
... | ... | |
102 | 110 |
</td> |
103 | 111 | |
104 | 112 |
<td> |
105 |
<div style="position:relative;height:<%= t_height + 24 %>px;overflow:auto;"> |
|
113 |
<div style="position:relative;height:<%= t_height + 24 %>px;overflow:auto;" id="gantt_area">
|
|
106 | 114 |
<% |
107 | 115 |
style = "" |
108 | 116 |
style += "width: #{g_width - 1}px;" |
... | ... | |
231 | 239 |
%> |
232 | 240 |
<%= content_tag(:div, ' '.html_safe, :style => style) %> |
233 | 241 |
<% end %> |
234 | ||
242 |
<% |
|
243 |
style = "" |
|
244 |
style += "position: absolute;" |
|
245 |
style += "height: #{g_height}px;" |
|
246 |
style += "top: #{headers_height + 1}px;" |
|
247 |
style += "left: 0px;" |
|
248 |
style += "width: #{g_width - 1}px;" |
|
249 |
%> |
|
250 |
<%= content_tag(:div, '', :style => style, :id => "gantt_draw_area") %> |
|
235 | 251 |
</div> |
236 | 252 |
</td> |
237 | 253 |
</tr> |
public/javascripts/gantt.js | ||
---|---|---|
1 |
var draw_gantt = null; |
|
2 |
var draw_top; |
|
3 |
var draw_right; |
|
4 |
var draw_left; |
|
5 | ||
6 |
function setDrawArea() { |
|
7 |
draw_top = $("#gantt_draw_area").position().top; |
|
8 |
draw_right = $("#gantt_draw_area").width(); |
|
9 |
draw_left = $("#gantt_area").scrollLeft(); |
|
10 |
} |
|
11 | ||
12 |
function drawGanttHandler() { |
|
13 |
var folder = document.getElementById('gantt_draw_area'); |
|
14 |
if(draw_gantt != null) |
|
15 |
draw_gantt.clear(); |
|
16 |
else |
|
17 |
draw_gantt = Raphael(folder); |
|
18 |
setDrawArea(); |
|
19 |
} |
app/models/issue_relation.rb | ||
---|---|---|
51 | 51 |
TYPE_DUPLICATED => { :name => :label_duplicated_by, :sym_name => :label_duplicates, |
52 | 52 |
:order => 3, :sym => TYPE_DUPLICATES, :reverse => TYPE_DUPLICATES }, |
53 | 53 |
TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, |
54 |
:order => 4, :sym => TYPE_BLOCKED }, |
|
54 |
:order => 4, :sym => TYPE_BLOCKED, |
|
55 |
:landscape_margin => 12 }, |
|
55 | 56 |
TYPE_BLOCKED => { :name => :label_blocked_by, :sym_name => :label_blocks, |
56 | 57 |
:order => 5, :sym => TYPE_BLOCKS, :reverse => TYPE_BLOCKS }, |
57 | 58 |
TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, |
58 |
:order => 6, :sym => TYPE_FOLLOWS }, |
|
59 |
:order => 6, :sym => TYPE_FOLLOWS, |
|
60 |
:landscape_margin => 16 }, |
|
59 | 61 |
TYPE_FOLLOWS => { :name => :label_follows, :sym_name => :label_precedes, |
60 | 62 |
:order => 7, :sym => TYPE_PRECEDES, :reverse => TYPE_PRECEDES }, |
61 | 63 |
TYPE_COPIED_TO => { :name => :label_copied_to, :sym_name => :label_copied_from, |
... | ... | |
64 | 66 |
:order => 9, :sym => TYPE_COPIED_TO, :reverse => TYPE_COPIED_TO } |
65 | 67 |
}.freeze |
66 | 68 | |
69 |
DRAW_TYPES = { |
|
70 |
TYPE_BLOCKS => TYPES[TYPE_BLOCKS], |
|
71 |
TYPE_BLOCKED => TYPES[TYPE_BLOCKED], |
|
72 |
TYPE_PRECEDES => TYPES[TYPE_PRECEDES], |
|
73 |
TYPE_FOLLOWS => TYPES[TYPE_FOLLOWS], |
|
74 |
}.freeze |
|
75 | ||
76 |
DRAW_TYPES_JSON = DRAW_TYPES.to_json |
|
77 | ||
67 | 78 |
validates_presence_of :issue_from, :issue_to, :relation_type |
68 | 79 |
validates_inclusion_of :relation_type, :in => TYPES.keys |
69 | 80 |
validates_numericality_of :delay, :allow_nil => true |
app/views/gantts/show.html.erb | ||
---|---|---|
3 | 3 |
<%= javascript_include_tag 'gantt' %> |
4 | 4 |
<% end %> |
5 | 5 |
<%= javascript_tag do %> |
6 |
var issue_relation_type = <%= IssueRelation::DRAW_TYPES_JSON.html_safe %>; |
|
6 | 7 |
$(document).ready(drawGanttHandler); |
7 | 8 |
$(window).resize(drawGanttHandler); |
9 |
$(function() { |
|
10 |
$("#draw_rels").change(drawGanttHandler); |
|
11 |
}); |
|
8 | 12 |
<% end %> |
9 | 13 |
<% @gantt.view = self %> |
10 | 14 |
<h2><%= @query.new_record? ? l(:label_gantt) : h(@query.name) %></h2> |
... | ... | |
20 | 24 |
<%= render :partial => 'queries/filters', :locals => {:query => @query} %> |
21 | 25 |
</div> |
22 | 26 |
</fieldset> |
27 |
<fieldset id="filters" class="collapsible"> |
|
28 |
<legend onclick="toggleFieldset(this);"><%= l(:label_display) %></legend> |
|
29 |
<div> |
|
30 |
<fieldset> |
|
31 |
<legend><%= l(:label_related_issues) %></legend> |
|
32 |
<label> |
|
33 |
<%= check_box_tag "draw_rels", 0, params["draw_rels"] %> |
|
34 |
<% rels = [IssueRelation::TYPE_BLOCKS, IssueRelation::TYPE_PRECEDES] %> |
|
35 |
<% rels.each do |rel| %> |
|
36 |
<%= content_tag(:span, ' '.html_safe, |
|
37 |
:id => "gantt_draw_rel_color_#{rel}") %> |
|
38 |
<%= l(IssueRelation::TYPES[rel][:name]) %> |
|
39 |
<% end %> |
|
40 |
</label> |
|
41 |
</fieldset> |
|
42 |
</div> |
|
43 |
</fieldset> |
|
23 | 44 | |
24 | 45 |
<p class="contextual"> |
25 | 46 |
<%= gantt_zoom_link(@gantt, :in) %> |
lib/redmine/helpers/gantt.rb | ||
---|---|---|
705 | 705 |
params[:image].text(params[:indent], params[:top] + 2, subject) |
706 | 706 |
end |
707 | 707 | |
708 |
def issue_relations(issue) |
|
709 |
relations = {} |
|
710 |
issue.relations_to.each do |relation| |
|
711 |
relation_type = relation.relation_type_for(relation.issue_to) |
|
712 |
if !IssueRelation::DRAW_TYPES[relation_type].nil? |
|
713 |
(relations[relation_type] ||= []) << relation.issue_from_id |
|
714 |
end |
|
715 |
end |
|
716 |
relations |
|
717 |
end |
|
718 | ||
708 | 719 |
def html_task(params, coords, options={}) |
709 | 720 |
output = '' |
710 | 721 |
# Renders the task bar, with progress and late |
... | ... | |
714 | 725 |
style << "top:#{params[:top]}px;" |
715 | 726 |
style << "left:#{coords[:bar_start]}px;" |
716 | 727 |
style << "width:#{width}px;" |
717 |
output << view.content_tag(:div, ' '.html_safe, |
|
718 |
:style => style, |
|
719 |
:class => "#{options[:css]} task_todo") |
|
728 |
html_id = "task-todo-issue-#{options[:issue].id}" if options[:issue] |
|
729 |
content_opt = {:style => style, |
|
730 |
:class => "#{options[:css]} task_todo", |
|
731 |
:id => html_id} |
|
732 |
if options[:issue] |
|
733 |
rels_hash = {} |
|
734 |
issue_relations(options[:issue]).each do |k, v| |
|
735 |
rels_hash[k] = v.join(',') |
|
736 |
end |
|
737 |
content_opt[:data] = {"rels" => rels_hash} |
|
738 |
end |
|
739 |
output << view.content_tag(:div, ' '.html_safe, content_opt) |
|
720 | 740 |
if coords[:bar_late_end] |
721 | 741 |
width = coords[:bar_late_end] - coords[:bar_start] - 2 |
722 | 742 |
style = "" |
public/javascripts/gantt.js | ||
---|---|---|
9 | 9 |
draw_left = $("#gantt_area").scrollLeft(); |
10 | 10 |
} |
11 | 11 | |
12 |
function getRelationsArray() { |
|
13 |
var arr = new Array(); |
|
14 |
$.each($('div.task_todo'), function(index_div, element) { |
|
15 |
var element_id = $(element).attr("id"); |
|
16 |
if (element_id != null) { |
|
17 |
var issue_id = element_id.replace("task-todo-issue-", ""); |
|
18 |
var data_rels = $(element).data("rels"); |
|
19 |
if (data_rels != null) { |
|
20 |
for (rel_type_key in issue_relation_type) { |
|
21 |
if (rel_type_key in data_rels) { |
|
22 |
var issue_arr = data_rels[rel_type_key].toString().split(","); |
|
23 |
if ("reverse" in issue_relation_type[rel_type_key]) { |
|
24 |
$.each(issue_arr, function(index_issue, element_issue) { |
|
25 |
arr.push({issue_from: element_issue, issue_to: issue_id, |
|
26 |
rel_type: issue_relation_type[rel_type_key]["reverse"]}); |
|
27 |
}); |
|
28 |
} else { |
|
29 |
$.each(issue_arr, function(index_issue, element_issue) { |
|
30 |
arr.push({issue_from: issue_id, issue_to: element_issue, |
|
31 |
rel_type: rel_type_key}); |
|
32 |
}); |
|
33 |
} |
|
34 |
} |
|
35 |
} |
|
36 |
} |
|
37 |
} |
|
38 |
}); |
|
39 |
return arr; |
|
40 |
} |
|
41 | ||
42 |
function drawRelations() { |
|
43 |
var arr = getRelationsArray(); |
|
44 |
$.each(arr, function(index_issue, element_issue) { |
|
45 |
var issue_from = $("#task-todo-issue-" + element_issue["issue_from"]); |
|
46 |
var issue_to = $("#task-todo-issue-" + element_issue["issue_to"]); |
|
47 |
if (issue_from.size() == 0 || issue_to.size() == 0) { |
|
48 |
return; |
|
49 |
} |
|
50 |
var issue_height = issue_from.height(); |
|
51 |
var issue_from_top = issue_from.position().top + (issue_height / 2) - draw_top; |
|
52 |
var issue_from_right = issue_from.position().left + issue_from.width(); |
|
53 |
var issue_to_top = issue_to.position().top + (issue_height / 2) - draw_top; |
|
54 |
var issue_to_left = issue_to.position().left; |
|
55 |
var color = $("#gantt_draw_rel_color_" + element_issue["rel_type"]) |
|
56 |
.css("background-color"); |
|
57 |
var landscape_margin = issue_relation_type[element_issue["rel_type"]]["landscape_margin"]; |
|
58 |
var issue_from_right_rel = issue_from_right + landscape_margin; |
|
59 |
var issue_to_left_rel = issue_to_left - landscape_margin; |
|
60 |
draw_gantt.path(["M", issue_from_right + draw_left, issue_from_top, |
|
61 |
"L", issue_from_right_rel + draw_left, issue_from_top]) |
|
62 |
.attr({stroke: color, "stroke-width": 2}); |
|
63 |
if (issue_from_right_rel < issue_to_left_rel) { |
|
64 |
draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_from_top, |
|
65 |
"L", issue_from_right_rel + draw_left, issue_to_top]) |
|
66 |
.attr({stroke: color, "stroke-width": 2}); |
|
67 |
draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_to_top, |
|
68 |
"L", issue_to_left + draw_left, issue_to_top]) |
|
69 |
.attr({stroke: color, |
|
70 |
"stroke-width": 2, |
|
71 |
"stroke-linecap": "butt", |
|
72 |
"stroke-linejoin": "miter", |
|
73 |
}); |
|
74 |
} else { |
|
75 |
var issue_middle_top = issue_to_top + |
|
76 |
(issue_height * |
|
77 |
((issue_from_top > issue_to_top) ? 1 : -1)); |
|
78 |
draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_from_top, |
|
79 |
"L", issue_from_right_rel + draw_left, issue_middle_top]) |
|
80 |
.attr({stroke: color, "stroke-width": 2}); |
|
81 |
draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_middle_top, |
|
82 |
"L", issue_to_left_rel + draw_left, issue_middle_top]) |
|
83 |
.attr({stroke:color, "stroke-width": 2}); |
|
84 |
draw_gantt.path(["M", issue_to_left_rel + draw_left, issue_middle_top, |
|
85 |
"L", issue_to_left_rel + draw_left, issue_to_top]) |
|
86 |
.attr({stroke: color, "stroke-width": 2}); |
|
87 |
draw_gantt.path(["M", issue_to_left_rel + draw_left, issue_to_top, |
|
88 |
"L", issue_to_left + draw_left, issue_to_top]) |
|
89 |
.attr({stroke: color, |
|
90 |
"stroke-width": 2, |
|
91 |
"stroke-linecap": "butt", |
|
92 |
"stroke-linejoin": "miter", |
|
93 |
}); |
|
94 |
} |
|
95 |
draw_gantt.path(["M", issue_to_left + draw_left, issue_to_top, |
|
96 |
"l", -8, -4, "l", 0, 8, "z"]) |
|
97 |
.attr({stroke: "none", |
|
98 |
fill: color, |
|
99 |
"stroke-linecap": "butt", |
|
100 |
"stroke-linejoin": "miter", |
|
101 |
}); |
|
102 |
}); |
|
103 |
} |
|
104 | ||
12 | 105 |
function drawGanttHandler() { |
13 | 106 |
var folder = document.getElementById('gantt_draw_area'); |
14 | 107 |
if(draw_gantt != null) |
... | ... | |
16 | 109 |
else |
17 | 110 |
draw_gantt = Raphael(folder); |
18 | 111 |
setDrawArea(); |
112 |
if ($("#draw_rels").attr('checked')) |
|
113 |
drawRelations(); |
|
19 | 114 |
} |
public/stylesheets/application.css | ||
---|---|---|
879 | 879 |
a.close-icon:hover {background-image:url('../images/close_hl.png');} |
880 | 880 | |
881 | 881 |
/***** Gantt chart *****/ |
882 |
#gantt_draw_rel_color_blocks {background-color:#fb7d2f;} |
|
883 |
#gantt_draw_rel_color_precedes {background-color:#df347c;} |
|
884 | ||
882 | 885 |
.gantt_hdr { |
883 | 886 |
position:absolute; |
884 | 887 |
top:0; |