Feature #3436 » gantt-relations-r10973-V2.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/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::TYPES.to_json.html_safe %>; |
|
| 6 | 7 |
$(document).ready(drawGanttHandler); |
| 7 | 8 |
$(window).resize(drawGanttHandler); |
| 9 |
$(function() {
|
|
| 10 |
$.each($("#draw_rels").children("label").children("input"), function(index, element) {
|
|
| 11 |
$(element).change(drawGanttHandler); |
|
| 12 |
}); |
|
| 13 |
}); |
|
| 8 | 14 |
<% end %> |
| 9 | 15 |
<% @gantt.view = self %> |
| 10 | 16 |
<h2><%= @query.new_record? ? l(:label_gantt) : h(@query.name) %></h2> |
| ... | ... | |
| 20 | 26 |
<%= render :partial => 'queries/filters', :locals => {:query => @query} %>
|
| 21 | 27 |
</div> |
| 22 | 28 |
</fieldset> |
| 29 |
<fieldset id="filters" class="collapsible"> |
|
| 30 |
<legend onclick="toggleFieldset(this);"><%= l(:label_display) %></legend> |
|
| 31 |
<div> |
|
| 32 |
<fieldset id = "draw_rels"> |
|
| 33 |
<legend><%= l(:label_related_issues) %></legend> |
|
| 34 |
<% |
|
| 35 |
rels = IssueRelation::TYPES.to_a.select {|rel|
|
|
| 36 |
rel[1][:reverse].nil? |
|
| 37 |
} |
|
| 38 |
rels_sorted = rels.sort {|x, y|
|
|
| 39 |
x[1][:order] <=> y[1][:order] |
|
| 40 |
}.collect{|v| [v[0], l(v[1][:name])]}
|
|
| 41 |
%> |
|
| 42 |
<% rels_sorted.each do |rel| %> |
|
| 43 |
<label> |
|
| 44 |
<%= check_box_tag "draw_#{rel[0]}", 0, params["draw_#{rel[0]}"] %>
|
|
| 45 |
<%= content_tag(:span, ' '.html_safe, |
|
| 46 |
:id => "gantt_draw_rel_color_#{rel[0]}") %>
|
|
| 47 |
<%= rel[1] %> |
|
| 48 |
</label> |
|
| 49 |
<% end %> |
|
| 50 |
</fieldset> |
|
| 51 |
</div> |
|
| 52 |
</fieldset> |
|
| 23 | 53 | |
| 24 | 54 |
<p class="contextual"> |
| 25 | 55 |
<%= 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 |
(relations[relation_type] ||= []) << relation.issue_from_id |
|
| 713 |
end |
|
| 714 |
relations |
|
| 715 |
end |
|
| 716 | ||
| 708 | 717 |
def html_task(params, coords, options={})
|
| 709 | 718 |
output = '' |
| 710 | 719 |
# Renders the task bar, with progress and late |
| ... | ... | |
| 714 | 723 |
style << "top:#{params[:top]}px;"
|
| 715 | 724 |
style << "left:#{coords[:bar_start]}px;"
|
| 716 | 725 |
style << "width:#{width}px;"
|
| 717 |
output << view.content_tag(:div, ' '.html_safe, |
|
| 718 |
:style => style, |
|
| 719 |
:class => "#{options[:css]} task_todo")
|
|
| 726 |
html_id = "task-todo-issue-#{options[:issue].id}" if options[:issue]
|
|
| 727 |
content_opt = {:style => style,
|
|
| 728 |
:class => "#{options[:css]} task_todo",
|
|
| 729 |
:id => html_id} |
|
| 730 |
if options[:issue] |
|
| 731 |
rels_hash = {}
|
|
| 732 |
issue_relations(options[:issue]).each do |k, v| |
|
| 733 |
rels_hash[k] = v.join(',')
|
|
| 734 |
end |
|
| 735 |
content_opt[:data] = {"rels" => rels_hash}
|
|
| 736 |
end |
|
| 737 |
output << view.content_tag(:div, ' '.html_safe, content_opt) |
|
| 720 | 738 |
if coords[:bar_late_end] |
| 721 | 739 |
width = coords[:bar_late_end] - coords[:bar_start] - 2 |
| 722 | 740 |
style = "" |
| public/javascripts/gantt.js | ||
|---|---|---|
| 3 | 3 |
var draw_right; |
| 4 | 4 |
var draw_left; |
| 5 | 5 | |
| 6 |
var draw_relations = {
|
|
| 7 |
"relates": {
|
|
| 8 |
"landscape_margin": 8, |
|
| 9 |
"no_arrow": true, |
|
| 10 |
}, |
|
| 11 |
"duplicates": {
|
|
| 12 |
"landscape_margin": 12, |
|
| 13 |
}, |
|
| 14 |
"blocks": {
|
|
| 15 |
"landscape_margin": 16, |
|
| 16 |
}, |
|
| 17 |
"precedes": {
|
|
| 18 |
"landscape_margin": 20, |
|
| 19 |
}, |
|
| 20 |
"copied_to": {
|
|
| 21 |
"landscape_margin": 24, |
|
| 22 |
}, |
|
| 23 |
}; |
|
| 24 | ||
| 6 | 25 |
function setDrawArea() {
|
| 7 | 26 |
draw_top = $("#gantt_draw_area").position().top;
|
| 8 | 27 |
draw_right = $("#gantt_draw_area").width();
|
| 9 | 28 |
draw_left = $("#gantt_area").scrollLeft();
|
| 10 | 29 |
} |
| 11 | 30 | |
| 31 |
function getRelationsArray() {
|
|
| 32 |
var arr = new Array(); |
|
| 33 |
$.each($('div.task_todo'), function(index_div, element) {
|
|
| 34 |
var element_id = $(element).attr("id");
|
|
| 35 |
if (element_id != null) {
|
|
| 36 |
var issue_id = element_id.replace("task-todo-issue-", "");
|
|
| 37 |
var data_rels = $(element).data("rels");
|
|
| 38 |
if (data_rels != null) {
|
|
| 39 |
for (rel_type_key in issue_relation_type) {
|
|
| 40 |
if (rel_type_key in data_rels) {
|
|
| 41 |
var issue_arr = data_rels[rel_type_key].toString().split(",");
|
|
| 42 |
if ("reverse" in issue_relation_type[rel_type_key]) {
|
|
| 43 |
$.each(issue_arr, function(index_issue, element_issue) {
|
|
| 44 |
arr.push({issue_from: element_issue, issue_to: issue_id,
|
|
| 45 |
rel_type: issue_relation_type[rel_type_key]["reverse"]}); |
|
| 46 |
}); |
|
| 47 |
} else {
|
|
| 48 |
$.each(issue_arr, function(index_issue, element_issue) {
|
|
| 49 |
arr.push({issue_from: issue_id, issue_to: element_issue,
|
|
| 50 |
rel_type: rel_type_key}); |
|
| 51 |
}); |
|
| 52 |
} |
|
| 53 |
} |
|
| 54 |
} |
|
| 55 |
} |
|
| 56 |
} |
|
| 57 |
}); |
|
| 58 |
return arr; |
|
| 59 |
} |
|
| 60 | ||
| 61 |
function drawRelations() {
|
|
| 62 |
var arr = getRelationsArray(); |
|
| 63 |
$.each(arr, function(index_issue, element_issue) {
|
|
| 64 |
if (!$("#draw_" + element_issue["rel_type"]).attr('checked')) {
|
|
| 65 |
return; |
|
| 66 |
} |
|
| 67 |
var issue_from = $("#task-todo-issue-" + element_issue["issue_from"]);
|
|
| 68 |
var issue_to = $("#task-todo-issue-" + element_issue["issue_to"]);
|
|
| 69 |
if (issue_from.size() == 0 || issue_to.size() == 0) {
|
|
| 70 |
return; |
|
| 71 |
} |
|
| 72 |
var issue_height = issue_from.height(); |
|
| 73 |
var issue_from_top = issue_from.position().top + (issue_height / 2) - draw_top; |
|
| 74 |
var issue_from_right = issue_from.position().left + issue_from.width(); |
|
| 75 |
var issue_to_top = issue_to.position().top + (issue_height / 2) - draw_top; |
|
| 76 |
var issue_to_left = issue_to.position().left; |
|
| 77 |
var color = $("#gantt_draw_rel_color_" + element_issue["rel_type"])
|
|
| 78 |
.css("background-color");
|
|
| 79 |
var landscape_margin = draw_relations[element_issue["rel_type"]]["landscape_margin"]; |
|
| 80 |
var issue_from_right_rel = issue_from_right + landscape_margin; |
|
| 81 |
var issue_to_left_rel = issue_to_left - landscape_margin; |
|
| 82 |
draw_gantt.path(["M", issue_from_right + draw_left, issue_from_top, |
|
| 83 |
"L", issue_from_right_rel + draw_left, issue_from_top]) |
|
| 84 |
.attr({stroke: color, "stroke-width": 2});
|
|
| 85 |
if (issue_from_right_rel < issue_to_left_rel) {
|
|
| 86 |
draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_from_top, |
|
| 87 |
"L", issue_from_right_rel + draw_left, issue_to_top]) |
|
| 88 |
.attr({stroke: color, "stroke-width": 2});
|
|
| 89 |
draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_to_top, |
|
| 90 |
"L", issue_to_left + draw_left, issue_to_top]) |
|
| 91 |
.attr({stroke: color,
|
|
| 92 |
"stroke-width": 2, |
|
| 93 |
"stroke-linecap": "butt", |
|
| 94 |
"stroke-linejoin": "miter", |
|
| 95 |
}); |
|
| 96 |
} else {
|
|
| 97 |
var issue_middle_top = issue_to_top + |
|
| 98 |
(issue_height * |
|
| 99 |
((issue_from_top > issue_to_top) ? 1 : -1)); |
|
| 100 |
draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_from_top, |
|
| 101 |
"L", issue_from_right_rel + draw_left, issue_middle_top]) |
|
| 102 |
.attr({stroke: color, "stroke-width": 2});
|
|
| 103 |
draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_middle_top, |
|
| 104 |
"L", issue_to_left_rel + draw_left, issue_middle_top]) |
|
| 105 |
.attr({stroke:color, "stroke-width": 2});
|
|
| 106 |
draw_gantt.path(["M", issue_to_left_rel + draw_left, issue_middle_top, |
|
| 107 |
"L", issue_to_left_rel + draw_left, issue_to_top]) |
|
| 108 |
.attr({stroke: color, "stroke-width": 2});
|
|
| 109 |
draw_gantt.path(["M", issue_to_left_rel + draw_left, issue_to_top, |
|
| 110 |
"L", issue_to_left + draw_left, issue_to_top]) |
|
| 111 |
.attr({stroke: color,
|
|
| 112 |
"stroke-width": 2, |
|
| 113 |
"stroke-linecap": "butt", |
|
| 114 |
"stroke-linejoin": "miter", |
|
| 115 |
}); |
|
| 116 |
} |
|
| 117 |
if (!("no_arrow" in draw_relations[element_issue["rel_type"]])) {
|
|
| 118 |
draw_gantt.path(["M", issue_to_left + draw_left, issue_to_top, |
|
| 119 |
"l", -8, -4, "l", 0, 8, "z"]) |
|
| 120 |
.attr({stroke: "none",
|
|
| 121 |
fill: color, |
|
| 122 |
"stroke-linecap": "butt", |
|
| 123 |
"stroke-linejoin": "miter", |
|
| 124 |
}); |
|
| 125 |
} |
|
| 126 |
}); |
|
| 127 |
} |
|
| 128 | ||
| 12 | 129 |
function drawGanttHandler() {
|
| 13 | 130 |
var folder = document.getElementById('gantt_draw_area');
|
| 14 | 131 |
if(draw_gantt != null) |
| ... | ... | |
| 16 | 133 |
else |
| 17 | 134 |
draw_gantt = Raphael(folder); |
| 18 | 135 |
setDrawArea(); |
| 136 |
drawRelations(); |
|
| 19 | 137 |
} |
| public/stylesheets/application.css | ||
|---|---|---|
| 872 | 872 |
a.close-icon:hover {background-image:url('../images/close_hl.png');}
|
| 873 | 873 | |
| 874 | 874 |
/***** Gantt chart *****/ |
| 875 |
#gantt_draw_rel_color_relates {background-color:#00c551;}
|
|
| 876 |
#gantt_draw_rel_color_duplicates {background-color:#4f52c3;}
|
|
| 877 |
#gantt_draw_rel_color_blocks {background-color:#fb7d2f;}
|
|
| 878 |
#gantt_draw_rel_color_precedes {background-color:#df347c;}
|
|
| 879 |
#gantt_draw_rel_color_copied_to {background-color:#00c5c2;}
|
|
| 880 | ||
| 875 | 881 |
.gantt_hdr {
|
| 876 | 882 |
position:absolute; |
| 877 | 883 |
top:0; |