Extract Gantt view structure and wire Stimulus controllers (#43397).

Patch by Katsuya HIDAKA (user:hidakatsuya).


git-svn-id: https://svn.redmine.org/redmine/trunk@24085 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
Go MAEDA
2025-10-29 02:35:11 +00:00
parent 53ac36c3b8
commit 3775bb31d6
13 changed files with 1339 additions and 881 deletions

View File

@@ -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);
});
}

View File

@@ -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 {

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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 })
}
}
}
}

View File

@@ -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())
}
}

View File

@@ -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)
})
}
}

View File

@@ -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")
}
}
}
}
}

View File

@@ -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 %>
<p class="warning"><%= l(:notice_gantt_chart_truncated, max: gantt.max_rows) %></p>
<% end %>
<%= gantt_chart_tag(@query) do %>
<tr>
<%= gantt_column_tag('subjects', min_width: 100,
style: "width:#{query.draw_selected_columns ? subject_width + 1 : subject_width + 2}px;") do %>
<%
subjects_container_classes = "gantt_subjects_container"
subjects_container_classes << " draw_selected_columns" if query.draw_selected_columns
subjects_container_style = +"position:relative;"
subjects_container_style << "height: #{t_height + 24}px;"
subjects_container_style << "width: #{subject_width + 1}px;"
%>
<%= content_tag(:div,
style: subjects_container_style,
class: subjects_container_classes,
data: {'gantt--chart-target': 'subjectsContainer'}) do %>
<%
header_bg_style = +"width: #{subject_width + 1}px;"
header_bg_style << "height: #{headers_height}px;"
header_bg_style << 'background: #f1f3f5;' # oc-gray-1
%>
<%= content_tag(:div, "", style: header_bg_style, class: "gantt_hdr") %>
<%
header_overlay_style = +"z-index: 1;"
header_overlay_style << "width: #{subject_width + 1}px;"
header_overlay_style << "height: #{t_height}px;"
header_overlay_style << 'overflow: hidden;'
%>
<%= content_tag(:div, "", style: header_overlay_style, class: "gantt_hdr") %>
<%= gantt_subjects_tag 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 %>
<% end %>
<%
query.columns.each do |column|
next if Redmine::Helpers::Gantt::UNAVAILABLE_COLUMNS.include?(column.name)
column_name = column.name.to_s.tr('.', '_')
%>
<%= gantt_column_tag(column_name, min_width: 20, id: column_name,
class: ['gantt_selected_column', { 'last_gantt_selected_column': query.columns.last == column }]) do %>
<%
column_container_style = +"position: relative;"
column_container_style << "height: #{t_height + 24}px;"
%>
<%= content_tag(:div, style: column_container_style, class: "gantt_#{column_name}_container gantt_selected_column_container") do %>
<%
column_header_overlay_style = +"height: #{t_height}px;"
column_header_overlay_style << 'overflow: hidden;'
%>
<%= content_tag(:div, '', style: column_header_overlay_style, class: "gantt_hdr") %>
<%
column_header_style = +"height: #{headers_height}px;"
column_header_style << 'background: #f1f3f5;' # oc-gray-1
%>
<%= content_tag(:div,
content_tag(:p, column.caption, class: 'gantt_hdr_selected_column_name'),
style: column_header_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 %>
<% end %>
<td>
<div style="position:relative;height:<%= t_height + 24 %>px;overflow:auto;" id="gantt_area" data-gantt--chart-target="ganttArea">
<%
months_header_style = +"width: #{g_width - 1}px;"
months_header_style << "height: #{headers_height}px;"
months_header_style << 'background: #f1f3f5;' # oc-gray-1
%>
<%= content_tag(:div, '&nbsp;'.html_safe, style: months_header_style, class: "gantt_hdr") %>
<% month_f = gantt.date_from %>
<% left = 0 %>
<% months_height = (show_weeks ? header_height : header_height + g_height) %>
<% gantt.months.times do %>
<% width = (((month_f >> 1) - month_f) * zoom - 1).to_i %>
<% month_style = +"left: #{left}px;" %>
<% month_style << "width: #{width}px;" %>
<% month_style << "height: #{months_height}px;" %>
<%= content_tag(:div, style: month_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 += width + 1 %>
<% month_f = month_f >> 1 %>
<% end %>
<% if show_weeks %>
<% left = 0 %>
<% weeks_height = (show_days ? header_height - 1 : header_height - 1 + g_height) %>
<% if gantt.date_from.cwday == 1 %>
<% week_f = gantt.date_from %>
<% else %>
<% week_f = gantt.date_from + (7 - gantt.date_from.cwday + 1) %>
<% width = (7 - gantt.date_from.cwday + 1) * zoom - 1 %>
<% gap_style = +"left: #{left}px;" %>
<% gap_style << "top: 19px;" %>
<% gap_style << "width: #{width}px;" %>
<% gap_style << "height: #{weeks_height}px;" %>
<%= content_tag(:div, '&nbsp;'.html_safe, style: gap_style, class: "gantt_hdr") %>
<% 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 %>
<% week_style = +"left: #{left}px;" %>
<% week_style << "top: 19px;" %>
<% week_style << "width: #{width}px;" %>
<% week_style << "height: #{weeks_height}px;" %>
<%= content_tag(:div, style: week_style, class: "gantt_hdr") do %>
<%= content_tag(:small) do %>
<%= week_f.cweek if width >= 16 %>
<% end %>
<% end %>
<% left += width + 1 %>
<% week_f += 7 %>
<% end %>
<% end %>
<% if show_day_num %>
<% left = 0 %>
<% days_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 %>
<% day_style = +"left:#{left}px;" %>
<% day_style << "top:37px;" %>
<% day_style << "width:#{width}px;" %>
<% day_style << "height:#{days_height}px;" %>
<% day_style << "font-size:0.7em;" %>
<% day_classes = +"gantt_hdr" %>
<% day_classes << " nwday" if gantt.non_working_week_days.include?(wday) %>
<%= content_tag(:div, style: day_style, class: day_classes) do %>
<%= day_num.day %>
<% end %>
<% left += width + 1 %>
<% day_num += 1 %>
<% wday += 1 %>
<% wday = 1 if wday > 7 %>
<% end %>
<% end %>
<% if show_days %>
<% left = 0 %>
<% days_height = g_height + header_height - 1 %>
<% days_top = (show_day_num ? 55 : 37) %>
<% (gantt.date_from..gantt.date_to).each do |g_date| %>
<% width = zoom - 1 %>
<% day_style = +"left: #{left}px;" %>
<% day_style << "top: #{days_top}px;" %>
<% day_style << "width: #{width}px;" %>
<% day_style << "height: #{days_height}px;" %>
<% day_style << "font-size:0.7em;" %>
<% day_classes = +"gantt_hdr" %>
<% day_classes << " nwday" if gantt.non_working_week_days.include?(g_date.cwday) %>
<%= content_tag(:div, style: day_style, class: day_classes) do %>
<%= day_letter(g_date.cwday) %>
<% end %>
<% 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 %>
<% if User.current.today >= gantt.date_from && User.current.today <= gantt.date_to %>
<% today_left = (((User.current.today - gantt.date_from + 1) * zoom).floor - 1).to_i %>
<% today_style = +"position: absolute;" %>
<% today_style << "height: #{g_height}px;" %>
<% today_style << "top: #{headers_height + 1}px;" %>
<% today_style << "left: #{today_left}px;" %>
<% today_style << "width:10px;" %>
<% today_style << "border-left: 1px dashed red;" %>
<%= content_tag(:div, '&nbsp;'.html_safe, style: today_style, id: 'today_line') %>
<% end %>
<%
draw_area_style = +"position: absolute;"
draw_area_style << "height: #{g_height}px;"
draw_area_style << "top: #{headers_height + 1}px;"
draw_area_style << 'left: 0px;'
draw_area_style << "width: #{g_width - 1}px;"
%>
<%= content_tag(:div, '', style: draw_area_style, id: "gantt_draw_area", data: {'gantt--chart-target': 'drawArea'}) %>
</div>
</td>
</tr>
<% end %>
<span class="pagination">
<ul class="pages">
<li class="previous page">
<%= link_to("\xc2\xab " + l(:label_previous),
{params: request.query_parameters.merge(gantt.params_previous)},
accesskey: accesskey(:previous)) %>
</li><li class="next page">
<%= link_to(l(:label_next) + " \xc2\xbb",
{params: request.query_parameters.merge(gantt.params_next)},
accesskey: accesskey(:next)) %>
</li>
</ul>
</span>
<% other_formats_links do |f| %>
<%= f.link_to_with_query_parameters 'PDF', gantt.params %>
<%= f.link_to_with_query_parameters('PNG', gantt.params) if gantt.respond_to?('to_image') %>
<% end %>

View File

@@ -0,0 +1,126 @@
<%= form_tag(
{controller: 'gantts', action: 'show', project_id: project, month: params[:month], year: params[:year], months: params[:months]},
method: :get,
id: 'query_form',
data: {gantt_target: 'form'}
) do %>
<%= hidden_field_tag 'set_filter', '1' %>
<%= hidden_field_tag 'gantt', '1' %>
<div id="query_form_with_buttons" class="hide-when-print">
<div id="query_form_content">
<fieldset id="filters" class="collapsible <%= query.new_record? ? '' : 'collapsed' %>">
<legend onclick="toggleFieldset(this);" class="icon icon-<%= query.new_record? ? 'expanded' : 'collapsed' %>">
<%= sprite_icon(query.new_record? ? 'angle-down' : 'angle-right', rtl: !query.new_record?) %>
<%= l(:label_filter_plural) %>
</legend>
<div style="<%= query.new_record? ? '' : 'display: none;' %>">
<%= render partial: 'queries/filters', locals: {query: query} %>
</div>
</fieldset>
<fieldset id="options" class="collapsible collapsed"
data-controller="gantt--options"
data-gantt--options-unavailable-columns-value="<%= Redmine::Helpers::Gantt::UNAVAILABLE_COLUMNS.map(&:to_s).to_json %>">
<legend onclick="toggleFieldset(this);" class="icon icon-collapsed">
<%= sprite_icon('angle-right', rtl: true) %>
<%= l(:label_options) %>
</legend>
<div style="display: none;">
<div>
<fieldset>
<legend><%= l(:field_column_names) %></legend>
<div id="list-definition">
<div>
<label for="draw_selected_columns">
<%= check_box 'query',
'draw_selected_columns',
id: 'draw_selected_columns',
data: {
enables: '#list-definition .query-columns select, #list-definition .query-columns input',
action: 'change->gantt--options#toggleDisplay',
'gantt--options-target': 'display'
} %>
<%= l(:label_display) %>
</label>
</div>
<div>
<%= render_query_columns_selection(query) %>
</div>
</div>
</fieldset>
<fieldset>
<legend><%= l(:label_related_issues) %></legend>
<label for="draw_relations">
<%= check_box 'query',
'draw_relations',
id: 'draw_relations',
data: {
action: 'change->gantt--options#toggleRelations',
'gantt--options-target': 'relations'
} %>
<% [IssueRelation::TYPE_BLOCKS, IssueRelation::TYPE_PRECEDES].each do |rel| %>
<% color = Redmine::Helpers::Gantt::DRAW_TYPES[rel][:color] %>
<%= content_tag(:span, '&nbsp;&nbsp;&nbsp;'.html_safe, style: "background-color: #{color}") %>
<%= l(IssueRelation::TYPES[rel][:name]) %>
<% end %>
</label>
</fieldset>
<fieldset>
<legend><%= l(:label_gantt_progress_line) %></legend>
<label for="draw_progress_line">
<%= check_box 'query',
'draw_progress_line',
id: 'draw_progress_line',
data: {
action: 'change->gantt--options#toggleProgress',
'gantt--options-target': 'progress'
} %>
<%= l(:label_display) %>
</label>
</fieldset>
</div>
</div>
</fieldset>
</div>
<p class="contextual">
<span>
<%= gantt_zoom_link(gantt, :in) %>
<%= gantt_zoom_link(gantt, :out) %>
</span>
<span>
<%= link_to_previous_month(gantt.year_from, gantt.month_from, accesskey: accesskey(:previous)) %> |
<%= link_to_next_month(gantt.year_from, gantt.month_from, accesskey: accesskey(:next)) %>
</span>
</p>
<p class="buttons">
<%= number_field_tag 'months', gantt.months, min: 1, max: Setting.gantt_months_limit.to_i, autocomplete: false %>
<%= l(:label_months_from) %>
<%= select_month(gantt.month_from, prefix: 'month', discard_type: true) %>
<%= select_year(gantt.year_from, prefix: 'year', discard_type: true) %>
<%= hidden_field_tag 'zoom', gantt.zoom %>
<%= link_to_function sprite_icon('checked', l(:button_apply)), '$("#query_form").submit()',
class: 'icon icon-checked' %>
<%= link_to sprite_icon('reload', l(:button_clear)), {project_id: project, set_filter: 1},
class: 'icon icon-reload' %>
<% if query.new_record? && User.current.allowed_to?(:save_queries, project, global: true) %>
<%= link_to_function sprite_icon('save', l(:button_save_object, object_name: l(:label_query)).capitalize),
"$('#query_form').attr('action', '#{ project ? new_project_query_path(project) : new_query_path }').submit();",
class: 'icon icon-save' %>
<% end %>
<% if !query.new_record? && query.editable_by?(User.current) %>
<%= link_to sprite_icon('edit', l(:button_edit_object, object_name: l(:label_query)).capitalize),
edit_query_path(query, gantt: 1),
class: 'icon icon-edit' %>
<%= delete_link query_path(query, gantt: 1), {}, l(:button_delete_object, object_name: l(:label_query)).capitalize %>
<% end %>
</p>
</div>
<% end %>

View File

@@ -1,433 +1,29 @@
<% @gantt.view = self %>
<div class="contextual">
</div>
<div class="contextual"></div>
<h2><%= @query.new_record? ? l(:label_gantt) : @query.name %></h2>
<%= @query.persisted? && @query.description.present? ? content_tag('p', @query.description, class: 'subtitle') : '' %>
<%= form_tag({:controller => 'gantts', :action => 'show',
:project_id => @project, :month => params[:month],
:year => params[:year], :months => params[:months]},
:method => :get, :id => 'query_form') do %>
<%= hidden_field_tag 'set_filter', '1' %>
<%= hidden_field_tag 'gantt', '1' %>
<div id="query_form_with_buttons" class="hide-when-print">
<div id="query_form_content">
<fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
<legend onclick="toggleFieldset(this);" class="icon icon-<%= @query.new_record? ? "expanded" : "collapsed" %>">
<%= sprite_icon(@query.new_record? ? "angle-down" : "angle-right", rtl: !@query.new_record?) %>
<%= l(:label_filter_plural) %>
</legend>
<div style="<%= @query.new_record? ? "" : "display: none;" %>">
<%= render :partial => 'queries/filters', :locals => {:query => @query} %>
</div>
</fieldset>
<fieldset id="options" class="collapsible collapsed">
<legend onclick="toggleFieldset(this);" class="icon icon-collapsed">
<%= sprite_icon("angle-right", rtl: true) %>
<%= l(:label_options) %>
</legend>
<div style="display: none;">
<div>
<fieldset>
<legend>
<%= l(:field_column_names) %>
</legend>
<div id="list-definition">
<div>
<label for="draw_selected_columns">
<%= check_box 'query', 'draw_selected_columns', :id => 'draw_selected_columns', 'data-enables' => '#list-definition .query-columns select, #list-definition .query-columns input' %>
<%= l(:label_display) %>
</label>
</div>
<div>
<%= render_query_columns_selection(@query) %>
</div>
</div>
</fieldset>
<fieldset>
<legend><%= l(:label_related_issues) %></legend>
<label for="draw_relations">
<%= check_box 'query', 'draw_relations', :id => 'draw_relations' %>
<% rels = [IssueRelation::TYPE_BLOCKS, IssueRelation::TYPE_PRECEDES] %>
<% rels.each do |rel| %>
<% color = Redmine::Helpers::Gantt::DRAW_TYPES[rel][:color] %>
<%= content_tag(:span, '&nbsp;&nbsp;&nbsp;'.html_safe,
:style => "background-color: #{color}") %>
<%= l(IssueRelation::TYPES[rel][:name]) %>
<% end %>
</label>
</fieldset>
<fieldset>
<legend><%= l(:label_gantt_progress_line) %></legend>
<label for="draw_progress_line">
<%= check_box 'query', 'draw_progress_line', :id => 'draw_progress_line' %>
<%= l(:label_display) %>
</label>
</fieldset>
</div>
</div>
</fieldset>
</div>
<p class="contextual">
<span>
<%= gantt_zoom_link(@gantt, :in) %>
<%= gantt_zoom_link(@gantt, :out) %>
</span>
<span>
<%= link_to_previous_month(@gantt.year_from, @gantt.month_from, :accesskey => accesskey(:previous)) %> | <%= link_to_next_month(@gantt.year_from, @gantt.month_from, :accesskey => accesskey(:next)) %>
</span>
</p>
<p class="buttons">
<%= number_field_tag 'months', @gantt.months, :min => 1, :max => Setting.gantt_months_limit.to_i, :autocomplete => false %>
<%= l(:label_months_from) %>
<%= select_month(@gantt.month_from, :prefix => "month", :discard_type => true) %>
<%= select_year(@gantt.year_from, :prefix => "year", :discard_type => true) %>
<%= hidden_field_tag 'zoom', @gantt.zoom %>
<%= link_to_function sprite_icon('checked', l(:button_apply)), '$("#query_form").submit()',
:class => 'icon icon-checked' %>
<%= link_to sprite_icon('reload', l(:button_clear)), { :project_id => @project, :set_filter => 1 },
:class => 'icon icon-reload' %>
<% if @query.new_record? && User.current.allowed_to?(:save_queries, @project, :global => true) %>
<%= link_to_function sprite_icon('save', l(:button_save_object, object_name: l(:label_query)).capitalize),
"$('#query_form').attr('action', '#{ @project ? new_project_query_path(@project) : new_query_path }').submit();",
:class => 'icon icon-save' %>
<% end %>
<% if !@query.new_record? && @query.editable_by?(User.current) %>
<%= link_to sprite_icon('edit', l(:button_edit_object, object_name: l(:label_query)).capitalize), edit_query_path(@query, :gantt => 1), :class => 'icon icon-edit' %>
<%= delete_link query_path(@query, :gantt => 1), {}, l(:button_delete_object, object_name: l(:label_query)).capitalize %>
<% end %>
</p>
</div>
<% if @query.persisted? && @query.description.present? %>
<%= content_tag('p', @query.description, class: 'subtitle') %>
<% end %>
<%= render partial: 'query_form', locals: {project: @project, query: @query, gantt: @gantt} %>
<%= error_messages_for 'query' %>
<% if @query.valid? %>
<%
zoom = 1
@gantt.zoom.times { zoom = 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
# Width of the entire chart
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 %>
<p class="warning"><%= l(:notice_gantt_chart_truncated, :max => @gantt.max_rows) %></p>
<%= render partial: 'chart', locals: {gantt: @gantt, query: @query} %>
<% end %>
<table class='gantt-table'>
<tr>
<td style="width:<%= @query.draw_selected_columns ? subject_width + 1 : subject_width + 2 %>px;" class="gantt_subjects_column">
<%
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 %>
</td>
<%
@query.columns.each do |column|
next if Redmine::Helpers::Gantt::UNAVAILABLE_COLUMNS.include?(column.name)
column_name = column.name.to_s.tr('.', '_')
%>
<td class="gantt_<%= column_name %>_column gantt_selected_column <%= 'last_gantt_selected_column' if @query.columns.last == column %>" id="<%= column_name %>">
<%
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 %>
</td>
<% end %>
<td>
<div style="position:relative;height:<%= t_height + 24 %>px;overflow:auto;" id="gantt_area">
<%
style = ""
style += "width: #{g_width - 1}px;"
style += "height: #{headers_height}px;"
style += 'background: #f1f3f5;' # oc-gray-1
%>
<%= content_tag(:div, '&nbsp;'.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, '&nbsp;'.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, '&nbsp;'.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") %>
</div>
</td>
</tr>
</table>
<span class="pagination">
<ul class="pages">
<li class="previous page">
<%= link_to("\xc2\xab " + l(:label_previous),
{:params => request.query_parameters.merge(@gantt.params_previous)},
:accesskey => accesskey(:previous)) %>
</li><li class="next page">
<%= link_to(l(:label_next) + " \xc2\xbb",
{:params => request.query_parameters.merge(@gantt.params_next)},
:accesskey => accesskey(:next)) %>
</li>
</ul>
</span>
<% other_formats_links do |f| %>
<%= f.link_to_with_query_parameters 'PDF', @gantt.params %>
<%= f.link_to_with_query_parameters('PNG', @gantt.params) if @gantt.respond_to?('to_image') %>
<% end %>
<% end # query.valid? %>
<% content_for :sidebar do %>
<%= render :partial => 'issues/sidebar' %>
<%= render partial: 'issues/sidebar' %>
<% end %>
<% html_title(l(:label_gantt)) -%>
<% html_title l(:label_gantt) %>
<% content_for :header_tags do %>
<%= stylesheet_link_tag 'gantt', media: 'all' %>
<%= javascript_include_tag 'raphael' %>
<%= javascript_include_tag 'gantt' %>
<% end %>
<%= javascript_tag do %>
var issue_relation_type = <%= raw Redmine::Helpers::Gantt::DRAW_TYPES.to_json %>;
$(function() {
disable_unavailable_columns('<%= Redmine::Helpers::Gantt::UNAVAILABLE_COLUMNS.map(&:to_s).join(',') %>'.split(','));
drawGanttHandler();
resizableSubjectColumn();
drawSelectedColumns();
$("#draw_relations, #draw_progress_line, #draw_selected_columns").change(drawGanttHandler);
$('div.gantt_subjects .expander').on('click', ganttEntryClick);
});
$(window).resize(function() {
drawGanttHandler();
resizableSubjectColumn();
});
<% end %>
<%= context_menu %>

View File

@@ -816,7 +816,10 @@ module Redmine
}
end
if has_children
content = view.content_tag(:span, view.sprite_icon('angle-down').html_safe, :class => 'icon icon-expanded expander') + content
content = view.content_tag(:span,
view.sprite_icon('angle-down').html_safe,
:class => 'icon icon-expanded expander',
:data => {:action => 'click->gantt--subjects#handleEntryClick'}) + content
tag_options[:class] += ' open'
else
if params[:indent]

View File

@@ -2,7 +2,7 @@
require_relative '../application_system_test_case'
class GanttSystemTest < ApplicationSystemTestCase
class GanttsTest < ApplicationSystemTestCase
setup do
log_user('jsmith', 'jsmith')
end
@@ -11,33 +11,33 @@ class GanttSystemTest < ApplicationSystemTestCase
visit_gantt
expand_options
assert_no_selector('td#status', visible: :visible)
assert_no_selector('td#priority', visible: :visible)
assert_no_selector('td#assigned_to', visible: :visible)
assert_no_selector('td#updated_on', visible: :visible)
assert_no_selector 'td#status'
assert_no_selector 'td#priority'
assert_no_selector 'td#assigned_to'
assert_no_selector 'td#updated_on'
find('#draw_selected_columns').check
assert_selector('div.gantt_subjects_container.draw_selected_columns')
assert_selector('td#status', visible: :visible)
assert_selector('td#priority', visible: :visible)
assert_selector('td#assigned_to', visible: :visible)
assert_selector('td#updated_on', visible: :visible)
assert_selector 'div.gantt_subjects_container.draw_selected_columns'
assert_selector 'td#status'
assert_selector 'td#priority'
assert_selector 'td#assigned_to'
assert_selector 'td#updated_on'
end
test 'related issues toggle displays and hides relation arrows' do
visit_gantt
expand_options
assert_selector('#gantt_draw_area path', minimum: 1)
assert_selector '#gantt_draw_area path', minimum: 1
find('#draw_relations').uncheck
assert_no_selector('#gantt_draw_area path')
assert_no_selector '#gantt_draw_area path'
find('#draw_relations').check
assert_selector('#gantt_draw_area path', minimum: 1)
assert_selector '#gantt_draw_area path', minimum: 1
end
test 'progress line toggle draws zigzag line' do
@@ -45,11 +45,11 @@ class GanttSystemTest < ApplicationSystemTestCase
expand_options
find('#draw_relations').uncheck
assert_no_selector('#gantt_draw_area path')
assert_no_selector '#gantt_draw_area path'
find('#draw_progress_line').check
assert_selector('#gantt_draw_area path', minimum: 1)
assert_selector '#gantt_draw_area path', minimum: 1
end
test 'selected columns can be resized by dragging' do
@@ -73,20 +73,20 @@ class GanttSystemTest < ApplicationSystemTestCase
task_area = find('div.tooltip.hascontextmenu', match: :first, visible: :all)
task_area.hover
assert_selector('div.tooltip span.tip', text: issue_reference, visible: :visible)
assert_selector 'div.tooltip span.tip', text: issue_reference
issue_subject.right_click
assert_selector('#context-menu', visible: :visible)
assert_selector('#context-menu a.icon-edit', visible: :visible)
assert_selector '#context-menu'
assert_selector '#context-menu a.icon-edit'
page.send_keys(:escape)
task_area = find('div.tooltip.hascontextmenu', match: :first, visible: :all)
task_area.right_click
assert_selector('#context-menu', visible: :visible)
assert_selector('#context-menu a.icon-edit', visible: :visible)
assert_selector '#context-menu'
assert_selector '#context-menu a.icon-edit'
page.send_keys(:escape)
end
@@ -107,7 +107,7 @@ class GanttSystemTest < ApplicationSystemTestCase
end
def drag_column_resizer(column_id, distance)
handle = find("td##{column_id} .ui-resizable-e", visible: :visible)
handle = find("td##{column_id} .ui-resizable-e")
page.driver.browser.action.click_and_hold(handle.native).move_by(distance, 0).release.perform
end
end