Patch #43397 » 0002-Extract-Gantt-view-structure-and-wire-Stimulus-controllers.patch
| app/assets/javascripts/gantt.js | ||
|---|---|---|
| 1 |
/** |
|
| 2 |
* Redmine - project management software |
|
| 3 |
* Copyright (C) 2006- Jean-Philippe Lang |
|
| 4 |
* This code is released under the GNU General Public License. |
|
| 5 |
*/ |
|
| 6 | ||
| 7 |
var draw_gantt = null; |
|
| 8 |
var draw_top; |
|
| 9 |
var draw_right; |
|
| 10 |
var draw_left; |
|
| 11 | ||
| 12 |
var rels_stroke_width = 2; |
|
| 13 | ||
| 14 |
function setDrawArea() {
|
|
| 15 |
draw_top = $("#gantt_draw_area").position().top;
|
|
| 16 |
draw_right = $("#gantt_draw_area").width();
|
|
| 17 |
draw_left = $("#gantt_area").scrollLeft();
|
|
| 18 |
} |
|
| 19 | ||
| 20 |
function getRelationsArray() {
|
|
| 21 |
var arr = new Array(); |
|
| 22 |
$.each($('div.task_todo[data-rels]'), function(index_div, element) {
|
|
| 23 |
if(!$(element).is(':visible')) return true;
|
|
| 24 |
var element_id = $(element).attr("id");
|
|
| 25 |
if (element_id != null) {
|
|
| 26 |
var issue_id = element_id.replace("task-todo-issue-", "");
|
|
| 27 |
var data_rels = $(element).data("rels");
|
|
| 28 |
for (rel_type_key in data_rels) {
|
|
| 29 |
$.each(data_rels[rel_type_key], 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 |
return arr; |
|
| 37 |
} |
|
| 38 | ||
| 39 |
function drawRelations() {
|
|
| 40 |
var arr = getRelationsArray(); |
|
| 41 |
$.each(arr, function(index_issue, element_issue) {
|
|
| 42 |
var issue_from = $("#task-todo-issue-" + element_issue["issue_from"]);
|
|
| 43 |
var issue_to = $("#task-todo-issue-" + element_issue["issue_to"]);
|
|
| 44 |
if (issue_from.length == 0 || issue_to.length == 0) {
|
|
| 45 |
return; |
|
| 46 |
} |
|
| 47 |
var issue_height = issue_from.height(); |
|
| 48 |
var issue_from_top = issue_from.position().top + (issue_height / 2) - draw_top; |
|
| 49 |
var issue_from_right = issue_from.position().left + issue_from.width(); |
|
| 50 |
var issue_to_top = issue_to.position().top + (issue_height / 2) - draw_top; |
|
| 51 |
var issue_to_left = issue_to.position().left; |
|
| 52 |
var color = issue_relation_type[element_issue["rel_type"]]["color"]; |
|
| 53 |
var landscape_margin = issue_relation_type[element_issue["rel_type"]]["landscape_margin"]; |
|
| 54 |
var issue_from_right_rel = issue_from_right + landscape_margin; |
|
| 55 |
var issue_to_left_rel = issue_to_left - landscape_margin; |
|
| 56 |
draw_gantt.path(["M", issue_from_right + draw_left, issue_from_top, |
|
| 57 |
"L", issue_from_right_rel + draw_left, issue_from_top]) |
|
| 58 |
.attr({stroke: color,
|
|
| 59 |
"stroke-width": rels_stroke_width |
|
| 60 |
}); |
|
| 61 |
if (issue_from_right_rel < issue_to_left_rel) {
|
|
| 62 |
draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_from_top, |
|
| 63 |
"L", issue_from_right_rel + draw_left, issue_to_top]) |
|
| 64 |
.attr({stroke: color,
|
|
| 65 |
"stroke-width": rels_stroke_width |
|
| 66 |
}); |
|
| 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": rels_stroke_width |
|
| 71 |
}); |
|
| 72 |
} else {
|
|
| 73 |
var issue_middle_top = issue_to_top + |
|
| 74 |
(issue_height * |
|
| 75 |
((issue_from_top > issue_to_top) ? 1 : -1)); |
|
| 76 |
draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_from_top, |
|
| 77 |
"L", issue_from_right_rel + draw_left, issue_middle_top]) |
|
| 78 |
.attr({stroke: color,
|
|
| 79 |
"stroke-width": rels_stroke_width |
|
| 80 |
}); |
|
| 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,
|
|
| 84 |
"stroke-width": rels_stroke_width |
|
| 85 |
}); |
|
| 86 |
draw_gantt.path(["M", issue_to_left_rel + draw_left, issue_middle_top, |
|
| 87 |
"L", issue_to_left_rel + draw_left, issue_to_top]) |
|
| 88 |
.attr({stroke: color,
|
|
| 89 |
"stroke-width": rels_stroke_width |
|
| 90 |
}); |
|
| 91 |
draw_gantt.path(["M", issue_to_left_rel + draw_left, issue_to_top, |
|
| 92 |
"L", issue_to_left + draw_left, issue_to_top]) |
|
| 93 |
.attr({stroke: color,
|
|
| 94 |
"stroke-width": rels_stroke_width |
|
| 95 |
}); |
|
| 96 |
} |
|
| 97 |
draw_gantt.path(["M", issue_to_left + draw_left, issue_to_top, |
|
| 98 |
"l", -4 * rels_stroke_width, -2 * rels_stroke_width, |
|
| 99 |
"l", 0, 4 * rels_stroke_width, "z"]) |
|
| 100 |
.attr({stroke: "none",
|
|
| 101 |
fill: color, |
|
| 102 |
"stroke-linecap": "butt", |
|
| 103 |
"stroke-linejoin": "miter" |
|
| 104 |
}); |
|
| 105 |
}); |
|
| 106 |
} |
|
| 107 | ||
| 108 |
function getProgressLinesArray() {
|
|
| 109 |
var arr = new Array(); |
|
| 110 |
var today_left = $('#today_line').position().left;
|
|
| 111 |
arr.push({left: today_left, top: 0});
|
|
| 112 |
$.each($('div.issue-subject, div.version-name'), function(index, element) {
|
|
| 113 |
if(!$(element).is(':visible')) return true;
|
|
| 114 |
var t = $(element).position().top - draw_top ; |
|
| 115 |
var h = ($(element).height() / 9); |
|
| 116 |
var element_top_upper = t - h; |
|
| 117 |
var element_top_center = t + (h * 3); |
|
| 118 |
var element_top_lower = t + (h * 8); |
|
| 119 |
var issue_closed = $(element).children('span').hasClass('issue-closed');
|
|
| 120 |
var version_closed = $(element).children('span').hasClass('version-closed');
|
|
| 121 |
if (issue_closed || version_closed) {
|
|
| 122 |
arr.push({left: today_left, top: element_top_center});
|
|
| 123 |
} else {
|
|
| 124 |
var issue_done = $("#task-done-" + $(element).attr("id"));
|
|
| 125 |
var is_behind_start = $(element).children('span').hasClass('behind-start-date');
|
|
| 126 |
var is_over_end = $(element).children('span').hasClass('over-end-date');
|
|
| 127 |
if (is_over_end) {
|
|
| 128 |
arr.push({left: draw_right, top: element_top_upper, is_right_edge: true});
|
|
| 129 |
arr.push({left: draw_right, top: element_top_lower, is_right_edge: true, none_stroke: true});
|
|
| 130 |
} else if (issue_done.length > 0) {
|
|
| 131 |
var done_left = issue_done.first().position().left + |
|
| 132 |
issue_done.first().width(); |
|
| 133 |
arr.push({left: done_left, top: element_top_center});
|
|
| 134 |
} else if (is_behind_start) {
|
|
| 135 |
arr.push({left: 0 , top: element_top_upper, is_left_edge: true});
|
|
| 136 |
arr.push({left: 0 , top: element_top_lower, is_left_edge: true, none_stroke: true});
|
|
| 137 |
} else {
|
|
| 138 |
var todo_left = today_left; |
|
| 139 |
var issue_todo = $("#task-todo-" + $(element).attr("id"));
|
|
| 140 |
if (issue_todo.length > 0){
|
|
| 141 |
todo_left = issue_todo.first().position().left; |
|
| 142 |
} |
|
| 143 |
arr.push({left: Math.min(today_left, todo_left), top: element_top_center});
|
|
| 144 |
} |
|
| 145 |
} |
|
| 146 |
}); |
|
| 147 |
return arr; |
|
| 148 |
} |
|
| 149 | ||
| 150 |
function drawGanttProgressLines() {
|
|
| 151 |
var arr = getProgressLinesArray(); |
|
| 152 |
var color = $("#today_line")
|
|
| 153 |
.css("border-left-color");
|
|
| 154 |
var i; |
|
| 155 |
for(i = 1 ; i < arr.length ; i++) {
|
|
| 156 |
if (!("none_stroke" in arr[i]) &&
|
|
| 157 |
(!("is_right_edge" in arr[i - 1] && "is_right_edge" in arr[i]) &&
|
|
| 158 |
!("is_left_edge" in arr[i - 1] && "is_left_edge" in arr[i]))
|
|
| 159 |
) {
|
|
| 160 |
var x1 = (arr[i - 1].left == 0) ? 0 : arr[i - 1].left + draw_left; |
|
| 161 |
var x2 = (arr[i].left == 0) ? 0 : arr[i].left + draw_left; |
|
| 162 |
draw_gantt.path(["M", x1, arr[i - 1].top, |
|
| 163 |
"L", x2, arr[i].top]) |
|
| 164 |
.attr({stroke: color, "stroke-width": 2});
|
|
| 165 |
} |
|
| 166 |
} |
|
| 167 |
} |
|
| 168 | ||
| 169 |
function drawSelectedColumns(){
|
|
| 170 |
if ($("#draw_selected_columns").prop('checked')) {
|
|
| 171 |
if(isMobile()) {
|
|
| 172 |
$('td.gantt_selected_column').each(function(i) {
|
|
| 173 |
$(this).hide(); |
|
| 174 |
}); |
|
| 175 |
}else{
|
|
| 176 |
$('.gantt_subjects_container').addClass('draw_selected_columns');
|
|
| 177 |
$('td.gantt_selected_column').each(function() {
|
|
| 178 |
$(this).show(); |
|
| 179 |
var column_name = $(this).attr('id');
|
|
| 180 |
$(this).resizable({
|
|
| 181 |
zIndex: 30, |
|
| 182 |
alsoResize: '.gantt_' + column_name + '_container, .gantt_' + column_name + '_container > .gantt_hdr', |
|
| 183 |
minWidth: 20, |
|
| 184 |
handles: "e", |
|
| 185 |
create: function() {
|
|
| 186 |
$(".ui-resizable-e").css("cursor","ew-resize");
|
|
| 187 |
} |
|
| 188 |
}).on('resize', function (e) {
|
|
| 189 |
e.stopPropagation(); |
|
| 190 |
}); |
|
| 191 |
}); |
|
| 192 |
} |
|
| 193 |
}else{
|
|
| 194 |
$('td.gantt_selected_column').each(function (i) {
|
|
| 195 |
$(this).hide(); |
|
| 196 |
$('.gantt_subjects_container').removeClass('draw_selected_columns');
|
|
| 197 |
}); |
|
| 198 |
} |
|
| 199 |
} |
|
| 200 | ||
| 201 |
function drawGanttHandler() {
|
|
| 202 |
var folder = document.getElementById('gantt_draw_area');
|
|
| 203 |
if(draw_gantt != null) |
|
| 204 |
draw_gantt.clear(); |
|
| 205 |
else |
|
| 206 |
draw_gantt = Raphael(folder); |
|
| 207 |
setDrawArea(); |
|
| 208 |
drawSelectedColumns(); |
|
| 209 |
if ($("#draw_progress_line").prop('checked'))
|
|
| 210 |
try{drawGanttProgressLines();}catch(e){}
|
|
| 211 |
if ($("#draw_relations").prop('checked'))
|
|
| 212 |
drawRelations(); |
|
| 213 |
$('#content').addClass('gantt_content');
|
|
| 214 |
} |
|
| 215 | ||
| 216 |
function resizableSubjectColumn(){
|
|
| 217 |
$('.issue-subject, .project-name, .version-name').each(function(){
|
|
| 218 |
$(this).width($(".gantt_subjects_column").width()-$(this).position().left);
|
|
| 219 |
}); |
|
| 220 |
$('td.gantt_subjects_column').resizable({
|
|
| 221 |
alsoResize: '.gantt_subjects_container, .gantt_subjects_container>.gantt_hdr, .project-name, .issue-subject, .version-name', |
|
| 222 |
minWidth: 100, |
|
| 223 |
handles: 'e', |
|
| 224 |
zIndex: 30, |
|
| 225 |
create: function( event, ui ) {
|
|
| 226 |
$('.ui-resizable-e').css('cursor','ew-resize');
|
|
| 227 |
} |
|
| 228 |
}).on('resize', function (e) {
|
|
| 229 |
e.stopPropagation(); |
|
| 230 |
}); |
|
| 231 |
if(isMobile()) {
|
|
| 232 |
$('td.gantt_subjects_column').resizable('disable');
|
|
| 233 |
}else{
|
|
| 234 |
$('td.gantt_subjects_column').resizable('enable');
|
|
| 235 |
}; |
|
| 236 |
} |
|
| 237 | ||
| 238 |
ganttEntryClick = function(e){
|
|
| 239 |
var icon_expander = e.currentTarget; |
|
| 240 |
var subject = $(icon_expander.parentElement); |
|
| 241 |
var subject_left = parseInt(subject.css('left')) + parseInt(icon_expander.offsetWidth);
|
|
| 242 |
var target_shown = null; |
|
| 243 |
var target_top = 0; |
|
| 244 |
var total_height = 0; |
|
| 245 |
var out_of_hierarchy = false; |
|
| 246 |
var iconChange = null; |
|
| 247 |
if(subject.hasClass('open'))
|
|
| 248 |
iconChange = function(element){
|
|
| 249 |
var expander = $(element).find('.expander')
|
|
| 250 |
expander.switchClass('icon-expanded', 'icon-collapsed');
|
|
| 251 |
$(element).removeClass('open');
|
|
| 252 |
if (expander.find('svg').length === 1) {
|
|
| 253 |
updateSVGIcon(expander[0], 'angle-right') |
|
| 254 |
} |
|
| 255 |
}; |
|
| 256 |
else |
|
| 257 |
iconChange = function(element){
|
|
| 258 |
var expander = $(element).find('.expander')
|
|
| 259 |
expander.find('.expander').switchClass('icon-collapsed', 'icon-expanded');
|
|
| 260 |
$(element).addClass('open');
|
|
| 261 |
if (expander.find('svg').length === 1) {
|
|
| 262 |
updateSVGIcon(expander[0], 'angle-down') |
|
| 263 |
} |
|
| 264 |
}; |
|
| 265 |
iconChange(subject); |
|
| 266 |
subject.nextAll('div').each(function(_, element){
|
|
| 267 |
var el = $(element); |
|
| 268 |
var json = el.data('collapse-expand');
|
|
| 269 |
var number_of_rows = el.data('number-of-rows');
|
|
| 270 |
var el_task_bars = '#gantt_area form > div[data-collapse-expand="' + json.obj_id + '"][data-number-of-rows="' + number_of_rows + '"]'; |
|
| 271 |
var el_selected_columns = 'td.gantt_selected_column div[data-collapse-expand="' + json.obj_id + '"][data-number-of-rows="' + number_of_rows + '"]'; |
|
| 272 |
if(out_of_hierarchy || parseInt(el.css('left')) <= subject_left){
|
|
| 273 |
out_of_hierarchy = true; |
|
| 274 |
if(target_shown == null) return false; |
|
| 275 | ||
| 276 |
var new_top_val = parseInt(el.css('top')) + total_height * (target_shown ? -1 : 1);
|
|
| 277 |
el.css('top', new_top_val);
|
|
| 278 |
$([el_task_bars, el_selected_columns].join()).each(function(_, el){
|
|
| 279 |
$(el).css('top', new_top_val);
|
|
| 280 |
}); |
|
| 281 |
return true; |
|
| 282 |
} |
|
| 283 | ||
| 284 |
var is_shown = el.is(':visible');
|
|
| 285 |
if(target_shown == null){
|
|
| 286 |
target_shown = is_shown; |
|
| 287 |
target_top = parseInt(el.css('top'));
|
|
| 288 |
total_height = 0; |
|
| 289 |
} |
|
| 290 |
if(is_shown == target_shown){
|
|
| 291 |
$(el_task_bars).each(function(_, task) {
|
|
| 292 |
var el_task = $(task); |
|
| 293 |
if(!is_shown) |
|
| 294 |
el_task.css('top', target_top + total_height);
|
|
| 295 |
if(!el_task.hasClass('tooltip'))
|
|
| 296 |
el_task.toggle(!is_shown); |
|
| 297 |
}); |
|
| 298 |
$(el_selected_columns).each(function (_, attr) {
|
|
| 299 |
var el_attr = $(attr); |
|
| 300 |
if (!is_shown) |
|
| 301 |
el_attr.css('top', target_top + total_height);
|
|
| 302 |
el_attr.toggle(!is_shown); |
|
| 303 |
}); |
|
| 304 |
if(!is_shown) |
|
| 305 |
el.css('top', target_top + total_height);
|
|
| 306 |
iconChange(el); |
|
| 307 |
el.toggle(!is_shown); |
|
| 308 |
total_height += parseInt(json.top_increment); |
|
| 309 |
} |
|
| 310 |
}); |
|
| 311 |
drawGanttHandler(); |
|
| 312 |
}; |
|
| 313 | ||
| 314 |
function disable_unavailable_columns(unavailable_columns) {
|
|
| 315 |
$.each(unavailable_columns, function (index, value) {
|
|
| 316 |
$('#available_c, #selected_c').children("[value='" + value + "']").prop('disabled', true);
|
|
| 317 |
}); |
|
| 318 |
} |
|
| app/assets/stylesheets/application.css | ||
|---|---|---|
| 434 | 434 |
tr.entry.file td.filename a { margin-left: 26px; }
|
| 435 | 435 |
tr.entry.file td.filename_no_report a { margin-left: 16px; }
|
| 436 | 436 | |
| 437 |
tr span.expander, .gantt_subjects div > span.expander {margin-left: 0; cursor: pointer;}
|
|
| 438 |
.gantt_subjects .avatar {margin-right: 4px;}
|
|
| 439 |
.gantt_subjects div.project-name a, .gantt_subjects div.version-name a {margin-left: 4px;}
|
|
| 437 |
tr span.expander {margin-left: 0; cursor: pointer;}
|
|
| 440 | 438 | |
| 441 | 439 |
tr.changeset { height: 20px }
|
| 442 | 440 |
tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; }
|
| ... | ... | |
| 1705 | 1703 | |
| 1706 | 1704 |
#my-page .list th.checkbox, #my-page .list td.checkbox {display:none;}
|
| 1707 | 1705 | |
| 1708 |
/***** Gantt chart *****/ |
|
| 1709 |
table.gantt-table {
|
|
| 1710 |
width: 100%; |
|
| 1711 |
border-collapse: collapse; |
|
| 1712 |
} |
|
| 1713 |
table.gantt-table td {
|
|
| 1714 |
padding: 0px; |
|
| 1715 |
} |
|
| 1716 |
.gantt_hdr {
|
|
| 1717 |
position:absolute; |
|
| 1718 |
top:0; |
|
| 1719 |
height:16px; |
|
| 1720 |
border-top: 1px solid var(--oc-gray-4); |
|
| 1721 |
border-bottom: 1px solid var(--oc-gray-4); |
|
| 1722 |
border-left: 1px solid var(--oc-gray-4); |
|
| 1723 |
text-align: center; |
|
| 1724 |
overflow: hidden; |
|
| 1725 |
} |
|
| 1726 |
#gantt_area .gantt_hdr {
|
|
| 1727 |
border-left: 0px; |
|
| 1728 |
border-right: 1px solid var(--oc-gray-4); |
|
| 1729 |
} |
|
| 1730 |
.gantt_subjects_container:not(.draw_selected_columns) .gantt_hdr, |
|
| 1731 |
.last_gantt_selected_column .gantt_hdr {
|
|
| 1732 |
border-right: 1px solid var(--oc-gray-4); |
|
| 1733 |
} |
|
| 1734 |
.last_gantt_selected_column .gantt_selected_column_container, |
|
| 1735 |
.gantt_subjects_container .gantt_subjects * {
|
|
| 1736 |
z-index: 10; |
|
| 1737 |
} |
|
| 1738 | ||
| 1739 |
.gantt_subjects_column + td {
|
|
| 1740 |
padding: 0; |
|
| 1741 |
} |
|
| 1742 | ||
| 1743 |
.gantt_hdr.nwday {background-color:var(--oc-gray-1); color:var(--oc-gray-6);}
|
|
| 1744 | ||
| 1745 |
.gantt_subjects, |
|
| 1746 |
.gantt_selected_column_content.gantt_hdr {
|
|
| 1747 |
font-size: 0.8em; |
|
| 1748 |
position: relative; |
|
| 1749 |
z-index: 1; |
|
| 1750 |
} |
|
| 1751 |
.gantt_subjects div, |
|
| 1752 |
.gantt_selected_column_content div {
|
|
| 1753 |
line-height: 16px; |
|
| 1754 |
height: 16px; |
|
| 1755 |
overflow: hidden; |
|
| 1756 |
white-space: nowrap; |
|
| 1757 |
text-overflow: clip; |
|
| 1758 |
width: 100%; |
|
| 1759 |
} |
|
| 1760 |
.gantt_subjects div.issue-subject:hover { background-color:var(--oc-yellow-0); }
|
|
| 1761 |
.gantt_selected_column_content > div { padding-left: 3px; box-sizing: border-box; }
|
|
| 1762 | ||
| 1763 |
.gantt_hdr_selected_column_name {
|
|
| 1764 |
position: absolute; |
|
| 1765 |
top: 50%; |
|
| 1766 |
width:100%; |
|
| 1767 |
transform: translateY(-50%); |
|
| 1768 |
-webkit-transform: translateY(-50%); |
|
| 1769 |
font-size: 0.8em; |
|
| 1770 |
overflow: hidden; |
|
| 1771 |
text-overflow: ellipsis; |
|
| 1772 |
white-space: nowrap; |
|
| 1773 | ||
| 1774 |
} |
|
| 1775 |
td.gantt_selected_column {
|
|
| 1776 |
width: 50px; |
|
| 1777 |
} |
|
| 1778 |
td.gantt_selected_column .gantt_hdr,.gantt_selected_column_container {
|
|
| 1779 |
width: 49px; |
|
| 1780 |
} |
|
| 1781 | ||
| 1782 |
td.gantt_watcher_users_column div.issue_watcher_users ul {
|
|
| 1783 |
margin: 0; |
|
| 1784 |
padding: 0; |
|
| 1785 |
list-style: none; |
|
| 1786 |
} |
|
| 1787 | ||
| 1788 |
td.gantt_watcher_users_column div.issue_watcher_users ul li {
|
|
| 1789 |
display: inline; |
|
| 1790 |
} |
|
| 1791 | ||
| 1792 |
td.gantt_watcher_users_column div.issue_watcher_users ul li:not(:last-child)::after {
|
|
| 1793 |
content: ', '; |
|
| 1794 |
white-space: pre; |
|
| 1795 |
} |
|
| 1796 | ||
| 1797 |
.task {
|
|
| 1798 |
position: absolute; |
|
| 1799 |
height:8px; |
|
| 1800 |
font-size:0.8em; |
|
| 1801 |
color:var(--oc-gray-6); |
|
| 1802 |
padding:0; |
|
| 1803 |
margin:0; |
|
| 1804 |
line-height:16px; |
|
| 1805 |
white-space:nowrap; |
|
| 1806 |
} |
|
| 1807 | ||
| 1808 |
.task.label {width:100%;}
|
|
| 1809 |
.task.label.project, .task.label.version { font-weight: bold; }
|
|
| 1810 | ||
| 1811 |
.task_late { background:var(--oc-red-5) url(/task_late.png); border: 1px solid var(--oc-red-5); }
|
|
| 1812 |
.task_done { background:var(--oc-green-7) url(/task_done.png); border: 1px solid var(--oc-green-7); }
|
|
| 1813 |
.task_todo { background:var(--oc-gray-5) url(/task_todo.png); border: 1px solid var(--oc-gray-5); }
|
|
| 1814 | ||
| 1815 |
.task_todo.parent { background: var(--oc-gray-6); border: 1px solid var(--oc-gray-6); height: 3px;}
|
|
| 1816 |
.task_late.parent, .task_done.parent { height: 3px;}
|
|
| 1817 |
.task.parent.marker.starting { position: absolute; background: url(/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; left: 0px; top: -1px;}
|
|
| 1818 |
.task.parent.marker.ending { position: absolute; background: url(/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; right: 0px; top: -1px;}
|
|
| 1819 | ||
| 1820 |
.version.task_late { background:var(oc-red-5) url(/milestone_late.png); border: 1px solid var(oc-red-5); height: 2px; margin-top: 3px;}
|
|
| 1821 |
.version.task_done { background:var(--oc-green-7) url(/milestone_done.png); border: 1px solid var(--oc-green-7); height: 2px; margin-top: 3px;}
|
|
| 1822 |
.version.task_todo { background:var(--oc-white) url(/milestone_todo.png); border: 1px solid var(--oc-white); height: 2px; margin-top: 3px;}
|
|
| 1823 |
.version.marker { background-image:url(/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
|
|
| 1824 | ||
| 1825 |
.project.task_late { background:var(oc-red-5) url(/milestone_late.png); border: 1px solid var(oc-red-5); height: 2px; margin-top: 3px;}
|
|
| 1826 |
.project.task_done { background:var(--oc-green-7) url(/milestone_done.png); border: 1px solid var(--oc-green-7); height: 2px; margin-top: 3px;}
|
|
| 1827 |
.project.task_todo { background:var(--oc-white) url(/milestone_todo.png); border: 1px solid var(--oc-white); height: 2px; margin-top: 3px;}
|
|
| 1828 |
.project.marker { background-image:url(/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
|
|
| 1829 | ||
| 1830 |
.version-behind-schedule a, .issue-behind-schedule a {color: var(--oc-yellow-8);}
|
|
| 1831 |
.version-overdue a, .issue-overdue a, .project-overdue a {color: var(--oc-red-8);}
|
|
| 1832 | 1706 | |
| 1833 | 1707 |
/***** User events (ex: journal, notes, replies, comments) *****/ |
| 1834 | 1708 |
.journals h4.journal-header {
|
| app/assets/stylesheets/gantt.css | ||
|---|---|---|
| 1 |
/** |
|
| 2 |
* Redmine - project management software |
|
| 3 |
* Copyright (C) 2006- Jean-Philippe Lang |
|
| 4 |
* This code is released under the GNU General Public License. |
|
| 5 |
*/ |
|
| 6 | ||
| 7 |
.gantt_subjects div > span.expander {
|
|
| 8 |
margin-left: 0; |
|
| 9 |
cursor: pointer; |
|
| 10 |
} |
|
| 11 | ||
| 12 |
.gantt_subjects .avatar {
|
|
| 13 |
margin-right: 4px; |
|
| 14 |
} |
|
| 15 | ||
| 16 |
.gantt_subjects div.project-name a, |
|
| 17 |
.gantt_subjects div.version-name a {
|
|
| 18 |
margin-left: 4px; |
|
| 19 |
} |
|
| 20 | ||
| 21 |
/***** Gantt chart *****/ |
|
| 22 |
table.gantt-table {
|
|
| 23 |
width: 100%; |
|
| 24 |
border-collapse: collapse; |
|
| 25 |
} |
|
| 26 | ||
| 27 |
table.gantt-table td {
|
|
| 28 |
padding: 0; |
|
| 29 |
} |
|
| 30 | ||
| 31 |
.gantt_hdr {
|
|
| 32 |
position: absolute; |
|
| 33 |
top: 0; |
|
| 34 |
height: 16px; |
|
| 35 |
border-top: 1px solid var(--oc-gray-4); |
|
| 36 |
border-bottom: 1px solid var(--oc-gray-4); |
|
| 37 |
border-left: 1px solid var(--oc-gray-4); |
|
| 38 |
text-align: center; |
|
| 39 |
overflow: hidden; |
|
| 40 |
} |
|
| 41 | ||
| 42 |
#gantt_area .gantt_hdr {
|
|
| 43 |
border-left: 0; |
|
| 44 |
border-right: 1px solid var(--oc-gray-4); |
|
| 45 |
} |
|
| 46 | ||
| 47 |
.gantt_subjects_container:not(.draw_selected_columns) .gantt_hdr, |
|
| 48 |
.last_gantt_selected_column .gantt_hdr {
|
|
| 49 |
border-right: 1px solid var(--oc-gray-4); |
|
| 50 |
} |
|
| 51 | ||
| 52 |
.last_gantt_selected_column .gantt_selected_column_container, |
|
| 53 |
.gantt_subjects_container .gantt_subjects * {
|
|
| 54 |
z-index: 10; |
|
| 55 |
} |
|
| 56 | ||
| 57 |
.gantt_subjects_column + td {
|
|
| 58 |
padding: 0; |
|
| 59 |
} |
|
| 60 | ||
| 61 |
.gantt_hdr.nwday {
|
|
| 62 |
background-color: var(--oc-gray-1); |
|
| 63 |
color: var(--oc-gray-6); |
|
| 64 |
} |
|
| 65 | ||
| 66 |
.gantt_subjects, |
|
| 67 |
.gantt_selected_column_content.gantt_hdr {
|
|
| 68 |
font-size: 0.8em; |
|
| 69 |
position: relative; |
|
| 70 |
z-index: 1; |
|
| 71 |
} |
|
| 72 | ||
| 73 |
.gantt_subjects div, |
|
| 74 |
.gantt_selected_column_content div {
|
|
| 75 |
line-height: 16px; |
|
| 76 |
height: 16px; |
|
| 77 |
overflow: hidden; |
|
| 78 |
white-space: nowrap; |
|
| 79 |
text-overflow: clip; |
|
| 80 |
width: 100%; |
|
| 81 |
} |
|
| 82 | ||
| 83 |
.gantt_subjects div.issue-subject:hover {
|
|
| 84 |
background-color: var(--oc-yellow-0); |
|
| 85 |
} |
|
| 86 | ||
| 87 |
.gantt_selected_column_content > div {
|
|
| 88 |
padding-left: 3px; |
|
| 89 |
box-sizing: border-box; |
|
| 90 |
} |
|
| 91 | ||
| 92 |
.gantt_hdr_selected_column_name {
|
|
| 93 |
position: absolute; |
|
| 94 |
top: 50%; |
|
| 95 |
width: 100%; |
|
| 96 |
transform: translateY(-50%); |
|
| 97 |
-webkit-transform: translateY(-50%); |
|
| 98 |
font-size: 0.8em; |
|
| 99 |
overflow: hidden; |
|
| 100 |
text-overflow: ellipsis; |
|
| 101 |
white-space: nowrap; |
|
| 102 |
} |
|
| 103 | ||
| 104 |
td.gantt_selected_column {
|
|
| 105 |
width: 50px; |
|
| 106 |
} |
|
| 107 | ||
| 108 |
td.gantt_selected_column .gantt_hdr, |
|
| 109 |
.gantt_selected_column_container {
|
|
| 110 |
width: 49px; |
|
| 111 |
} |
|
| 112 | ||
| 113 |
td.gantt_watcher_users_column div.issue_watcher_users ul {
|
|
| 114 |
margin: 0; |
|
| 115 |
padding: 0; |
|
| 116 |
list-style: none; |
|
| 117 |
} |
|
| 118 | ||
| 119 |
td.gantt_watcher_users_column div.issue_watcher_users ul li {
|
|
| 120 |
display: inline; |
|
| 121 |
} |
|
| 122 | ||
| 123 |
td.gantt_watcher_users_column div.issue_watcher_users ul li:not(:last-child)::after {
|
|
| 124 |
content: ', '; |
|
| 125 |
white-space: pre; |
|
| 126 |
} |
|
| 127 | ||
| 128 |
.task {
|
|
| 129 |
position: absolute; |
|
| 130 |
height: 8px; |
|
| 131 |
font-size: 0.8em; |
|
| 132 |
color: var(--oc-gray-6); |
|
| 133 |
padding: 0; |
|
| 134 |
margin: 0; |
|
| 135 |
line-height: 16px; |
|
| 136 |
white-space: nowrap; |
|
| 137 |
} |
|
| 138 | ||
| 139 |
.task.label {
|
|
| 140 |
width: 100%; |
|
| 141 |
} |
|
| 142 | ||
| 143 |
.task.label.project, |
|
| 144 |
.task.label.version {
|
|
| 145 |
font-weight: bold; |
|
| 146 |
} |
|
| 147 | ||
| 148 |
.task_late {
|
|
| 149 |
background: var(--oc-red-5) url(/task_late.png); |
|
| 150 |
border: 1px solid var(--oc-red-5); |
|
| 151 |
} |
|
| 152 | ||
| 153 |
.task_done {
|
|
| 154 |
background: var(--oc-green-7) url(/task_done.png); |
|
| 155 |
border: 1px solid var(--oc-green-7); |
|
| 156 |
} |
|
| 157 | ||
| 158 |
.task_todo {
|
|
| 159 |
background: var(--oc-gray-5) url(/task_todo.png); |
|
| 160 |
border: 1px solid var(--oc-gray-5); |
|
| 161 |
} |
|
| 162 | ||
| 163 |
.task_todo.parent {
|
|
| 164 |
background: var(--oc-gray-6); |
|
| 165 |
border: 1px solid var(--oc-gray-6); |
|
| 166 |
height: 3px; |
|
| 167 |
} |
|
| 168 | ||
| 169 |
.task_late.parent, |
|
| 170 |
.task_done.parent {
|
|
| 171 |
height: 3px; |
|
| 172 |
} |
|
| 173 | ||
| 174 |
.task.parent.marker.starting {
|
|
| 175 |
position: absolute; |
|
| 176 |
background: url(/task_parent_end.png) no-repeat 0 0; |
|
| 177 |
width: 8px; |
|
| 178 |
height: 16px; |
|
| 179 |
margin-left: -4px; |
|
| 180 |
left: 0; |
|
| 181 |
top: -1px; |
|
| 182 |
} |
|
| 183 | ||
| 184 |
.task.parent.marker.ending {
|
|
| 185 |
position: absolute; |
|
| 186 |
background: url(/task_parent_end.png) no-repeat 0 0; |
|
| 187 |
width: 8px; |
|
| 188 |
height: 16px; |
|
| 189 |
margin-left: -4px; |
|
| 190 |
right: 0; |
|
| 191 |
top: -1px; |
|
| 192 |
} |
|
| 193 | ||
| 194 |
.version.task_late {
|
|
| 195 |
background: var(oc-red-5) url(/milestone_late.png); |
|
| 196 |
border: 1px solid var(oc-red-5); |
|
| 197 |
height: 2px; |
|
| 198 |
margin-top: 3px; |
|
| 199 |
} |
|
| 200 | ||
| 201 |
.version.task_done {
|
|
| 202 |
background: var(--oc-green-7) url(/milestone_done.png); |
|
| 203 |
border: 1px solid var(--oc-green-7); |
|
| 204 |
height: 2px; |
|
| 205 |
margin-top: 3px; |
|
| 206 |
} |
|
| 207 | ||
| 208 |
.version.task_todo {
|
|
| 209 |
background: var(--oc-white) url(/milestone_todo.png); |
|
| 210 |
border: 1px solid var(--oc-white); |
|
| 211 |
height: 2px; |
|
| 212 |
margin-top: 3px; |
|
| 213 |
} |
|
| 214 | ||
| 215 |
.version.marker {
|
|
| 216 |
background-image: url(/version_marker.png); |
|
| 217 |
background-repeat: no-repeat; |
|
| 218 |
border: 0; |
|
| 219 |
margin-left: -4px; |
|
| 220 |
margin-top: 1px; |
|
| 221 |
} |
|
| 222 | ||
| 223 |
.project.task_late {
|
|
| 224 |
background: var(oc-red-5) url(/milestone_late.png); |
|
| 225 |
border: 1px solid var(oc-red-5); |
|
| 226 |
height: 2px; |
|
| 227 |
margin-top: 3px; |
|
| 228 |
} |
|
| 229 | ||
| 230 |
.project.task_done {
|
|
| 231 |
background: var(--oc-green-7) url(/milestone_done.png); |
|
| 232 |
border: 1px solid var(--oc-green-7); |
|
| 233 |
height: 2px; |
|
| 234 |
margin-top: 3px; |
|
| 235 |
} |
|
| 236 | ||
| 237 |
.project.task_todo {
|
|
| 238 |
background: var(--oc-white) url(/milestone_todo.png); |
|
| 239 |
border: 1px solid var(--oc-white); |
|
| 240 |
height: 2px; |
|
| 241 |
margin-top: 3px; |
|
| 242 |
} |
|
| 243 | ||
| 244 |
.project.marker {
|
|
| 245 |
background-image: url(/project_marker.png); |
|
| 246 |
background-repeat: no-repeat; |
|
| 247 |
border: 0; |
|
| 248 |
margin-left: -4px; |
|
| 249 |
margin-top: 1px; |
|
| 250 |
} |
|
| 251 | ||
| 252 |
.version-behind-schedule a, |
|
| 253 |
.issue-behind-schedule a {
|
|
| 254 |
color: var(--oc-yellow-8); |
|
| 255 |
} |
|
| 256 | ||
| 257 |
.version-overdue a, |
|
| 258 |
.issue-overdue a, |
|
| 259 |
.project-overdue a {
|
|
| 260 |
color: var(--oc-red-8); |
|
| 261 |
} |
|
| app/helpers/gantt_helper.rb | ||
|---|---|---|
| 41 | 41 |
end |
| 42 | 42 |
end |
| 43 | 43 |
end |
| 44 | ||
| 45 |
def gantt_chart_tag(query, &) |
|
| 46 |
data_attributes = {
|
|
| 47 |
controller: 'gantt--chart', |
|
| 48 |
# Events emitted by child controllers the chart listens to. |
|
| 49 |
# - `gantt--options` toggles checkboxes under Options. |
|
| 50 |
# - `gantt--subjects` reports tree expand/collapse. |
|
| 51 |
# - Window resize triggers a redraw of progress lines and relations. |
|
| 52 |
action: %w( |
|
| 53 |
gantt--options:toggle-display@document->gantt--chart#handleOptionsDisplay |
|
| 54 |
gantt--options:toggle-relations@document->gantt--chart#handleOptionsRelations |
|
| 55 |
gantt--options:toggle-progress@document->gantt--chart#handleOptionsProgress |
|
| 56 |
gantt--subjects:toggle-tree->gantt--chart#handleSubjectTreeChanged |
|
| 57 |
resize@window->gantt--chart#handleWindowResize |
|
| 58 |
).join(' '),
|
|
| 59 |
'gantt--chart-issue-relation-types-value': Redmine::Helpers::Gantt::DRAW_TYPES.to_json, |
|
| 60 |
'gantt--chart-show-selected-columns-value': query.draw_selected_columns ? 'true' : 'false', |
|
| 61 |
'gantt--chart-show-relations-value': query.draw_relations ? 'true' : 'false', |
|
| 62 |
'gantt--chart-show-progress-value': query.draw_progress_line ? 'true' : 'false' |
|
| 63 |
} |
|
| 64 | ||
| 65 |
tag.table(class: 'gantt-table', data: data_attributes, &) |
|
| 66 |
end |
|
| 67 | ||
| 68 |
def gantt_column_tag(column_name, min_width: nil, **options, &) |
|
| 69 |
options[:data] = {
|
|
| 70 |
controller: 'gantt--column', |
|
| 71 |
action: 'resize@window->gantt--column#handleWindowResize', |
|
| 72 |
'gantt--column-min-width-value': min_width, |
|
| 73 |
'gantt--column-column-value': column_name |
|
| 74 |
} |
|
| 75 |
options[:class] = ["gantt_#{column_name}_column", options[:class]]
|
|
| 76 | ||
| 77 |
tag.td(**options, &) |
|
| 78 |
end |
|
| 79 | ||
| 80 |
def gantt_subjects_tag(&) |
|
| 81 |
data_attributes = {
|
|
| 82 |
controller: 'gantt--subjects', |
|
| 83 |
action: 'gantt--column:resize-column-subjects@document->gantt--subjects#handleResizeColumn' |
|
| 84 |
} |
|
| 85 |
tag.div(class: "gantt_subjects", data: data_attributes, &) |
|
| 86 |
end |
|
| 44 | 87 |
end |
| app/javascript/controllers/gantt/chart_controller.js | ||
|---|---|---|
| 1 |
import { Controller } from "@hotwired/stimulus"
|
|
| 2 | ||
| 3 |
const RELATION_STROKE_WIDTH = 2 |
|
| 4 | ||
| 5 |
export default class extends Controller {
|
|
| 6 |
static targets = ["ganttArea", "drawArea", "subjectsContainer"] |
|
| 7 | ||
| 8 |
static values = {
|
|
| 9 |
issueRelationTypes: Object, |
|
| 10 |
showSelectedColumns: Boolean, |
|
| 11 |
showRelations: Boolean, |
|
| 12 |
showProgress: Boolean |
|
| 13 |
} |
|
| 14 | ||
| 15 |
#drawTop = 0 |
|
| 16 |
#drawRight = 0 |
|
| 17 |
#drawLeft = 0 |
|
| 18 |
#drawPaper = null |
|
| 19 | ||
| 20 |
initialize() {
|
|
| 21 |
this.$ = window.jQuery |
|
| 22 |
this.Raphael = window.Raphael |
|
| 23 |
} |
|
| 24 | ||
| 25 |
connect() {
|
|
| 26 |
this.#drawTop = 0 |
|
| 27 |
this.#drawRight = 0 |
|
| 28 |
this.#drawLeft = 0 |
|
| 29 | ||
| 30 |
this.#drawProgressLineAndRelations() |
|
| 31 |
this.#drawSelectedColumns() |
|
| 32 |
} |
|
| 33 | ||
| 34 |
disconnect() {
|
|
| 35 |
if (this.#drawPaper) {
|
|
| 36 |
this.#drawPaper.remove() |
|
| 37 |
this.#drawPaper = null |
|
| 38 |
} |
|
| 39 |
} |
|
| 40 | ||
| 41 |
showSelectedColumnsValueChanged() {
|
|
| 42 |
this.#drawSelectedColumns() |
|
| 43 |
} |
|
| 44 | ||
| 45 |
showRelationsValueChanged() {
|
|
| 46 |
this.#drawProgressLineAndRelations() |
|
| 47 |
} |
|
| 48 | ||
| 49 |
showProgressValueChanged() {
|
|
| 50 |
this.#drawProgressLineAndRelations() |
|
| 51 |
} |
|
| 52 | ||
| 53 |
handleWindowResize() {
|
|
| 54 |
this.#drawProgressLineAndRelations() |
|
| 55 |
this.#drawSelectedColumns() |
|
| 56 |
} |
|
| 57 | ||
| 58 |
handleSubjectTreeChanged() {
|
|
| 59 |
this.#drawProgressLineAndRelations() |
|
| 60 |
this.#drawSelectedColumns() |
|
| 61 |
} |
|
| 62 | ||
| 63 |
handleOptionsDisplay(event) {
|
|
| 64 |
this.showSelectedColumnsValue = !!(event.detail && event.detail.enabled) |
|
| 65 |
} |
|
| 66 | ||
| 67 |
handleOptionsRelations(event) {
|
|
| 68 |
this.showRelationsValue = !!(event.detail && event.detail.enabled) |
|
| 69 |
} |
|
| 70 | ||
| 71 |
handleOptionsProgress(event) {
|
|
| 72 |
this.showProgressValue = !!(event.detail && event.detail.enabled) |
|
| 73 |
} |
|
| 74 | ||
| 75 |
#drawProgressLineAndRelations() {
|
|
| 76 |
if (this.#drawPaper) {
|
|
| 77 |
this.#drawPaper.clear() |
|
| 78 |
} else {
|
|
| 79 |
this.#drawPaper = this.Raphael(this.drawAreaTarget) |
|
| 80 |
} |
|
| 81 | ||
| 82 |
this.#setupDrawArea() |
|
| 83 | ||
| 84 |
if (this.showProgressValue) {
|
|
| 85 |
this.#drawGanttProgressLines() |
|
| 86 |
} |
|
| 87 | ||
| 88 |
if (this.showRelationsValue) {
|
|
| 89 |
this.#drawRelations() |
|
| 90 |
} |
|
| 91 | ||
| 92 |
const content = document.getElementById("content")
|
|
| 93 |
if (content) {
|
|
| 94 |
content.classList.add("gantt_content")
|
|
| 95 |
} |
|
| 96 |
} |
|
| 97 | ||
| 98 |
#setupDrawArea() {
|
|
| 99 |
const $drawArea = this.$(this.drawAreaTarget) |
|
| 100 |
const $ganttArea = this.hasGanttAreaTarget ? this.$(this.ganttAreaTarget) : null |
|
| 101 | ||
| 102 |
this.#drawTop = $drawArea.position().top |
|
| 103 |
this.#drawRight = $drawArea.width() |
|
| 104 |
this.#drawLeft = $ganttArea ? $ganttArea.scrollLeft() : 0 |
|
| 105 |
} |
|
| 106 | ||
| 107 |
#drawSelectedColumns() {
|
|
| 108 |
const $selectedColumns = this.$("td.gantt_selected_column")
|
|
| 109 |
const $subjectsContainer = this.$(".gantt_subjects_container")
|
|
| 110 | ||
| 111 |
const isMobileDevice = typeof window.isMobile === "function" && window.isMobile() |
|
| 112 | ||
| 113 |
if (this.showSelectedColumnsValue) {
|
|
| 114 |
if (isMobileDevice) {
|
|
| 115 |
$selectedColumns.each((_, element) => {
|
|
| 116 |
this.$(element).hide() |
|
| 117 |
}) |
|
| 118 |
} else {
|
|
| 119 |
$subjectsContainer.addClass("draw_selected_columns")
|
|
| 120 |
$selectedColumns.show() |
|
| 121 |
} |
|
| 122 |
} else {
|
|
| 123 |
$selectedColumns.each((_, element) => {
|
|
| 124 |
this.$(element).hide() |
|
| 125 |
}) |
|
| 126 |
$subjectsContainer.removeClass("draw_selected_columns")
|
|
| 127 |
} |
|
| 128 |
} |
|
| 129 | ||
| 130 |
get #relationsArray() {
|
|
| 131 |
const relations = [] |
|
| 132 | ||
| 133 |
this.$("div.task_todo[data-rels]").each((_, element) => {
|
|
| 134 |
const $element = this.$(element) |
|
| 135 | ||
| 136 |
if (!$element.is(":visible")) return
|
|
| 137 | ||
| 138 |
const elementId = $element.attr("id")
|
|
| 139 | ||
| 140 |
if (!elementId) return |
|
| 141 | ||
| 142 |
const issueId = elementId.replace("task-todo-issue-", "")
|
|
| 143 |
const dataRels = $element.data("rels") || {}
|
|
| 144 | ||
| 145 |
Object.keys(dataRels).forEach((relTypeKey) => {
|
|
| 146 |
this.$.each(dataRels[relTypeKey], (_, relatedIssue) => {
|
|
| 147 |
relations.push({ issue_from: issueId, issue_to: relatedIssue, rel_type: relTypeKey })
|
|
| 148 |
}) |
|
| 149 |
}) |
|
| 150 |
}) |
|
| 151 | ||
| 152 |
return relations |
|
| 153 |
} |
|
| 154 | ||
| 155 |
#drawRelations() {
|
|
| 156 |
const relations = this.#relationsArray |
|
| 157 | ||
| 158 |
relations.forEach((relation) => {
|
|
| 159 |
const issueFrom = this.$(`#task-todo-issue-${relation.issue_from}`)
|
|
| 160 |
const issueTo = this.$(`#task-todo-issue-${relation.issue_to}`)
|
|
| 161 | ||
| 162 |
if (issueFrom.length === 0 || issueTo.length === 0) return |
|
| 163 | ||
| 164 |
const issueHeight = issueFrom.height() |
|
| 165 |
const issueFromTop = issueFrom.position().top + issueHeight / 2 - this.#drawTop |
|
| 166 |
const issueFromRight = issueFrom.position().left + issueFrom.width() |
|
| 167 |
const issueToTop = issueTo.position().top + issueHeight / 2 - this.#drawTop |
|
| 168 |
const issueToLeft = issueTo.position().left |
|
| 169 |
const relationConfig = this.issueRelationTypesValue[relation.rel_type] || {}
|
|
| 170 |
const color = relationConfig.color || "#000" |
|
| 171 |
const landscapeMargin = relationConfig.landscape_margin || 0 |
|
| 172 |
const issueFromRightRel = issueFromRight + landscapeMargin |
|
| 173 |
const issueToLeftRel = issueToLeft - landscapeMargin |
|
| 174 | ||
| 175 |
this.#drawPaper |
|
| 176 |
.path([ |
|
| 177 |
"M", |
|
| 178 |
issueFromRight + this.#drawLeft, |
|
| 179 |
issueFromTop, |
|
| 180 |
"L", |
|
| 181 |
issueFromRightRel + this.#drawLeft, |
|
| 182 |
issueFromTop |
|
| 183 |
]) |
|
| 184 |
.attr({ stroke: color, "stroke-width": RELATION_STROKE_WIDTH })
|
|
| 185 | ||
| 186 |
if (issueFromRightRel < issueToLeftRel) {
|
|
| 187 |
this.#drawPaper |
|
| 188 |
.path([ |
|
| 189 |
"M", |
|
| 190 |
issueFromRightRel + this.#drawLeft, |
|
| 191 |
issueFromTop, |
|
| 192 |
"L", |
|
| 193 |
issueFromRightRel + this.#drawLeft, |
|
| 194 |
issueToTop |
|
| 195 |
]) |
|
| 196 |
.attr({ stroke: color, "stroke-width": RELATION_STROKE_WIDTH })
|
|
| 197 |
this.#drawPaper |
|
| 198 |
.path([ |
|
| 199 |
"M", |
|
| 200 |
issueFromRightRel + this.#drawLeft, |
|
| 201 |
issueToTop, |
|
| 202 |
"L", |
|
| 203 |
issueToLeft + this.#drawLeft, |
|
| 204 |
issueToTop |
|
| 205 |
]) |
|
| 206 |
.attr({ stroke: color, "stroke-width": RELATION_STROKE_WIDTH })
|
|
| 207 |
} else {
|
|
| 208 |
const issueMiddleTop = issueToTop + issueHeight * (issueFromTop > issueToTop ? 1 : -1) |
|
| 209 |
this.#drawPaper |
|
| 210 |
.path([ |
|
| 211 |
"M", |
|
| 212 |
issueFromRightRel + this.#drawLeft, |
|
| 213 |
issueFromTop, |
|
| 214 |
"L", |
|
| 215 |
issueFromRightRel + this.#drawLeft, |
|
| 216 |
issueMiddleTop |
|
| 217 |
]) |
|
| 218 |
.attr({ stroke: color, "stroke-width": RELATION_STROKE_WIDTH })
|
|
| 219 |
this.#drawPaper |
|
| 220 |
.path([ |
|
| 221 |
"M", |
|
| 222 |
issueFromRightRel + this.#drawLeft, |
|
| 223 |
issueMiddleTop, |
|
| 224 |
"L", |
|
| 225 |
issueToLeftRel + this.#drawLeft, |
|
| 226 |
issueMiddleTop |
|
| 227 |
]) |
|
| 228 |
.attr({ stroke: color, "stroke-width": RELATION_STROKE_WIDTH })
|
|
| 229 |
this.#drawPaper |
|
| 230 |
.path([ |
|
| 231 |
"M", |
|
| 232 |
issueToLeftRel + this.#drawLeft, |
|
| 233 |
issueMiddleTop, |
|
| 234 |
"L", |
|
| 235 |
issueToLeftRel + this.#drawLeft, |
|
| 236 |
issueToTop |
|
| 237 |
]) |
|
| 238 |
.attr({ stroke: color, "stroke-width": RELATION_STROKE_WIDTH })
|
|
| 239 |
this.#drawPaper |
|
| 240 |
.path([ |
|
| 241 |
"M", |
|
| 242 |
issueToLeftRel + this.#drawLeft, |
|
| 243 |
issueToTop, |
|
| 244 |
"L", |
|
| 245 |
issueToLeft + this.#drawLeft, |
|
| 246 |
issueToTop |
|
| 247 |
]) |
|
| 248 |
.attr({ stroke: color, "stroke-width": RELATION_STROKE_WIDTH })
|
|
| 249 |
} |
|
| 250 |
this.#drawPaper |
|
| 251 |
.path([ |
|
| 252 |
"M", |
|
| 253 |
issueToLeft + this.#drawLeft, |
|
| 254 |
issueToTop, |
|
| 255 |
"l", |
|
| 256 |
-4 * RELATION_STROKE_WIDTH, |
|
| 257 |
-2 * RELATION_STROKE_WIDTH, |
|
| 258 |
"l", |
|
| 259 |
0, |
|
| 260 |
4 * RELATION_STROKE_WIDTH, |
|
| 261 |
"z" |
|
| 262 |
]) |
|
| 263 |
.attr({
|
|
| 264 |
stroke: "none", |
|
| 265 |
fill: color, |
|
| 266 |
"stroke-linecap": "butt", |
|
| 267 |
"stroke-linejoin": "miter" |
|
| 268 |
}) |
|
| 269 |
}) |
|
| 270 |
} |
|
| 271 | ||
| 272 |
get #progressLinesArray() {
|
|
| 273 |
const lines = [] |
|
| 274 |
const todayLeft = this.$("#today_line").position().left
|
|
| 275 | ||
| 276 |
lines.push({ left: todayLeft, top: 0 })
|
|
| 277 | ||
| 278 |
this.$("div.issue-subject, div.version-name").each((_, element) => {
|
|
| 279 |
const $element = this.$(element) |
|
| 280 | ||
| 281 |
if (!$element.is(":visible")) return true
|
|
| 282 | ||
| 283 |
const topPosition = $element.position().top - this.#drawTop |
|
| 284 |
const elementHeight = $element.height() / 9 |
|
| 285 |
const elementTopUpper = topPosition - elementHeight |
|
| 286 |
const elementTopCenter = topPosition + elementHeight * 3 |
|
| 287 |
const elementTopLower = topPosition + elementHeight * 8 |
|
| 288 |
const issueClosed = $element.children("span").hasClass("issue-closed")
|
|
| 289 |
const versionClosed = $element.children("span").hasClass("version-closed")
|
|
| 290 | ||
| 291 |
if (issueClosed || versionClosed) {
|
|
| 292 |
lines.push({ left: todayLeft, top: elementTopCenter })
|
|
| 293 |
} else {
|
|
| 294 |
const issueDone = this.$(`#task-done-${$element.attr("id")}`)
|
|
| 295 |
const isBehindStart = $element.children("span").hasClass("behind-start-date")
|
|
| 296 |
const isOverEnd = $element.children("span").hasClass("over-end-date")
|
|
| 297 | ||
| 298 |
if (isOverEnd) {
|
|
| 299 |
lines.push({ left: this.#drawRight, top: elementTopUpper, is_right_edge: true })
|
|
| 300 |
lines.push({
|
|
| 301 |
left: this.#drawRight, |
|
| 302 |
top: elementTopLower, |
|
| 303 |
is_right_edge: true, |
|
| 304 |
none_stroke: true |
|
| 305 |
}) |
|
| 306 |
} else if (issueDone.length > 0) {
|
|
| 307 |
const doneLeft = issueDone.first().position().left + issueDone.first().width() |
|
| 308 |
lines.push({ left: doneLeft, top: elementTopCenter })
|
|
| 309 |
} else if (isBehindStart) {
|
|
| 310 |
lines.push({ left: 0, top: elementTopUpper, is_left_edge: true })
|
|
| 311 |
lines.push({
|
|
| 312 |
left: 0, |
|
| 313 |
top: elementTopLower, |
|
| 314 |
is_left_edge: true, |
|
| 315 |
none_stroke: true |
|
| 316 |
}) |
|
| 317 |
} else {
|
|
| 318 |
let todoLeft = todayLeft |
|
| 319 |
const issueTodo = this.$(`#task-todo-${$element.attr("id")}`)
|
|
| 320 |
if (issueTodo.length > 0) {
|
|
| 321 |
todoLeft = issueTodo.first().position().left |
|
| 322 |
} |
|
| 323 |
lines.push({ left: Math.min(todayLeft, todoLeft), top: elementTopCenter })
|
|
| 324 |
} |
|
| 325 |
} |
|
| 326 |
}) |
|
| 327 | ||
| 328 |
return lines |
|
| 329 |
} |
|
| 330 | ||
| 331 |
#drawGanttProgressLines() {
|
|
| 332 |
if (this.$("#today_line").length === 0) return
|
|
| 333 | ||
| 334 |
const progressLines = this.#progressLinesArray |
|
| 335 |
const color = this.$("#today_line").css("border-left-color") || "#ff0000"
|
|
| 336 | ||
| 337 |
for (let index = 1; index < progressLines.length; index += 1) {
|
|
| 338 |
const current = progressLines[index] |
|
| 339 |
const previous = progressLines[index - 1] |
|
| 340 | ||
| 341 |
if ( |
|
| 342 |
!current.none_stroke && |
|
| 343 |
!( |
|
| 344 |
(previous.is_right_edge && current.is_right_edge) || |
|
| 345 |
(previous.is_left_edge && current.is_left_edge) |
|
| 346 |
) |
|
| 347 |
) {
|
|
| 348 |
const x1 = previous.left === 0 ? 0 : previous.left + this.#drawLeft |
|
| 349 |
const x2 = current.left === 0 ? 0 : current.left + this.#drawLeft |
|
| 350 | ||
| 351 |
this.#drawPaper |
|
| 352 |
.path(["M", x1, previous.top, "L", x2, current.top]) |
|
| 353 |
.attr({ stroke: color, "stroke-width": 2 })
|
|
| 354 |
} |
|
| 355 |
} |
|
| 356 |
} |
|
| 357 |
} |
|
| app/javascript/controllers/gantt/column_controller.js | ||
|---|---|---|
| 1 |
import { Controller } from "@hotwired/stimulus"
|
|
| 2 | ||
| 3 |
export default class extends Controller {
|
|
| 4 |
static values = {
|
|
| 5 |
minWidth: Number, |
|
| 6 |
column: String, |
|
| 7 |
// Local value |
|
| 8 |
mobileMode: { type: Boolean, default: false }
|
|
| 9 |
} |
|
| 10 | ||
| 11 |
#$element = null |
|
| 12 | ||
| 13 |
initialize() {
|
|
| 14 |
this.$ = window.jQuery |
|
| 15 |
} |
|
| 16 | ||
| 17 |
connect() {
|
|
| 18 |
this.#$element = this.$(this.element) |
|
| 19 |
this.#setupResizable() |
|
| 20 |
this.#dispatchResizeColumn() |
|
| 21 |
} |
|
| 22 | ||
| 23 |
disconnect() {
|
|
| 24 |
this.#$element?.resizable("destroy")
|
|
| 25 |
this.#$element = null |
|
| 26 |
} |
|
| 27 | ||
| 28 |
handleWindowResize(_event) {
|
|
| 29 |
this.mobileModeValue = this.#isMobile() |
|
| 30 | ||
| 31 |
this.#dispatchResizeColumn() |
|
| 32 |
} |
|
| 33 | ||
| 34 |
mobileModeValueChanged(current, old) {
|
|
| 35 |
if (current == old) return |
|
| 36 | ||
| 37 |
if (this.mobileModeValue) {
|
|
| 38 |
this.#$element?.resizable("disable")
|
|
| 39 |
} else {
|
|
| 40 |
this.#$element?.resizable("enable")
|
|
| 41 |
} |
|
| 42 |
} |
|
| 43 | ||
| 44 |
#setupResizable() {
|
|
| 45 |
const alsoResize = [ |
|
| 46 |
`.gantt_${this.columnValue}_container`,
|
|
| 47 |
`.gantt_${this.columnValue}_container > .gantt_hdr`
|
|
| 48 |
] |
|
| 49 |
const options = {
|
|
| 50 |
handles: "e", |
|
| 51 |
minWidth: this.minWidthValue, |
|
| 52 |
zIndex: 30, |
|
| 53 |
alsoResize: alsoResize.join(","),
|
|
| 54 |
create: () => {
|
|
| 55 |
this.$(".ui-resizable-e").css("cursor", "ew-resize")
|
|
| 56 |
} |
|
| 57 |
} |
|
| 58 | ||
| 59 |
this.#$element |
|
| 60 |
.resizable(options) |
|
| 61 |
.on("resize", (event) => {
|
|
| 62 |
event.stopPropagation() |
|
| 63 |
this.#dispatchResizeColumn() |
|
| 64 |
}) |
|
| 65 |
} |
|
| 66 | ||
| 67 |
#dispatchResizeColumn() {
|
|
| 68 |
if (!this.#$element) return |
|
| 69 | ||
| 70 |
this.dispatch(`resize-column-${this.columnValue}`, { detail: { width: this.#$element.width() } })
|
|
| 71 |
} |
|
| 72 | ||
| 73 |
#isMobile() {
|
|
| 74 |
return !!(typeof window.isMobile === "function" && window.isMobile()) |
|
| 75 |
} |
|
| 76 |
} |
|
| app/javascript/controllers/gantt/options_controller.js | ||
|---|---|---|
| 1 |
import { Controller } from "@hotwired/stimulus"
|
|
| 2 | ||
| 3 |
export default class extends Controller {
|
|
| 4 |
static targets = ["display", "relations", "progress"] |
|
| 5 | ||
| 6 |
static values = {
|
|
| 7 |
unavailableColumns: Array |
|
| 8 |
} |
|
| 9 | ||
| 10 |
initialize() {
|
|
| 11 |
this.$ = window.jQuery |
|
| 12 |
} |
|
| 13 | ||
| 14 |
connect() {
|
|
| 15 |
this.#dispatchInitialStates() |
|
| 16 |
this.#disableUnavailableColumns() |
|
| 17 |
} |
|
| 18 | ||
| 19 |
toggleDisplay(event) {
|
|
| 20 |
this.dispatch("toggle-display", {
|
|
| 21 |
detail: { enabled: event.currentTarget.checked }
|
|
| 22 |
}) |
|
| 23 |
} |
|
| 24 | ||
| 25 |
toggleRelations(event) {
|
|
| 26 |
this.dispatch("toggle-relations", {
|
|
| 27 |
detail: { enabled: event.currentTarget.checked }
|
|
| 28 |
}) |
|
| 29 |
} |
|
| 30 | ||
| 31 |
toggleProgress(event) {
|
|
| 32 |
this.dispatch("toggle-progress", {
|
|
| 33 |
detail: { enabled: event.currentTarget.checked }
|
|
| 34 |
}) |
|
| 35 |
} |
|
| 36 | ||
| 37 |
#dispatchInitialStates() {
|
|
| 38 |
if (this.hasDisplayTarget) {
|
|
| 39 |
this.dispatch("toggle-display", {
|
|
| 40 |
detail: { enabled: this.displayTarget.checked }
|
|
| 41 |
}) |
|
| 42 |
} |
|
| 43 |
if (this.hasRelationsTarget) {
|
|
| 44 |
this.dispatch("toggle-relations", {
|
|
| 45 |
detail: { enabled: this.relationsTarget.checked }
|
|
| 46 |
}) |
|
| 47 |
} |
|
| 48 |
if (this.hasProgressTarget) {
|
|
| 49 |
this.dispatch("toggle-progress", {
|
|
| 50 |
detail: { enabled: this.progressTarget.checked }
|
|
| 51 |
}) |
|
| 52 |
} |
|
| 53 |
} |
|
| 54 | ||
| 55 |
#disableUnavailableColumns() {
|
|
| 56 |
if (!Array.isArray(this.unavailableColumnsValue)) {
|
|
| 57 |
return |
|
| 58 |
} |
|
| 59 |
this.unavailableColumnsValue.forEach((column) => {
|
|
| 60 |
this.$("#available_c, #selected_c").children(`[value='${column}']`).prop("disabled", true)
|
|
| 61 |
}) |
|
| 62 |
} |
|
| 63 |
} |
|
| app/javascript/controllers/gantt/subjects_controller.js | ||
|---|---|---|
| 1 |
import { Controller } from "@hotwired/stimulus"
|
|
| 2 | ||
| 3 |
export default class extends Controller {
|
|
| 4 |
initialize() {
|
|
| 5 |
this.$ = window.jQuery |
|
| 6 |
} |
|
| 7 | ||
| 8 |
handleResizeColumn(event) {
|
|
| 9 |
const columnWidth = event.detail.width; |
|
| 10 | ||
| 11 |
this.$(".issue-subject, .project-name, .version-name").each((_, element) => {
|
|
| 12 |
const $element = this.$(element) |
|
| 13 |
$element.width(columnWidth - $element.position().left) |
|
| 14 |
}) |
|
| 15 |
} |
|
| 16 | ||
| 17 |
handleEntryClick(event) {
|
|
| 18 |
const iconExpander = event.currentTarget |
|
| 19 |
const $subject = this.$(iconExpander.parentElement) |
|
| 20 |
const subjectLeft = |
|
| 21 |
parseInt($subject.css("left"), 10) + parseInt(iconExpander.offsetWidth, 10)
|
|
| 22 | ||
| 23 |
let targetShown = null |
|
| 24 |
let targetTop = 0 |
|
| 25 |
let totalHeight = 0 |
|
| 26 |
let outOfHierarchy = false |
|
| 27 | ||
| 28 |
const willOpen = !$subject.hasClass("open")
|
|
| 29 | ||
| 30 |
this.#setIconState($subject, willOpen) |
|
| 31 | ||
| 32 |
$subject.nextAll("div").each((_, element) => {
|
|
| 33 |
const $element = this.$(element) |
|
| 34 |
const json = $element.data("collapse-expand")
|
|
| 35 |
const numberOfRows = $element.data("number-of-rows")
|
|
| 36 |
const barsSelector = `#gantt_area form > div[data-collapse-expand='${json.obj_id}'][data-number-of-rows='${numberOfRows}']`
|
|
| 37 |
const selectedColumnsSelector = `td.gantt_selected_column div[data-collapse-expand='${json.obj_id}'][data-number-of-rows='${numberOfRows}']`
|
|
| 38 | ||
| 39 |
if (outOfHierarchy || parseInt($element.css("left"), 10) <= subjectLeft) {
|
|
| 40 |
outOfHierarchy = true |
|
| 41 | ||
| 42 |
if (targetShown === null) return false |
|
| 43 | ||
| 44 |
const newTopVal = parseInt($element.css("top"), 10) + totalHeight * (targetShown ? -1 : 1)
|
|
| 45 | ||
| 46 |
$element.css("top", newTopVal)
|
|
| 47 |
this.$([barsSelector, selectedColumnsSelector].join()).each((__, el) => {
|
|
| 48 |
this.$(el).css("top", newTopVal)
|
|
| 49 |
}) |
|
| 50 | ||
| 51 |
return true |
|
| 52 |
} |
|
| 53 | ||
| 54 |
const isShown = $element.is(":visible")
|
|
| 55 | ||
| 56 |
if (targetShown === null) {
|
|
| 57 |
targetShown = isShown |
|
| 58 |
targetTop = parseInt($element.css("top"), 10)
|
|
| 59 |
totalHeight = 0 |
|
| 60 |
} |
|
| 61 | ||
| 62 |
if (isShown === targetShown) {
|
|
| 63 |
this.$(barsSelector).each((__, task) => {
|
|
| 64 |
const $task = this.$(task) |
|
| 65 | ||
| 66 |
if (!isShown && willOpen) {
|
|
| 67 |
$task.css("top", targetTop + totalHeight)
|
|
| 68 |
} |
|
| 69 |
if (!$task.hasClass("tooltip")) {
|
|
| 70 |
$task.toggle(willOpen) |
|
| 71 |
} |
|
| 72 |
}) |
|
| 73 | ||
| 74 |
this.$(selectedColumnsSelector).each((__, attr) => {
|
|
| 75 |
const $attr = this.$(attr) |
|
| 76 | ||
| 77 |
if (!isShown && willOpen) {
|
|
| 78 |
$attr.css("top", targetTop + totalHeight)
|
|
| 79 |
} |
|
| 80 |
$attr.toggle(willOpen) |
|
| 81 |
}) |
|
| 82 | ||
| 83 |
if (!isShown && willOpen) {
|
|
| 84 |
$element.css("top", targetTop + totalHeight)
|
|
| 85 |
} |
|
| 86 | ||
| 87 |
this.#setIconState($element, willOpen) |
|
| 88 |
$element.toggle(willOpen) |
|
| 89 |
totalHeight += parseInt(json.top_increment, 10) |
|
| 90 |
} |
|
| 91 |
}) |
|
| 92 | ||
| 93 |
this.dispatch("toggle-tree", { bubbles: true })
|
|
| 94 |
} |
|
| 95 | ||
| 96 |
#setIconState(element, open) {
|
|
| 97 |
const $element = element.jquery ? element : this.$(element) |
|
| 98 |
const expander = $element.find(".expander")
|
|
| 99 | ||
| 100 |
if (open) {
|
|
| 101 |
$element.addClass("open")
|
|
| 102 | ||
| 103 |
if (expander.length > 0) {
|
|
| 104 |
expander.removeClass("icon-collapsed").addClass("icon-expanded")
|
|
| 105 | ||
| 106 |
if (expander.find("svg").length === 1) {
|
|
| 107 |
window.updateSVGIcon(expander[0], "angle-down") |
|
| 108 |
} |
|
| 109 |
} |
|
| 110 |
} else {
|
|
| 111 |
$element.removeClass("open")
|
|
| 112 | ||
| 113 |
if (expander.length > 0) {
|
|
| 114 |
expander.removeClass("icon-expanded").addClass("icon-collapsed")
|
|
| 115 | ||
| 116 |
if (expander.find("svg").length === 1) {
|
|
| 117 |
window.updateSVGIcon(expander[0], "angle-right") |
|
| 118 |
} |
|
| 119 |
} |
|
| 120 |
} |
|
| 121 |
} |
|
| 122 |
} |
|
| app/views/gantts/_chart.html.erb | ||
|---|---|---|
| 1 |
<% |
|
| 2 |
zoom = 1 |
|
| 3 |
gantt.zoom.times { zoom *= 2 }
|
|
| 4 | ||
| 5 |
subject_width = 330 |
|
| 6 |
header_height = 18 |
|
| 7 | ||
| 8 |
headers_height = header_height |
|
| 9 |
show_weeks = false |
|
| 10 |
show_days = false |
|
| 11 |
show_day_num = false |
|
| 12 | ||
| 13 |
if gantt.zoom > 1 |
|
| 14 |
show_weeks = true |
|
| 15 |
headers_height = 2 * header_height |
|
| 16 |
if gantt.zoom > 2 |
|
| 17 |
show_days = true |
|
| 18 |
headers_height = 3 * header_height |
|
| 19 |
if gantt.zoom > 3 |
|
| 20 |
show_day_num = true |
|
| 21 |
headers_height = 4 * header_height |
|
| 22 |
end |
|
| 23 |
end |
|
| 24 |
end |
|
| 25 | ||
| 26 |
g_width = ((gantt.date_to - gantt.date_from + 1) * zoom).to_i |
|
| 27 |
gantt.render( |
|
| 28 |
top: headers_height + 8, |
|
| 29 |
zoom: zoom, |
|
| 30 |
g_width: g_width, |
|
| 31 |
subject_width: subject_width |
|
| 32 |
) |
|
| 33 |
g_height = [(20 * (gantt.number_of_rows + 6)) + 150, 206].max |
|
| 34 |
t_height = g_height + headers_height |
|