diff --git a/app/assets/javascripts/gantt.js b/app/assets/javascripts/gantt.js deleted file mode 100644 index ceb6d9a51..000000000 --- a/app/assets/javascripts/gantt.js +++ /dev/null @@ -1,318 +0,0 @@ -/** - * Redmine - project management software - * Copyright (C) 2006- Jean-Philippe Lang - * This code is released under the GNU General Public License. - */ - -var draw_gantt = null; -var draw_top; -var draw_right; -var draw_left; - -var rels_stroke_width = 2; - -function setDrawArea() { - draw_top = $("#gantt_draw_area").position().top; - draw_right = $("#gantt_draw_area").width(); - draw_left = $("#gantt_area").scrollLeft(); -} - -function getRelationsArray() { - var arr = new Array(); - $.each($('div.task_todo[data-rels]'), function(index_div, element) { - if(!$(element).is(':visible')) return true; - var element_id = $(element).attr("id"); - if (element_id != null) { - var issue_id = element_id.replace("task-todo-issue-", ""); - var data_rels = $(element).data("rels"); - for (rel_type_key in data_rels) { - $.each(data_rels[rel_type_key], function(index_issue, element_issue) { - arr.push({issue_from: issue_id, issue_to: element_issue, - rel_type: rel_type_key}); - }); - } - } - }); - return arr; -} - -function drawRelations() { - var arr = getRelationsArray(); - $.each(arr, function(index_issue, element_issue) { - var issue_from = $("#task-todo-issue-" + element_issue["issue_from"]); - var issue_to = $("#task-todo-issue-" + element_issue["issue_to"]); - if (issue_from.length == 0 || issue_to.length == 0) { - return; - } - var issue_height = issue_from.height(); - var issue_from_top = issue_from.position().top + (issue_height / 2) - draw_top; - var issue_from_right = issue_from.position().left + issue_from.width(); - var issue_to_top = issue_to.position().top + (issue_height / 2) - draw_top; - var issue_to_left = issue_to.position().left; - var color = issue_relation_type[element_issue["rel_type"]]["color"]; - var landscape_margin = issue_relation_type[element_issue["rel_type"]]["landscape_margin"]; - var issue_from_right_rel = issue_from_right + landscape_margin; - var issue_to_left_rel = issue_to_left - landscape_margin; - draw_gantt.path(["M", issue_from_right + draw_left, issue_from_top, - "L", issue_from_right_rel + draw_left, issue_from_top]) - .attr({stroke: color, - "stroke-width": rels_stroke_width - }); - if (issue_from_right_rel < issue_to_left_rel) { - draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_from_top, - "L", issue_from_right_rel + draw_left, issue_to_top]) - .attr({stroke: color, - "stroke-width": rels_stroke_width - }); - draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_to_top, - "L", issue_to_left + draw_left, issue_to_top]) - .attr({stroke: color, - "stroke-width": rels_stroke_width - }); - } else { - var issue_middle_top = issue_to_top + - (issue_height * - ((issue_from_top > issue_to_top) ? 1 : -1)); - draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_from_top, - "L", issue_from_right_rel + draw_left, issue_middle_top]) - .attr({stroke: color, - "stroke-width": rels_stroke_width - }); - draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_middle_top, - "L", issue_to_left_rel + draw_left, issue_middle_top]) - .attr({stroke: color, - "stroke-width": rels_stroke_width - }); - draw_gantt.path(["M", issue_to_left_rel + draw_left, issue_middle_top, - "L", issue_to_left_rel + draw_left, issue_to_top]) - .attr({stroke: color, - "stroke-width": rels_stroke_width - }); - draw_gantt.path(["M", issue_to_left_rel + draw_left, issue_to_top, - "L", issue_to_left + draw_left, issue_to_top]) - .attr({stroke: color, - "stroke-width": rels_stroke_width - }); - } - draw_gantt.path(["M", issue_to_left + draw_left, issue_to_top, - "l", -4 * rels_stroke_width, -2 * rels_stroke_width, - "l", 0, 4 * rels_stroke_width, "z"]) - .attr({stroke: "none", - fill: color, - "stroke-linecap": "butt", - "stroke-linejoin": "miter" - }); - }); -} - -function getProgressLinesArray() { - var arr = new Array(); - var today_left = $('#today_line').position().left; - arr.push({left: today_left, top: 0}); - $.each($('div.issue-subject, div.version-name'), function(index, element) { - if(!$(element).is(':visible')) return true; - var t = $(element).position().top - draw_top ; - var h = ($(element).height() / 9); - var element_top_upper = t - h; - var element_top_center = t + (h * 3); - var element_top_lower = t + (h * 8); - var issue_closed = $(element).children('span').hasClass('issue-closed'); - var version_closed = $(element).children('span').hasClass('version-closed'); - if (issue_closed || version_closed) { - arr.push({left: today_left, top: element_top_center}); - } else { - var issue_done = $("#task-done-" + $(element).attr("id")); - var is_behind_start = $(element).children('span').hasClass('behind-start-date'); - var is_over_end = $(element).children('span').hasClass('over-end-date'); - if (is_over_end) { - arr.push({left: draw_right, top: element_top_upper, is_right_edge: true}); - arr.push({left: draw_right, top: element_top_lower, is_right_edge: true, none_stroke: true}); - } else if (issue_done.length > 0) { - var done_left = issue_done.first().position().left + - issue_done.first().width(); - arr.push({left: done_left, top: element_top_center}); - } else if (is_behind_start) { - arr.push({left: 0 , top: element_top_upper, is_left_edge: true}); - arr.push({left: 0 , top: element_top_lower, is_left_edge: true, none_stroke: true}); - } else { - var todo_left = today_left; - var issue_todo = $("#task-todo-" + $(element).attr("id")); - if (issue_todo.length > 0){ - todo_left = issue_todo.first().position().left; - } - arr.push({left: Math.min(today_left, todo_left), top: element_top_center}); - } - } - }); - return arr; -} - -function drawGanttProgressLines() { - var arr = getProgressLinesArray(); - var color = $("#today_line") - .css("border-left-color"); - var i; - for(i = 1 ; i < arr.length ; i++) { - if (!("none_stroke" in arr[i]) && - (!("is_right_edge" in arr[i - 1] && "is_right_edge" in arr[i]) && - !("is_left_edge" in arr[i - 1] && "is_left_edge" in arr[i])) - ) { - var x1 = (arr[i - 1].left == 0) ? 0 : arr[i - 1].left + draw_left; - var x2 = (arr[i].left == 0) ? 0 : arr[i].left + draw_left; - draw_gantt.path(["M", x1, arr[i - 1].top, - "L", x2, arr[i].top]) - .attr({stroke: color, "stroke-width": 2}); - } - } -} - -function drawSelectedColumns(){ - if ($("#draw_selected_columns").prop('checked')) { - if(isMobile()) { - $('td.gantt_selected_column').each(function(i) { - $(this).hide(); - }); - }else{ - $('.gantt_subjects_container').addClass('draw_selected_columns'); - $('td.gantt_selected_column').each(function() { - $(this).show(); - var column_name = $(this).attr('id'); - $(this).resizable({ - zIndex: 30, - alsoResize: '.gantt_' + column_name + '_container, .gantt_' + column_name + '_container > .gantt_hdr', - minWidth: 20, - handles: "e", - create: function() { - $(".ui-resizable-e").css("cursor","ew-resize"); - } - }).on('resize', function (e) { - e.stopPropagation(); - }); - }); - } - }else{ - $('td.gantt_selected_column').each(function (i) { - $(this).hide(); - $('.gantt_subjects_container').removeClass('draw_selected_columns'); - }); - } -} - -function drawGanttHandler() { - var folder = document.getElementById('gantt_draw_area'); - if(draw_gantt != null) - draw_gantt.clear(); - else - draw_gantt = Raphael(folder); - setDrawArea(); - drawSelectedColumns(); - if ($("#draw_progress_line").prop('checked')) - try{drawGanttProgressLines();}catch(e){} - if ($("#draw_relations").prop('checked')) - drawRelations(); - $('#content').addClass('gantt_content'); -} - -function resizableSubjectColumn(){ - $('.issue-subject, .project-name, .version-name').each(function(){ - $(this).width($(".gantt_subjects_column").width()-$(this).position().left); - }); - $('td.gantt_subjects_column').resizable({ - alsoResize: '.gantt_subjects_container, .gantt_subjects_container>.gantt_hdr, .project-name, .issue-subject, .version-name', - minWidth: 100, - handles: 'e', - zIndex: 30, - create: function( event, ui ) { - $('.ui-resizable-e').css('cursor','ew-resize'); - } - }).on('resize', function (e) { - e.stopPropagation(); - }); - if(isMobile()) { - $('td.gantt_subjects_column').resizable('disable'); - }else{ - $('td.gantt_subjects_column').resizable('enable'); - }; -} - -ganttEntryClick = function(e){ - var icon_expander = e.currentTarget; - var subject = $(icon_expander.parentElement); - var subject_left = parseInt(subject.css('left')) + parseInt(icon_expander.offsetWidth); - var target_shown = null; - var target_top = 0; - var total_height = 0; - var out_of_hierarchy = false; - var iconChange = null; - if(subject.hasClass('open')) - iconChange = function(element){ - var expander = $(element).find('.expander') - expander.switchClass('icon-expanded', 'icon-collapsed'); - $(element).removeClass('open'); - if (expander.find('svg').length === 1) { - updateSVGIcon(expander[0], 'angle-right') - } - }; - else - iconChange = function(element){ - var expander = $(element).find('.expander') - expander.find('.expander').switchClass('icon-collapsed', 'icon-expanded'); - $(element).addClass('open'); - if (expander.find('svg').length === 1) { - updateSVGIcon(expander[0], 'angle-down') - } - }; - iconChange(subject); - subject.nextAll('div').each(function(_, element){ - var el = $(element); - var json = el.data('collapse-expand'); - var number_of_rows = el.data('number-of-rows'); - var el_task_bars = '#gantt_area form > div[data-collapse-expand="' + json.obj_id + '"][data-number-of-rows="' + number_of_rows + '"]'; - var el_selected_columns = 'td.gantt_selected_column div[data-collapse-expand="' + json.obj_id + '"][data-number-of-rows="' + number_of_rows + '"]'; - if(out_of_hierarchy || parseInt(el.css('left')) <= subject_left){ - out_of_hierarchy = true; - if(target_shown == null) return false; - - var new_top_val = parseInt(el.css('top')) + total_height * (target_shown ? -1 : 1); - el.css('top', new_top_val); - $([el_task_bars, el_selected_columns].join()).each(function(_, el){ - $(el).css('top', new_top_val); - }); - return true; - } - - var is_shown = el.is(':visible'); - if(target_shown == null){ - target_shown = is_shown; - target_top = parseInt(el.css('top')); - total_height = 0; - } - if(is_shown == target_shown){ - $(el_task_bars).each(function(_, task) { - var el_task = $(task); - if(!is_shown) - el_task.css('top', target_top + total_height); - if(!el_task.hasClass('tooltip')) - el_task.toggle(!is_shown); - }); - $(el_selected_columns).each(function (_, attr) { - var el_attr = $(attr); - if (!is_shown) - el_attr.css('top', target_top + total_height); - el_attr.toggle(!is_shown); - }); - if(!is_shown) - el.css('top', target_top + total_height); - iconChange(el); - el.toggle(!is_shown); - total_height += parseInt(json.top_increment); - } - }); - drawGanttHandler(); -}; - -function disable_unavailable_columns(unavailable_columns) { - $.each(unavailable_columns, function (index, value) { - $('#available_c, #selected_c').children("[value='" + value + "']").prop('disabled', true); - }); -} diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index cfa5477f4..16fdedfb6 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -434,9 +434,7 @@ tr.entry td.age { text-align: right; } tr.entry.file td.filename a { margin-left: 26px; } tr.entry.file td.filename_no_report a { margin-left: 16px; } -tr span.expander, .gantt_subjects div > span.expander {margin-left: 0; cursor: pointer;} -.gantt_subjects .avatar {margin-right: 4px;} -.gantt_subjects div.project-name a, .gantt_subjects div.version-name a {margin-left: 4px;} +tr span.expander {margin-left: 0; cursor: pointer;} tr.changeset { height: 20px } tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; } @@ -1702,130 +1700,6 @@ div.wiki .task-list input.task-list-item-checkbox { #my-page .list th.checkbox, #my-page .list td.checkbox {display:none;} -/***** Gantt chart *****/ -table.gantt-table { - width: 100%; - border-collapse: collapse; -} -table.gantt-table td { - padding: 0px; -} -.gantt_hdr { - position:absolute; - top:0; - height:16px; - border-top: 1px solid var(--oc-gray-4); - border-bottom: 1px solid var(--oc-gray-4); - border-left: 1px solid var(--oc-gray-4); - text-align: center; - overflow: hidden; -} -#gantt_area .gantt_hdr { - border-left: 0px; - border-right: 1px solid var(--oc-gray-4); -} -.gantt_subjects_container:not(.draw_selected_columns) .gantt_hdr, -.last_gantt_selected_column .gantt_hdr { - border-right: 1px solid var(--oc-gray-4); -} -.last_gantt_selected_column .gantt_selected_column_container, -.gantt_subjects_container .gantt_subjects * { - z-index: 10; -} - -.gantt_subjects_column + td { - padding: 0; -} - -.gantt_hdr.nwday {background-color:var(--oc-gray-1); color:var(--oc-gray-6);} - -.gantt_subjects, -.gantt_selected_column_content.gantt_hdr { - font-size: 0.8em; - position: relative; - z-index: 1; -} -.gantt_subjects div, -.gantt_selected_column_content div { - line-height: 16px; - height: 16px; - overflow: hidden; - white-space: nowrap; - text-overflow: clip; - width: 100%; -} -.gantt_subjects div.issue-subject:hover { background-color:var(--oc-yellow-0); } -.gantt_selected_column_content > div { padding-left: 3px; box-sizing: border-box; } - -.gantt_hdr_selected_column_name { - position: absolute; - top: 50%; - width:100%; - transform: translateY(-50%); - -webkit-transform: translateY(-50%); - font-size: 0.8em; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - -} -td.gantt_selected_column { - width: 50px; -} -td.gantt_selected_column .gantt_hdr,.gantt_selected_column_container { - width: 49px; -} - -td.gantt_watcher_users_column div.issue_watcher_users ul { - margin: 0; - padding: 0; - list-style: none; -} - -td.gantt_watcher_users_column div.issue_watcher_users ul li { - display: inline; -} - -td.gantt_watcher_users_column div.issue_watcher_users ul li:not(:last-child)::after { - content: ', '; - white-space: pre; -} - -.task { - position: absolute; - height:8px; - font-size:0.8em; - color:var(--oc-gray-6); - padding:0; - margin:0; - line-height:16px; - white-space:nowrap; -} - -.task.label {width:100%;} -.task.label.project, .task.label.version { font-weight: bold; } - -.task_late { background:var(--oc-red-5) url(/task_late.png); border: 1px solid var(--oc-red-5); } -.task_done { background:var(--oc-green-7) url(/task_done.png); border: 1px solid var(--oc-green-7); } -.task_todo { background:var(--oc-gray-5) url(/task_todo.png); border: 1px solid var(--oc-gray-5); } - -.task_todo.parent { background: var(--oc-gray-6); border: 1px solid var(--oc-gray-6); height: 3px;} -.task_late.parent, .task_done.parent { height: 3px;} -.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;} -.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;} - -.version.task_late { background:var(oc-red-5) url(/milestone_late.png); border: 1px solid var(oc-red-5); height: 2px; margin-top: 3px;} -.version.task_done { background:var(--oc-green-7) url(/milestone_done.png); border: 1px solid var(--oc-green-7); height: 2px; margin-top: 3px;} -.version.task_todo { background:var(--oc-white) url(/milestone_todo.png); border: 1px solid var(--oc-white); height: 2px; margin-top: 3px;} -.version.marker { background-image:url(/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; } - -.project.task_late { background:var(oc-red-5) url(/milestone_late.png); border: 1px solid var(oc-red-5); height: 2px; margin-top: 3px;} -.project.task_done { background:var(--oc-green-7) url(/milestone_done.png); border: 1px solid var(--oc-green-7); height: 2px; margin-top: 3px;} -.project.task_todo { background:var(--oc-white) url(/milestone_todo.png); border: 1px solid var(--oc-white); height: 2px; margin-top: 3px;} -.project.marker { background-image:url(/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; } - -.version-behind-schedule a, .issue-behind-schedule a {color: var(--oc-yellow-8);} -.version-overdue a, .issue-overdue a, .project-overdue a {color: var(--oc-red-8);} /***** User events (ex: journal, notes, replies, comments) *****/ .journals h4.journal-header { diff --git a/app/assets/stylesheets/gantt.css b/app/assets/stylesheets/gantt.css new file mode 100644 index 000000000..5757b0b09 --- /dev/null +++ b/app/assets/stylesheets/gantt.css @@ -0,0 +1,261 @@ +/** + * Redmine - project management software + * Copyright (C) 2006- Jean-Philippe Lang + * This code is released under the GNU General Public License. + */ + +.gantt_subjects div > span.expander { + margin-left: 0; + cursor: pointer; +} + +.gantt_subjects .avatar { + margin-right: 4px; +} + +.gantt_subjects div.project-name a, +.gantt_subjects div.version-name a { + margin-left: 4px; +} + +/***** Gantt chart *****/ +table.gantt-table { + width: 100%; + border-collapse: collapse; +} + +table.gantt-table td { + padding: 0; +} + +.gantt_hdr { + position: absolute; + top: 0; + height: 16px; + border-top: 1px solid var(--oc-gray-4); + border-bottom: 1px solid var(--oc-gray-4); + border-left: 1px solid var(--oc-gray-4); + text-align: center; + overflow: hidden; +} + +#gantt_area .gantt_hdr { + border-left: 0; + border-right: 1px solid var(--oc-gray-4); +} + +.gantt_subjects_container:not(.draw_selected_columns) .gantt_hdr, +.last_gantt_selected_column .gantt_hdr { + border-right: 1px solid var(--oc-gray-4); +} + +.last_gantt_selected_column .gantt_selected_column_container, +.gantt_subjects_container .gantt_subjects * { + z-index: 10; +} + +.gantt_subjects_column + td { + padding: 0; +} + +.gantt_hdr.nwday { + background-color: var(--oc-gray-1); + color: var(--oc-gray-6); +} + +.gantt_subjects, +.gantt_selected_column_content.gantt_hdr { + font-size: 0.8em; + position: relative; + z-index: 1; +} + +.gantt_subjects div, +.gantt_selected_column_content div { + line-height: 16px; + height: 16px; + overflow: hidden; + white-space: nowrap; + text-overflow: clip; + width: 100%; +} + +.gantt_subjects div.issue-subject:hover { + background-color: var(--oc-yellow-0); +} + +.gantt_selected_column_content > div { + padding-left: 3px; + box-sizing: border-box; +} + +.gantt_hdr_selected_column_name { + position: absolute; + top: 50%; + width: 100%; + transform: translateY(-50%); + -webkit-transform: translateY(-50%); + font-size: 0.8em; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +td.gantt_selected_column { + width: 50px; +} + +td.gantt_selected_column .gantt_hdr, +.gantt_selected_column_container { + width: 49px; +} + +td.gantt_watcher_users_column div.issue_watcher_users ul { + margin: 0; + padding: 0; + list-style: none; +} + +td.gantt_watcher_users_column div.issue_watcher_users ul li { + display: inline; +} + +td.gantt_watcher_users_column div.issue_watcher_users ul li:not(:last-child)::after { + content: ', '; + white-space: pre; +} + +.task { + position: absolute; + height: 8px; + font-size: 0.8em; + color: var(--oc-gray-6); + padding: 0; + margin: 0; + line-height: 16px; + white-space: nowrap; +} + +.task.label { + width: 100%; +} + +.task.label.project, +.task.label.version { + font-weight: bold; +} + +.task_late { + background: var(--oc-red-5) url(/task_late.png); + border: 1px solid var(--oc-red-5); +} + +.task_done { + background: var(--oc-green-7) url(/task_done.png); + border: 1px solid var(--oc-green-7); +} + +.task_todo { + background: var(--oc-gray-5) url(/task_todo.png); + border: 1px solid var(--oc-gray-5); +} + +.task_todo.parent { + background: var(--oc-gray-6); + border: 1px solid var(--oc-gray-6); + height: 3px; +} + +.task_late.parent, +.task_done.parent { + height: 3px; +} + +.task.parent.marker.starting { + position: absolute; + background: url(/task_parent_end.png) no-repeat 0 0; + width: 8px; + height: 16px; + margin-left: -4px; + left: 0; + top: -1px; +} + +.task.parent.marker.ending { + position: absolute; + background: url(/task_parent_end.png) no-repeat 0 0; + width: 8px; + height: 16px; + margin-left: -4px; + right: 0; + top: -1px; +} + +.version.task_late { + background: var(oc-red-5) url(/milestone_late.png); + border: 1px solid var(oc-red-5); + height: 2px; + margin-top: 3px; +} + +.version.task_done { + background: var(--oc-green-7) url(/milestone_done.png); + border: 1px solid var(--oc-green-7); + height: 2px; + margin-top: 3px; +} + +.version.task_todo { + background: var(--oc-white) url(/milestone_todo.png); + border: 1px solid var(--oc-white); + height: 2px; + margin-top: 3px; +} + +.version.marker { + background-image: url(/version_marker.png); + background-repeat: no-repeat; + border: 0; + margin-left: -4px; + margin-top: 1px; +} + +.project.task_late { + background: var(oc-red-5) url(/milestone_late.png); + border: 1px solid var(oc-red-5); + height: 2px; + margin-top: 3px; +} + +.project.task_done { + background: var(--oc-green-7) url(/milestone_done.png); + border: 1px solid var(--oc-green-7); + height: 2px; + margin-top: 3px; +} + +.project.task_todo { + background: var(--oc-white) url(/milestone_todo.png); + border: 1px solid var(--oc-white); + height: 2px; + margin-top: 3px; +} + +.project.marker { + background-image: url(/project_marker.png); + background-repeat: no-repeat; + border: 0; + margin-left: -4px; + margin-top: 1px; +} + +.version-behind-schedule a, +.issue-behind-schedule a { + color: var(--oc-yellow-8); +} + +.version-overdue a, +.issue-overdue a, +.project-overdue a { + color: var(--oc-red-8); +} diff --git a/app/helpers/gantt_helper.rb b/app/helpers/gantt_helper.rb index 054aad366..b86e3dbbf 100644 --- a/app/helpers/gantt_helper.rb +++ b/app/helpers/gantt_helper.rb @@ -41,4 +41,47 @@ module GanttHelper end end end + + def gantt_chart_tag(query, &) + data_attributes = { + controller: 'gantt--chart', + # Events emitted by child controllers the chart listens to. + # - `gantt--options` toggles checkboxes under Options. + # - `gantt--subjects` reports tree expand/collapse. + # - Window resize triggers a redraw of progress lines and relations. + action: %w( + gantt--options:toggle-display@document->gantt--chart#handleOptionsDisplay + gantt--options:toggle-relations@document->gantt--chart#handleOptionsRelations + gantt--options:toggle-progress@document->gantt--chart#handleOptionsProgress + gantt--subjects:toggle-tree->gantt--chart#handleSubjectTreeChanged + resize@window->gantt--chart#handleWindowResize + ).join(' '), + 'gantt--chart-issue-relation-types-value': Redmine::Helpers::Gantt::DRAW_TYPES.to_json, + 'gantt--chart-show-selected-columns-value': query.draw_selected_columns ? 'true' : 'false', + 'gantt--chart-show-relations-value': query.draw_relations ? 'true' : 'false', + 'gantt--chart-show-progress-value': query.draw_progress_line ? 'true' : 'false' + } + + tag.table(class: 'gantt-table', data: data_attributes, &) + end + + def gantt_column_tag(column_name, min_width: nil, **options, &) + options[:data] = { + controller: 'gantt--column', + action: 'resize@window->gantt--column#handleWindowResize', + 'gantt--column-min-width-value': min_width, + 'gantt--column-column-value': column_name + } + options[:class] = ["gantt_#{column_name}_column", options[:class]] + + tag.td(**options, &) + end + + def gantt_subjects_tag(&) + data_attributes = { + controller: 'gantt--subjects', + action: 'gantt--column:resize-column-subjects@document->gantt--subjects#handleResizeColumn' + } + tag.div(class: "gantt_subjects", data: data_attributes, &) + end end diff --git a/app/javascript/controllers/gantt/chart_controller.js b/app/javascript/controllers/gantt/chart_controller.js new file mode 100644 index 000000000..1ea93c1ea --- /dev/null +++ b/app/javascript/controllers/gantt/chart_controller.js @@ -0,0 +1,357 @@ +import { Controller } from "@hotwired/stimulus" + +const RELATION_STROKE_WIDTH = 2 + +export default class extends Controller { + static targets = ["ganttArea", "drawArea", "subjectsContainer"] + + static values = { + issueRelationTypes: Object, + showSelectedColumns: Boolean, + showRelations: Boolean, + showProgress: Boolean + } + + #drawTop = 0 + #drawRight = 0 + #drawLeft = 0 + #drawPaper = null + + initialize() { + this.$ = window.jQuery + this.Raphael = window.Raphael + } + + connect() { + this.#drawTop = 0 + this.#drawRight = 0 + this.#drawLeft = 0 + + this.#drawProgressLineAndRelations() + this.#drawSelectedColumns() + } + + disconnect() { + if (this.#drawPaper) { + this.#drawPaper.remove() + this.#drawPaper = null + } + } + + showSelectedColumnsValueChanged() { + this.#drawSelectedColumns() + } + + showRelationsValueChanged() { + this.#drawProgressLineAndRelations() + } + + showProgressValueChanged() { + this.#drawProgressLineAndRelations() + } + + handleWindowResize() { + this.#drawProgressLineAndRelations() + this.#drawSelectedColumns() + } + + handleSubjectTreeChanged() { + this.#drawProgressLineAndRelations() + this.#drawSelectedColumns() + } + + handleOptionsDisplay(event) { + this.showSelectedColumnsValue = !!(event.detail && event.detail.enabled) + } + + handleOptionsRelations(event) { + this.showRelationsValue = !!(event.detail && event.detail.enabled) + } + + handleOptionsProgress(event) { + this.showProgressValue = !!(event.detail && event.detail.enabled) + } + + #drawProgressLineAndRelations() { + if (this.#drawPaper) { + this.#drawPaper.clear() + } else { + this.#drawPaper = this.Raphael(this.drawAreaTarget) + } + + this.#setupDrawArea() + + if (this.showProgressValue) { + this.#drawGanttProgressLines() + } + + if (this.showRelationsValue) { + this.#drawRelations() + } + + const content = document.getElementById("content") + if (content) { + content.classList.add("gantt_content") + } + } + + #setupDrawArea() { + const $drawArea = this.$(this.drawAreaTarget) + const $ganttArea = this.hasGanttAreaTarget ? this.$(this.ganttAreaTarget) : null + + this.#drawTop = $drawArea.position().top + this.#drawRight = $drawArea.width() + this.#drawLeft = $ganttArea ? $ganttArea.scrollLeft() : 0 + } + + #drawSelectedColumns() { + const $selectedColumns = this.$("td.gantt_selected_column") + const $subjectsContainer = this.$(".gantt_subjects_container") + + const isMobileDevice = typeof window.isMobile === "function" && window.isMobile() + + if (this.showSelectedColumnsValue) { + if (isMobileDevice) { + $selectedColumns.each((_, element) => { + this.$(element).hide() + }) + } else { + $subjectsContainer.addClass("draw_selected_columns") + $selectedColumns.show() + } + } else { + $selectedColumns.each((_, element) => { + this.$(element).hide() + }) + $subjectsContainer.removeClass("draw_selected_columns") + } + } + + get #relationsArray() { + const relations = [] + + this.$("div.task_todo[data-rels]").each((_, element) => { + const $element = this.$(element) + + if (!$element.is(":visible")) return + + const elementId = $element.attr("id") + + if (!elementId) return + + const issueId = elementId.replace("task-todo-issue-", "") + const dataRels = $element.data("rels") || {} + + Object.keys(dataRels).forEach((relTypeKey) => { + this.$.each(dataRels[relTypeKey], (_, relatedIssue) => { + relations.push({ issue_from: issueId, issue_to: relatedIssue, rel_type: relTypeKey }) + }) + }) + }) + + return relations + } + + #drawRelations() { + const relations = this.#relationsArray + + relations.forEach((relation) => { + const issueFrom = this.$(`#task-todo-issue-${relation.issue_from}`) + const issueTo = this.$(`#task-todo-issue-${relation.issue_to}`) + + if (issueFrom.length === 0 || issueTo.length === 0) return + + const issueHeight = issueFrom.height() + const issueFromTop = issueFrom.position().top + issueHeight / 2 - this.#drawTop + const issueFromRight = issueFrom.position().left + issueFrom.width() + const issueToTop = issueTo.position().top + issueHeight / 2 - this.#drawTop + const issueToLeft = issueTo.position().left + const relationConfig = this.issueRelationTypesValue[relation.rel_type] || {} + const color = relationConfig.color || "#000" + const landscapeMargin = relationConfig.landscape_margin || 0 + const issueFromRightRel = issueFromRight + landscapeMargin + const issueToLeftRel = issueToLeft - landscapeMargin + + this.#drawPaper + .path([ + "M", + issueFromRight + this.#drawLeft, + issueFromTop, + "L", + issueFromRightRel + this.#drawLeft, + issueFromTop + ]) + .attr({ stroke: color, "stroke-width": RELATION_STROKE_WIDTH }) + + if (issueFromRightRel < issueToLeftRel) { + this.#drawPaper + .path([ + "M", + issueFromRightRel + this.#drawLeft, + issueFromTop, + "L", + issueFromRightRel + this.#drawLeft, + issueToTop + ]) + .attr({ stroke: color, "stroke-width": RELATION_STROKE_WIDTH }) + this.#drawPaper + .path([ + "M", + issueFromRightRel + this.#drawLeft, + issueToTop, + "L", + issueToLeft + this.#drawLeft, + issueToTop + ]) + .attr({ stroke: color, "stroke-width": RELATION_STROKE_WIDTH }) + } else { + const issueMiddleTop = issueToTop + issueHeight * (issueFromTop > issueToTop ? 1 : -1) + this.#drawPaper + .path([ + "M", + issueFromRightRel + this.#drawLeft, + issueFromTop, + "L", + issueFromRightRel + this.#drawLeft, + issueMiddleTop + ]) + .attr({ stroke: color, "stroke-width": RELATION_STROKE_WIDTH }) + this.#drawPaper + .path([ + "M", + issueFromRightRel + this.#drawLeft, + issueMiddleTop, + "L", + issueToLeftRel + this.#drawLeft, + issueMiddleTop + ]) + .attr({ stroke: color, "stroke-width": RELATION_STROKE_WIDTH }) + this.#drawPaper + .path([ + "M", + issueToLeftRel + this.#drawLeft, + issueMiddleTop, + "L", + issueToLeftRel + this.#drawLeft, + issueToTop + ]) + .attr({ stroke: color, "stroke-width": RELATION_STROKE_WIDTH }) + this.#drawPaper + .path([ + "M", + issueToLeftRel + this.#drawLeft, + issueToTop, + "L", + issueToLeft + this.#drawLeft, + issueToTop + ]) + .attr({ stroke: color, "stroke-width": RELATION_STROKE_WIDTH }) + } + this.#drawPaper + .path([ + "M", + issueToLeft + this.#drawLeft, + issueToTop, + "l", + -4 * RELATION_STROKE_WIDTH, + -2 * RELATION_STROKE_WIDTH, + "l", + 0, + 4 * RELATION_STROKE_WIDTH, + "z" + ]) + .attr({ + stroke: "none", + fill: color, + "stroke-linecap": "butt", + "stroke-linejoin": "miter" + }) + }) + } + + get #progressLinesArray() { + const lines = [] + const todayLeft = this.$("#today_line").position().left + + lines.push({ left: todayLeft, top: 0 }) + + this.$("div.issue-subject, div.version-name").each((_, element) => { + const $element = this.$(element) + + if (!$element.is(":visible")) return true + + const topPosition = $element.position().top - this.#drawTop + const elementHeight = $element.height() / 9 + const elementTopUpper = topPosition - elementHeight + const elementTopCenter = topPosition + elementHeight * 3 + const elementTopLower = topPosition + elementHeight * 8 + const issueClosed = $element.children("span").hasClass("issue-closed") + const versionClosed = $element.children("span").hasClass("version-closed") + + if (issueClosed || versionClosed) { + lines.push({ left: todayLeft, top: elementTopCenter }) + } else { + const issueDone = this.$(`#task-done-${$element.attr("id")}`) + const isBehindStart = $element.children("span").hasClass("behind-start-date") + const isOverEnd = $element.children("span").hasClass("over-end-date") + + if (isOverEnd) { + lines.push({ left: this.#drawRight, top: elementTopUpper, is_right_edge: true }) + lines.push({ + left: this.#drawRight, + top: elementTopLower, + is_right_edge: true, + none_stroke: true + }) + } else if (issueDone.length > 0) { + const doneLeft = issueDone.first().position().left + issueDone.first().width() + lines.push({ left: doneLeft, top: elementTopCenter }) + } else if (isBehindStart) { + lines.push({ left: 0, top: elementTopUpper, is_left_edge: true }) + lines.push({ + left: 0, + top: elementTopLower, + is_left_edge: true, + none_stroke: true + }) + } else { + let todoLeft = todayLeft + const issueTodo = this.$(`#task-todo-${$element.attr("id")}`) + if (issueTodo.length > 0) { + todoLeft = issueTodo.first().position().left + } + lines.push({ left: Math.min(todayLeft, todoLeft), top: elementTopCenter }) + } + } + }) + + return lines + } + + #drawGanttProgressLines() { + if (this.$("#today_line").length === 0) return + + const progressLines = this.#progressLinesArray + const color = this.$("#today_line").css("border-left-color") || "#ff0000" + + for (let index = 1; index < progressLines.length; index += 1) { + const current = progressLines[index] + const previous = progressLines[index - 1] + + if ( + !current.none_stroke && + !( + (previous.is_right_edge && current.is_right_edge) || + (previous.is_left_edge && current.is_left_edge) + ) + ) { + const x1 = previous.left === 0 ? 0 : previous.left + this.#drawLeft + const x2 = current.left === 0 ? 0 : current.left + this.#drawLeft + + this.#drawPaper + .path(["M", x1, previous.top, "L", x2, current.top]) + .attr({ stroke: color, "stroke-width": 2 }) + } + } + } +} diff --git a/app/javascript/controllers/gantt/column_controller.js b/app/javascript/controllers/gantt/column_controller.js new file mode 100644 index 000000000..ba9bf108b --- /dev/null +++ b/app/javascript/controllers/gantt/column_controller.js @@ -0,0 +1,76 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static values = { + minWidth: Number, + column: String, + // Local value + mobileMode: { type: Boolean, default: false } + } + + #$element = null + + initialize() { + this.$ = window.jQuery + } + + connect() { + this.#$element = this.$(this.element) + this.#setupResizable() + this.#dispatchResizeColumn() + } + + disconnect() { + this.#$element?.resizable("destroy") + this.#$element = null + } + + handleWindowResize(_event) { + this.mobileModeValue = this.#isMobile() + + this.#dispatchResizeColumn() + } + + mobileModeValueChanged(current, old) { + if (current == old) return + + if (this.mobileModeValue) { + this.#$element?.resizable("disable") + } else { + this.#$element?.resizable("enable") + } + } + + #setupResizable() { + const alsoResize = [ + `.gantt_${this.columnValue}_container`, + `.gantt_${this.columnValue}_container > .gantt_hdr` + ] + const options = { + handles: "e", + minWidth: this.minWidthValue, + zIndex: 30, + alsoResize: alsoResize.join(","), + create: () => { + this.$(".ui-resizable-e").css("cursor", "ew-resize") + } + } + + this.#$element + .resizable(options) + .on("resize", (event) => { + event.stopPropagation() + this.#dispatchResizeColumn() + }) + } + + #dispatchResizeColumn() { + if (!this.#$element) return + + this.dispatch(`resize-column-${this.columnValue}`, { detail: { width: this.#$element.width() } }) + } + + #isMobile() { + return !!(typeof window.isMobile === "function" && window.isMobile()) + } +} diff --git a/app/javascript/controllers/gantt/options_controller.js b/app/javascript/controllers/gantt/options_controller.js new file mode 100644 index 000000000..c36018090 --- /dev/null +++ b/app/javascript/controllers/gantt/options_controller.js @@ -0,0 +1,63 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["display", "relations", "progress"] + + static values = { + unavailableColumns: Array + } + + initialize() { + this.$ = window.jQuery + } + + connect() { + this.#dispatchInitialStates() + this.#disableUnavailableColumns() + } + + toggleDisplay(event) { + this.dispatch("toggle-display", { + detail: { enabled: event.currentTarget.checked } + }) + } + + toggleRelations(event) { + this.dispatch("toggle-relations", { + detail: { enabled: event.currentTarget.checked } + }) + } + + toggleProgress(event) { + this.dispatch("toggle-progress", { + detail: { enabled: event.currentTarget.checked } + }) + } + + #dispatchInitialStates() { + if (this.hasDisplayTarget) { + this.dispatch("toggle-display", { + detail: { enabled: this.displayTarget.checked } + }) + } + if (this.hasRelationsTarget) { + this.dispatch("toggle-relations", { + detail: { enabled: this.relationsTarget.checked } + }) + } + if (this.hasProgressTarget) { + this.dispatch("toggle-progress", { + detail: { enabled: this.progressTarget.checked } + }) + } + } + + #disableUnavailableColumns() { + if (!Array.isArray(this.unavailableColumnsValue)) { + return + } + this.unavailableColumnsValue.forEach((column) => { + this.$("#available_c, #selected_c").children(`[value='${column}']`).prop("disabled", true) + }) + } +} diff --git a/app/javascript/controllers/gantt/subjects_controller.js b/app/javascript/controllers/gantt/subjects_controller.js new file mode 100644 index 000000000..8fdbb0954 --- /dev/null +++ b/app/javascript/controllers/gantt/subjects_controller.js @@ -0,0 +1,122 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + initialize() { + this.$ = window.jQuery + } + + handleResizeColumn(event) { + const columnWidth = event.detail.width; + + this.$(".issue-subject, .project-name, .version-name").each((_, element) => { + const $element = this.$(element) + $element.width(columnWidth - $element.position().left) + }) + } + + handleEntryClick(event) { + const iconExpander = event.currentTarget + const $subject = this.$(iconExpander.parentElement) + const subjectLeft = + parseInt($subject.css("left"), 10) + parseInt(iconExpander.offsetWidth, 10) + + let targetShown = null + let targetTop = 0 + let totalHeight = 0 + let outOfHierarchy = false + + const willOpen = !$subject.hasClass("open") + + this.#setIconState($subject, willOpen) + + $subject.nextAll("div").each((_, element) => { + const $element = this.$(element) + const json = $element.data("collapse-expand") + const numberOfRows = $element.data("number-of-rows") + const barsSelector = `#gantt_area form > div[data-collapse-expand='${json.obj_id}'][data-number-of-rows='${numberOfRows}']` + const selectedColumnsSelector = `td.gantt_selected_column div[data-collapse-expand='${json.obj_id}'][data-number-of-rows='${numberOfRows}']` + + if (outOfHierarchy || parseInt($element.css("left"), 10) <= subjectLeft) { + outOfHierarchy = true + + if (targetShown === null) return false + + const newTopVal = parseInt($element.css("top"), 10) + totalHeight * (targetShown ? -1 : 1) + + $element.css("top", newTopVal) + this.$([barsSelector, selectedColumnsSelector].join()).each((__, el) => { + this.$(el).css("top", newTopVal) + }) + + return true + } + + const isShown = $element.is(":visible") + + if (targetShown === null) { + targetShown = isShown + targetTop = parseInt($element.css("top"), 10) + totalHeight = 0 + } + + if (isShown === targetShown) { + this.$(barsSelector).each((__, task) => { + const $task = this.$(task) + + if (!isShown && willOpen) { + $task.css("top", targetTop + totalHeight) + } + if (!$task.hasClass("tooltip")) { + $task.toggle(willOpen) + } + }) + + this.$(selectedColumnsSelector).each((__, attr) => { + const $attr = this.$(attr) + + if (!isShown && willOpen) { + $attr.css("top", targetTop + totalHeight) + } + $attr.toggle(willOpen) + }) + + if (!isShown && willOpen) { + $element.css("top", targetTop + totalHeight) + } + + this.#setIconState($element, willOpen) + $element.toggle(willOpen) + totalHeight += parseInt(json.top_increment, 10) + } + }) + + this.dispatch("toggle-tree", { bubbles: true }) + } + + #setIconState(element, open) { + const $element = element.jquery ? element : this.$(element) + const expander = $element.find(".expander") + + if (open) { + $element.addClass("open") + + if (expander.length > 0) { + expander.removeClass("icon-collapsed").addClass("icon-expanded") + + if (expander.find("svg").length === 1) { + window.updateSVGIcon(expander[0], "angle-down") + } + } + } else { + $element.removeClass("open") + + if (expander.length > 0) { + expander.removeClass("icon-expanded").addClass("icon-collapsed") + + if (expander.find("svg").length === 1) { + window.updateSVGIcon(expander[0], "angle-right") + } + } + } + } +} diff --git a/app/views/gantts/_chart.html.erb b/app/views/gantts/_chart.html.erb new file mode 100644 index 000000000..870096870 --- /dev/null +++ b/app/views/gantts/_chart.html.erb @@ -0,0 +1,255 @@ +<% + zoom = 1 + gantt.zoom.times { zoom *= 2 } + + subject_width = 330 + header_height = 18 + + headers_height = header_height + show_weeks = false + show_days = false + show_day_num = false + + if gantt.zoom > 1 + show_weeks = true + headers_height = 2 * header_height + if gantt.zoom > 2 + show_days = true + headers_height = 3 * header_height + if gantt.zoom > 3 + show_day_num = true + headers_height = 4 * header_height + end + end + end + + g_width = ((gantt.date_to - gantt.date_from + 1) * zoom).to_i + gantt.render( + top: headers_height + 8, + zoom: zoom, + g_width: g_width, + subject_width: subject_width + ) + g_height = [(20 * (gantt.number_of_rows + 6)) + 150, 206].max + t_height = g_height + headers_height +%> + +<% if gantt.truncated %> +
<%= l(:notice_gantt_chart_truncated, max: gantt.max_rows) %>
+<% end %> + +<%= gantt_chart_tag(@query) do %> +<%= l(:notice_gantt_chart_truncated, :max => @gantt.max_rows) %>
+<%= render partial: 'chart', locals: {gantt: @gantt, query: @query} %> <% end %> -| - <% - style = "" - style += "position:relative;" - style += "height: #{t_height + 24}px;" - style += "width: #{subject_width + 1}px;" - %> - <%= content_tag(:div, :style => style, :class => "gantt_subjects_container #{'draw_selected_columns' if @query.draw_selected_columns}") do %> - <% - style = "" - style += "width: #{subject_width + 1}px;" - style += "height: #{headers_height}px;" - style += 'background: #f1f3f5;' # oc-gray-1 - %> - <%= content_tag(:div, "", :style => style, :class => "gantt_hdr") %> - <% - style = "" - style += "z-index: 1;" - style += "width: #{subject_width + 1}px;" - style += "height: #{t_height}px;" - style += 'overflow: hidden;' - %> - <%= content_tag(:div, "", :style => style, :class => "gantt_hdr") %> - <%= content_tag(:div, :class => "gantt_subjects") do %> - <%= form_tag({}, :data => {:cm_url => issues_context_menu_path}) do -%> - <%= hidden_field_tag 'back_url', url_for(:params => request.query_parameters), :id => nil %> - <%= @gantt.subjects.html_safe %> - <% end %> - <% end %> - <% end %> - | -<% - @query.columns.each do |column| - next if Redmine::Helpers::Gantt::UNAVAILABLE_COLUMNS.include?(column.name) - column_name = column.name.to_s.tr('.', '_') -%> -- <% - style = "position: relative;" - style += "height: #{t_height + 24}px;" - %> - <%= content_tag(:div, :style => style, :class => "gantt_#{column_name}_container gantt_selected_column_container") do %> - <% - style = "height: #{t_height}px;" - style += 'overflow: hidden;' - %> - <%= content_tag(:div, '', :style => style, :class => "gantt_hdr") %> - <% - style = "height: #{headers_height}px;" - style += 'background: #f1f3f5;' # oc-gray-1 - %> - <%= content_tag(:div, content_tag(:p, column.caption, :class => 'gantt_hdr_selected_column_name'), :style => style, :class => "gantt_hdr") %> - <%= content_tag(:div, :class => "gantt_#{column_name} gantt_selected_column_content") do %> - <%= @gantt.selected_column_content({:column => column, :top => headers_height + 8, :zoom => zoom, :g_width => g_width}).html_safe %> - <% end %> - <% end %> - | -<% end %> -
-
-<%
- style = ""
- style += "width: #{g_width - 1}px;"
- style += "height: #{headers_height}px;"
- style += 'background: #f1f3f5;' # oc-gray-1
-%>
-<%= content_tag(:div, ' '.html_safe, :style => style, :class => "gantt_hdr") %>
-
-<% ###### Months headers ###### %>
-<%
- month_f = @gantt.date_from
- left = 0
- height = (show_weeks ? header_height : header_height + g_height)
-%>
-<% @gantt.months.times do %>
- <%
- width = (((month_f >> 1) - month_f) * zoom - 1).to_i
- style = ""
- style += "left: #{left}px;"
- style += "width: #{width}px;"
- style += "height: #{height}px;"
- %>
- <%= content_tag(:div, :style => style, :class => "gantt_hdr") do %>
- <%= link_to "#{month_f.year}-#{month_f.month}",
- @gantt.params.merge(:year => month_f.year, :month => month_f.month),
- :title => "#{month_name(month_f.month)} #{month_f.year}" %>
- <% end %>
- <%
- left = left + width + 1
- month_f = month_f >> 1
- %>
-<% end %>
-
-<% ###### Weeks headers ###### %>
-<% if show_weeks %>
- <%
- left = 0
- height = (show_days ? header_height - 1 : header_height - 1 + g_height)
- %>
- <% if @gantt.date_from.cwday == 1 %>
- <%
- # @date_from is monday
- week_f = @gantt.date_from
- %>
- <% else %>
- <%
- # find next monday after @date_from
- week_f = @gantt.date_from + (7 - @gantt.date_from.cwday + 1)
- width = (7 - @gantt.date_from.cwday + 1) * zoom - 1
- style = ""
- style += "left: #{left}px;"
- style += "top: 19px;"
- style += "width: #{width}px;"
- style += "height: #{height}px;"
- %>
- <%= content_tag(:div, ' '.html_safe,
- :style => style, :class => "gantt_hdr") %>
- <% left = left + width + 1 %>
- <% end %>
- <% while week_f <= @gantt.date_to %>
- <%
- width = ((week_f + 6 <= @gantt.date_to) ?
- 7 * zoom - 1 :
- (@gantt.date_to - week_f + 1) * zoom - 1).to_i
- style = ""
- style += "left: #{left}px;"
- style += "top: 19px;"
- style += "width: #{width}px;"
- style += "height: #{height}px;"
- %>
- <%= content_tag(:div, :style => style, :class => "gantt_hdr") do %>
- <%= content_tag(:small) do %>
- <%= week_f.cweek if width >= 16 %>
- <% end %>
- <% end %>
- <%
- left = left + width + 1
- week_f = week_f + 7
- %>
- <% end %>
-<% end %>
-
-<% ###### Day numbers headers ###### %>
-<% if show_day_num %>
- <%
- left = 0
- height = g_height + header_height*2 - 1
- wday = @gantt.date_from.cwday
- day_num = @gantt.date_from
- %>
- <% (@gantt.date_to - @gantt.date_from + 1).to_i.times do %>
- <%
- width = zoom - 1
- style = ""
- style += "left:#{left}px;"
- style += "top:37px;"
- style += "width:#{width}px;"
- style += "height:#{height}px;"
- style += "font-size:0.7em;"
- clss = +"gantt_hdr"
- clss << " nwday" if @gantt.non_working_week_days.include?(wday)
- %>
- <%= content_tag(:div, :style => style, :class => clss) do %>
- <%= day_num.day %>
- <% end %>
- <%
- left = left + width+1
- day_num = day_num + 1
- wday = wday + 1
- wday = 1 if wday > 7
- %>
- <% end %>
-<% end %>
-
-<% ###### Days headers ####### %>
-<% if show_days %>
- <%
- left = 0
- height = g_height + header_height - 1
- top = (show_day_num ? 55 : 37)
- %>
- <% (@gantt.date_from..@gantt.date_to).each do |g_date| %>
- <%
- width = zoom - 1
- style = ""
- style += "left: #{left}px;"
- style += "top: #{top}px;"
- style += "width: #{width}px;"
- style += "height: #{height}px;"
- style += "font-size:0.7em;"
- clss = +"gantt_hdr"
- clss << " nwday" if @gantt.non_working_week_days.include?(g_date.cwday)
- %>
- <%= content_tag(:div, :style => style, :class => clss) do %>
- <%= day_letter(g_date.cwday) %>
- <% end %>
- <%
- left = left + width + 1
- %>
- <% end %>
-<% end %>
-
-<%= form_tag({}, :data => {:cm_url => issues_context_menu_path}) do -%>
- <%= hidden_field_tag 'back_url', url_for(:params => request.query_parameters), :id => nil %>
- <%= @gantt.lines.html_safe %>
-<% end %>
-
-<% ###### Today red line (excluded from cache) ###### %>
-<% if User.current.today >= @gantt.date_from and User.current.today <= @gantt.date_to %>
- <%
- today_left = (((User.current.today - @gantt.date_from + 1) * zoom).floor() - 1).to_i
- style = ""
- style += "position: absolute;"
- style += "height: #{g_height}px;"
- style += "top: #{headers_height + 1}px;"
- style += "left: #{today_left}px;"
- style += "width:10px;"
- style += "border-left: 1px dashed red;"
- %>
- <%= content_tag(:div, ' '.html_safe, :style => style, :id => 'today_line') %>
-<% end %>
-<%
- style = ""
- style += "position: absolute;"
- style += "height: #{g_height}px;"
- style += "top: #{headers_height + 1}px;"
- style += "left: 0px;"
- style += "width: #{g_width - 1}px;"
-%>
-<%= content_tag(:div, '', :style => style, :id => "gantt_draw_area") %>
-
- |
-