From cadbb8dd55d7f99a483814bb99821950e6c70b4a Mon Sep 17 00:00:00 2001 From: Yuichi HARADA Date: Tue, 9 Feb 2021 09:30:41 +0900 Subject: [PATCH 1/3] Moving gantt bar --- app/controllers/gantts_controller.rb | 23 ++- app/views/gantts/change_duration.js.erb | 74 +++++++++ config/routes.rb | 1 + lib/redmine/helpers/gantt.rb | 134 +++++++++------- public/javascripts/gantt.js | 161 ++++++++++++++++---- public/stylesheets/application.css | 96 +++++++++--- test/integration/routing/gantts_test.rb | 2 + test/unit/lib/redmine/helpers/gantt_test.rb | 90 ++++++----- 8 files changed, 445 insertions(+), 136 deletions(-) create mode 100644 app/views/gantts/change_duration.js.erb diff --git a/app/controllers/gantts_controller.rb b/app/controllers/gantts_controller.rb index 6d2c26128..3e7db9c73 100644 --- a/app/controllers/gantts_controller.rb +++ b/app/controllers/gantts_controller.rb @@ -19,7 +19,7 @@ class GanttsController < ApplicationController menu_item :gantt - before_action :find_optional_project + before_action :find_optional_project, :only => [:show] rescue_from Query::StatementInvalid, :with => :query_statement_invalid @@ -55,4 +55,25 @@ class GanttsController < ApplicationController end end end + + def change_duration + return render_error(:status => :unprocessable_entity) unless request.xhr? + + @obj = Issue.find(params[:id]) + raise Unauthorized unless @obj.visible? + + ActiveRecord::Base.transaction do + @obj.init_journal(User.current) + @obj.safe_attributes = params[:change_duration] + if !@obj.save + render_403(:message => @obj.errors.full_messages.join) + raise ActiveRecord::Rollback + end + end + retrieve_query + rescue ActiveRecord::StaleObjectError + render_403(:message => :notice_issue_update_conflict) + rescue ActiveRecord::RecordNotFound + render_404 + end end diff --git a/app/views/gantts/change_duration.js.erb b/app/views/gantts/change_duration.js.erb new file mode 100644 index 000000000..26d1e8a60 --- /dev/null +++ b/app/views/gantts/change_duration.js.erb @@ -0,0 +1,74 @@ +<% +@draw_objs = [] + +def select_precedes(issue) + issue.relations_from.where(:relation_type => IssueRelation::TYPE_PRECEDES).map(&:issue_to).each do |follows| + next if @draw_objs.include?(follows) + + while follows do + @draw_objs.concat [follows, follows.fixed_version, follows.project] + select_precedes(follows) + follows.children.each do |child| + @draw_objs.concat [child, child.fixed_version, child.project] + select_precedes(child) + end + follows = follows.parent + end + end +end + +issue = @obj +while issue do + @draw_objs.concat [issue, issue.fixed_version, issue.project] + select_precedes(issue) + issue = issue.parent +end +@draw_objs = @draw_objs.compact.uniq +@draw_objs.reject!{|obj| ![Project, Version, Issue].include?(obj.class)} +-%> +var elm; +<% +gantt = Redmine::Helpers::Gantt.new(params) +gantt.view = self +gantt.query = @query + +@draw_objs.each do |obj| + gantt.instance_variable_set(:@number_of_rows, 0) + gantt.instance_variable_set(:@lines, '') + gantt.render_object_row( + obj, + {format: :html, only: :lines, zoom: 2 ** gantt.zoom, top: 0, top_increment: 20} + ) + todo_content = Nokogiri::HTML.parse(gantt.instance_variable_get(:@lines)) + todo_content = todo_content.xpath( + "//div[contains(@class,'task') and contains(@class,'line')]/*" + ).to_s.tr("\n",'').gsub(/'/, "\\\\'") + + klass_name = obj.class.name.underscore + elm_todo = "[id=task-todo-#{klass_name}-#{obj.id}]" + css_subject = 'span:not(.expander)' + elm_subject = raw("[id=#{klass_name}-#{obj.id}] > #{css_subject}") + + subject_content = Nokogiri::HTML.parse(gantt.__send__(:html_subject_content, obj)) + subject_content = subject_content.css(css_subject).to_s.tr("\n",'').gsub(/'/, "\\\\'") +-%> +if($('<%= elm_subject %>').length){ + $('<%= elm_todo %>').parent().html('<%= raw(todo_content) %>'); + $('<%= elm_subject %>').replaceWith('<%= raw(subject_content) %>'); +<% + case obj + when Issue + @query.columns.each do |column| +-%> + elm = $('div.gantt_selected_column_content #<%= column.name %>_issue_<%= obj.id %>'); + if(elm.length){ + elm.html('<%= escape_javascript(column_content(column, obj)) %>'); + } +<% + end + end +-%> +} +<% +end +-%> diff --git a/config/routes.rb b/config/routes.rb index acc3ca465..fb619a817 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -60,6 +60,7 @@ Rails.application.routes.draw do get '/projects/:project_id/issues/gantt', :to => 'gantts#show', :as => 'project_gantt' get '/issues/gantt', :to => 'gantts#show' + put '/gantt/:id/change_duration', :to => 'gantts#change_duration', :as => 'gantt_change_duration' get '/projects/:project_id/issues/calendar', :to => 'calendars#show', :as => 'project_calendar' get '/issues/calendar', :to => 'calendars#show' diff --git a/lib/redmine/helpers/gantt.rb b/lib/redmine/helpers/gantt.rb index 9b68f32c2..ccc249fab 100644 --- a/lib/redmine/helpers/gantt.rb +++ b/lib/redmine/helpers/gantt.rb @@ -771,6 +771,7 @@ module Redmine tag_options[:class] = "version-name" has_children = object.fixed_issues.exists? when Project + tag_options[:id] = "project-#{object.id}" tag_options[:class] = "project-name" has_children = object.issues.exists? || object.versions.exists? end @@ -849,15 +850,21 @@ module Redmine end # Renders the task bar, with progress and late if coords[:bar_start] && coords[:bar_end] - width = coords[:bar_end] - coords[:bar_start] - 2 + width = coords[:bar_end] - coords[:bar_start] style = +"" - style << "top:#{params[:top]}px;" style << "left:#{coords[:bar_start]}px;" style << "width:#{width}px;" - html_id = "task-todo-issue-#{object.id}" if object.is_a?(Issue) - html_id = "task-todo-version-#{object.id}" if object.is_a?(Version) + html_id = + case object + when Project + "task-todo-project-#{object.id}" + when Version + "task-todo-version-#{object.id}" + when Issue + "task-todo-issue-#{object.id}" + end content_opt = {:style => style, - :class => "#{css} task_todo", + :class => "task_todo", :id => html_id, :data => {}} if object.is_a?(Issue) @@ -865,32 +872,78 @@ module Redmine if rels.present? content_opt[:data] = {"rels" => rels.to_json} end + content_opt[:data].merge!({ + :url_change_duration => Rails.application.routes.url_helpers.gantt_change_duration_path( + object + ), + :object => { + :start_date => object.start_date, + :due_date => object.due_date, + :lock_version => object.lock_version, + }.to_json, + }) end content_opt[:data].merge!(data_options) - output << view.content_tag(:div, ' '.html_safe, content_opt) + bar_contents = [] if coords[:bar_late_end] - width = coords[:bar_late_end] - coords[:bar_start] - 2 + width = coords[:bar_late_end] - coords[:bar_start] style = +"" - style << "top:#{params[:top]}px;" - style << "left:#{coords[:bar_start]}px;" style << "width:#{width}px;" - output << view.content_tag(:div, ' '.html_safe, - :style => style, - :class => "#{css} task_late", - :data => data_options) + bar_contents << view.content_tag(:div, ' '.html_safe, + :style => style, + :class => "task_late", + :data => data_options) end if coords[:bar_progress_end] - width = coords[:bar_progress_end] - coords[:bar_start] - 2 + width = coords[:bar_progress_end] - coords[:bar_start] style = +"" - style << "top:#{params[:top]}px;" - style << "left:#{coords[:bar_start]}px;" style << "width:#{width}px;" html_id = "task-done-issue-#{object.id}" if object.is_a?(Issue) html_id = "task-done-version-#{object.id}" if object.is_a?(Version) - output << view.content_tag(:div, ' '.html_safe, + bar_contents << view.content_tag(:div, ' '.html_safe, + :style => style, + :class => "task_done", + :id => html_id, + :data => data_options) + end + + # Renders the tooltip + if object.is_a?(Issue) + s = view.content_tag(:span, + view.render_issue_tooltip(object).html_safe, + :class => "tip") + s += view.content_tag(:input, nil, :type => 'checkbox', :name => 'ids[]', :value => object.id, :style => 'display:none;', :class => 'toggle-selection') + style = +"" + style << "width:#{coords[:bar_end] - coords[:bar_start]}px;" + style << "height:12px;" + bar_contents << view.content_tag(:div, s.html_safe, + :style => style, + :class => "tooltip hascontextmenu", + :data => data_options) + end + + # Renders the label on the right + if label + style = +"" + style << "top:0px;" + style << "left:#{coords[:bar_end] - coords[:bar_start] + 8}px;" + bar_contents << view.content_tag(:div, label, + :style => style, + :class => "label", + :data => data_options) + end + + bar_contents = bar_contents.join.presence + output << view.content_tag(:div, (bar_contents || ' ').html_safe, content_opt) + else + # Renders the label on the right + if label + style = +"" + style << "top:1px;" + style << "left:#{(coords[:bar_end] || 0) + 8}px;" + output << view.content_tag(:div, label, :style => style, - :class => "#{css} task_done", - :id => html_id, + :class => "label", :data => data_options) end end @@ -898,55 +951,26 @@ module Redmine if markers if coords[:start] style = +"" - style << "top:#{params[:top]}px;" style << "left:#{coords[:start]}px;" - style << "width:15px;" output << view.content_tag(:div, ' '.html_safe, :style => style, - :class => "#{css} marker starting", + :class => "marker starting", :data => data_options) end if coords[:end] style = +"" - style << "top:#{params[:top]}px;" style << "left:#{coords[:end]}px;" - style << "width:15px;" output << view.content_tag(:div, ' '.html_safe, :style => style, - :class => "#{css} marker ending", + :class => "marker ending", :data => data_options) end end - # Renders the label on the right - if label - style = +"" - style << "top:#{params[:top]}px;" - style << "left:#{(coords[:bar_end] || 0) + 8}px;" - style << "width:15px;" - output << view.content_tag(:div, label, - :style => style, - :class => "#{css} label", - :data => data_options) - end - # Renders the tooltip - if object.is_a?(Issue) && coords[:bar_start] && coords[:bar_end] - s = view.content_tag(:span, - view.render_issue_tooltip(object).html_safe, - :class => "tip") - s += view.content_tag(:input, nil, :type => 'checkbox', :name => 'ids[]', - :value => object.id, :style => 'display:none;', - :class => 'toggle-selection') - style = +"" - style << "position: absolute;" - style << "top:#{params[:top]}px;" - style << "left:#{coords[:bar_start]}px;" - style << "width:#{coords[:bar_end] - coords[:bar_start]}px;" - style << "height:12px;" - output << view.content_tag(:div, s.html_safe, - :style => style, - :class => "tooltip hascontextmenu", - :data => data_options) - end + output = view.content_tag(:div, output.html_safe, + :class => "#{css} line", + :style => "top:#{params[:top]}px;width:#{params[:g_width] - 1}px;", + :data => data_options + ) @lines << output output end diff --git a/public/javascripts/gantt.js b/public/javascripts/gantt.js index da3b86b7a..2b292af76 100644 --- a/public/javascripts/gantt.js +++ b/public/javascripts/gantt.js @@ -4,14 +4,12 @@ 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_top = $("#gantt_draw_area").offset().top; draw_right = $("#gantt_draw_area").width(); - draw_left = $("#gantt_area").scrollLeft(); } function getRelationsArray() { @@ -42,27 +40,27 @@ function drawRelations() { return; } var issue_height = issue_from.height(); - var issue_from_top = issue_from.position().top + (issue_height / 2) - draw_top; + var issue_from_top = issue_from.offset().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_top = issue_to.offset().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]) + draw_gantt.path(["M", issue_from_right, issue_from_top, + "L", issue_from_right_rel, 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]) + draw_gantt.path(["M", issue_from_right_rel, issue_from_top, + "L", issue_from_right_rel, 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]) + draw_gantt.path(["M", issue_from_right_rel, issue_to_top, + "L", issue_to_left, issue_to_top]) .attr({stroke: color, "stroke-width": rels_stroke_width }); @@ -70,28 +68,28 @@ function drawRelations() { 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]) + draw_gantt.path(["M", issue_from_right_rel, issue_from_top, + "L", issue_from_right_rel, 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]) + draw_gantt.path(["M", issue_from_right_rel, issue_middle_top, + "L", issue_to_left_rel, 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]) + draw_gantt.path(["M", issue_to_left_rel, issue_middle_top, + "L", issue_to_left_rel, 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]) + draw_gantt.path(["M", issue_to_left_rel, issue_to_top, + "L", issue_to_left, issue_to_top]) .attr({stroke: color, "stroke-width": rels_stroke_width }); } - draw_gantt.path(["M", issue_to_left + draw_left, issue_to_top, + draw_gantt.path(["M", issue_to_left, issue_to_top, "l", -4 * rels_stroke_width, -2 * rels_stroke_width, "l", 0, 4 * rels_stroke_width, "z"]) .attr({stroke: "none", @@ -104,11 +102,11 @@ function drawRelations() { function getProgressLinesArray() { var arr = new Array(); - var today_left = $('#today_line').position().left; + var today_left = $('#today_line').position().left + $("#gantt_area").scrollLeft(); 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 t = $(element).offset().top - draw_top ; var h = ($(element).height() / 9); var element_top_upper = t - h; var element_top_center = t + (h * 3); @@ -125,8 +123,11 @@ function getProgressLinesArray() { 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(); + var done_left = today_left; + var issue_todo = $("#task-todo-" + $(element).attr("id")); + if (issue_todo.length > 0){ + done_left = issue_todo.first().position().left; + } 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}); @@ -145,6 +146,7 @@ function getProgressLinesArray() { } function drawGanttProgressLines() { + if(!$("#today_line").length) return; var arr = getProgressLinesArray(); var color = $("#today_line") .css("border-left-color"); @@ -154,8 +156,8 @@ function drawGanttProgressLines() { (!("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; + var x1 = (arr[i - 1].left == 0) ? 0 : arr[i - 1].left; + var x2 = (arr[i].left == 0) ? 0 : arr[i].left; draw_gantt.path(["M", x1, arr[i - 1].top, "L", x2, arr[i].top]) .attr({stroke: color, "stroke-width": 2}); @@ -301,3 +303,110 @@ function disable_unavailable_columns(unavailable_columns) { $('#available_c, #selected_c').children("[value='" + value + "']").prop('disabled', true); }); } + +initGanttDnD = function(){ + var grid_x = 0; + if($('#zoom').length){ + switch(parseInt($('#zoom').val())){ + case 4: + grid_x = 16; + break; + case 3: + grid_x = 8; + break; + } + } + if(grid_x > 0){ + $('.leaf .task_todo').draggable({ + containment: 'parent', + axis: 'x', + grid: [grid_x, 0], + opacity: 0.5, + cursor: 'move', + revertDuration: 100, + start: function (event, ui) { + var helper = ui.helper[0]; + helper.startLeft = ui.position.left; + }, + }); + + $('.task.line').droppable({ + accept: '.leaf .task_todo', + drop: function (event, ui) { + var target = $(ui.draggable); + var url = target.attr('data-url-change-duration'); + var object = JSON.parse(target.attr('data-object')); + var startLeft = target[0].startLeft; + var relative_days = Math.floor((ui.position.left - startLeft) / grid_x); + if(relative_days == 0){ + return; + } + var start_date = new Date(object.start_date); + start_date.setDate(start_date.getDate() + relative_days); + start_date = + [ + start_date.getFullYear(), + ('0' + (start_date.getMonth() + 1)).slice(-2), + ('0' + start_date.getDate()).slice(-2), + ].join('-'); + var due_date = null; + if(object.due_date != null){ + due_date = new Date(object.due_date); + due_date.setDate(due_date.getDate() + relative_days); + due_date = + [ + due_date.getFullYear(), + ('0' + (due_date.getMonth() + 1)).slice(-2), + ('0' + due_date.getDate()).slice(-2), + ].join('-'); + } + + $('#selected_c option:not(:disabled)').prop('selected', true); + var form = $('#query_form').serializeArray(); + var json_param = {}; + form.forEach(function(data){ + var key = data.name; + var value = data.value; + if(/\[\]$/.test(key)){ + if(!json_param.hasOwnProperty(key)){ + json_param[key] = []; + } + json_param[key].push(value); + } + else{ + json_param[key] = value; + } + }); + $('#selected_c option:not(:disabled)').prop('selected', false); + Object.assign(json_param, { + change_duration: { + start_date: start_date, + due_date: due_date, + lock_version: object.lock_version, + }, + }); + + $.ajax({ + type: 'PUT', + url: url, + data: json_param, + }).done(function(data){ + drawGanttHandler(); + initGanttDnD(); + }).fail(function(jqXHR){ + var contents = $('
' + jqXHR.responseText + '
'); + var error_message = contents.find('p#errorExplanation'); + if(error_message.length){ + $('div#content h2:first-of-type').after(error_message); + $('p#errorExplanation').hide('fade', {}, 3000, function(){ + $(this).remove(); + }); + } + ui.draggable.animate({'left': ui.draggable[0].startLeft}, 'fast'); + }); + } + }); + } +}; + +$(document).ready(initGanttDnD); diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 1c2f76c15..95b0b02bd 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -1410,7 +1410,7 @@ td.gantt_selected_column .gantt_hdr,.gantt_selected_column_container { .task { position: absolute; - height:8px; + height:10px; font-size:0.8em; color:#888; padding:0; @@ -1419,27 +1419,87 @@ td.gantt_selected_column .gantt_hdr,.gantt_selected_column_container { white-space:nowrap; } -.task.label {width:100%;} -.task.label.project, .task.label.version { font-weight: bold; } +.task.line { left: 0; } +.task div.tooltip:hover span.tip { font-size: inherit; } +.task .task_todo .label { font-size: inherit; } +.task.project .task_todo .label { margin-top: -4px; } +.task.version .task_todo .label { margin-top: -3px; } -.task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; } -.task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; } -.task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; } +.task .label { position: absolute; width: auto; } +.task.project .label, .task.version .label { font-weight: bold; } -.task_todo.parent { background: #888; border: 1px solid #888; height: 3px;} -.task_late.parent, .task_done.parent { height: 3px;} -.task.parent.marker.starting { position: absolute; background: url(../images/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(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; right: 0px; top: -1px;} +.task_late { position: absolute; height: inherit; background:#f66; } +.task_done { position: absolute; height: inherit; background:#00c600; } +.task_todo { position: absolute; height: inherit; background:#aaa; } -.version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;} -.version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;} -.version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;} -.version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; } +.parent .task_todo { background: #888; height: 5px; } +.parent .task_late, .parent .task_done { height: 5px; } +.parent .marker { + background: #888; + display: inline-block; + position: absolute; + width: 8px; + height: 6px; + margin-left: -5px; + margin-bottom: -4px; +} +.parent .marker:after { + border-top: 3px solid #888; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + content: ''; + height: 0; + left: 0; + position: absolute; + bottom: -3px; + width: 0; +} + +.version .task_late { background:#f66; height: 4px; } +.version .task_done { background:#00c600; height: 4px; } +.version .task_todo { background:#aaa; height: 4px; margin-top: 3px; } +.version .marker { + width: 0; + height: 0; + border: 5px solid transparent; + border-bottom-color: black; + position: absolute; + margin-top: -5px; + margin-left: -6px; +} +.version .marker:after { + content: ''; + position: absolute; + left: -5px; + top: 5px; + width: 0; + height: 0; + border: 5px solid transparent; + border-top-color: black; +} -.project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;} -.project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;} -.project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;} -.project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; } +.project .task_late { background:#f66; height: 2px; } +.project .task_done { background:#00c600; height: 2px; } +.project .task_todo { background:#aaa; height: 2px; margin-top: 4px; } +.project .marker { + width: 0; + height: 0; + border: 5px solid transparent; + border-bottom-color: blue; + position: absolute; + margin-top: -5px; + margin-left: -6px; +} +.project .marker:after { + content: ''; + position: absolute; + left: -5px; + top: 5px; + width: 0; + height: 0; + border: 5px solid transparent; + border-top-color: blue; +} .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;} .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;} diff --git a/test/integration/routing/gantts_test.rb b/test/integration/routing/gantts_test.rb index 4e859c77d..29384c59a 100644 --- a/test/integration/routing/gantts_test.rb +++ b/test/integration/routing/gantts_test.rb @@ -26,5 +26,7 @@ class RoutingGanttsTest < Redmine::RoutingTest should_route 'GET /projects/foo/issues/gantt' => 'gantts#show', :project_id => 'foo' should_route 'GET /projects/foo/issues/gantt.pdf' => 'gantts#show', :project_id => 'foo', :format => 'pdf' + + should_route 'PUT /gantt/123/change_duration' => 'gantts#change_duration', :id => '123' end end diff --git a/test/unit/lib/redmine/helpers/gantt_test.rb b/test/unit/lib/redmine/helpers/gantt_test.rb index 22d02f164..7e3b31d76 100644 --- a/test/unit/lib/redmine/helpers/gantt_test.rb +++ b/test/unit/lib/redmine/helpers/gantt_test.rb @@ -235,19 +235,28 @@ class Redmine::Helpers::GanttHelperTest < Redmine::HelperTest @project.issues << @issue @output_buffer = @gantt.lines - assert_select "div.project.task_todo" - assert_select "div.project.starting" - assert_select "div.project.ending" - assert_select "div.label.project", /#{@project.name}/ + assert_select "div.task.project" do + assert_select "> div.task_todo" do + assert_select "> div.label", /#{@project.name}/ + end + assert_select "> div.starting" + assert_select "> div.ending" + end - assert_select "div.version.task_todo" - assert_select "div.version.starting" - assert_select "div.version.ending" - assert_select "div.label.version", /#{@version.name}/ + assert_select "div.task.version" do + assert_select "> div.task_todo" do + assert_select "div.label", /#{@version.name}/ + end + assert_select "> div.starting" + assert_select "> div.ending" + end - assert_select "div.task_todo" - assert_select "div.task.label", /#{@issue.done_ratio}/ - assert_select "div.tooltip", /#{@issue.subject}/ + assert_select "div.task" do + assert_select "> div.task_todo" do + assert_select "> div.label", /#{@issue.done_ratio}/ + assert_select "> div.tooltip", /#{@issue.subject}/ + end + end end test "#selected_column_content" do @@ -331,7 +340,9 @@ class Redmine::Helpers::GanttHelperTest < Redmine::HelperTest @project.stubs(:start_date).returns(today - 7) @project.stubs(:due_date).returns(today + 7) @output_buffer = @gantt.line_for_project(@project, :format => :html) - assert_select "div.project.label", :text => @project.name + assert_select "div.task.project > div.task_todo" do + assert_select "> div.label", :text => @project.name + end end test "#line_for_version" do @@ -341,17 +352,19 @@ class Redmine::Helpers::GanttHelperTest < Redmine::HelperTest version.stubs(:due_date).returns(today + 7) version.stubs(:visible_fixed_issues => stub(:completed_percent => 30)) @output_buffer = @gantt.line_for_version(version, :format => :html) - assert_select "div.version.label", :text => /Foo/ - assert_select "div.version.label", :text => /30%/ + assert_select "div.task.version > div.task_todo" do + assert_select "> div.label", :text => 'Foo 30%' + end end test "#line_for_issue" do create_gantt issue = Issue.generate!(:project => @project, :start_date => today - 7, :due_date => today + 7, :done_ratio => 30) @output_buffer = @gantt.line_for_issue(issue, :format => :html) - assert_select "div.task.label", :text => /#{issue.status.name}/ - assert_select "div.task.label", :text => /30%/ - assert_select "div.tooltip", /#{issue.subject}/ + assert_select "div.task_todo" do + assert_select "> div.label", :text => "#{issue.status.name} 30%" + assert_select "> div.tooltip", /#{issue.subject}/ + end end test "#line todo line should start from the starting point on the left" do @@ -365,8 +378,7 @@ class Redmine::Helpers::GanttHelperTest < Redmine::HelperTest [gantt_start - 1, gantt_start].each do |start_date| @output_buffer = @gantt.line(start_date, gantt_start, 30, false, 'line', :format => :html, :zoom => 4) # the leftmost date (Date.today - 14 days) - assert_select 'div.task_todo[style*="left:0px"]', 1, @output_buffer - assert_select 'div.task_todo[style*="width:2px"]', 1, @output_buffer + assert_select 'div.task_todo[style*="left:0px"][style*="width:4px"]', 1, @output_buffer end end @@ -375,74 +387,80 @@ class Redmine::Helpers::GanttHelperTest < Redmine::HelperTest [gantt_end, gantt_end + 1].each do |end_date| @output_buffer = @gantt.line(gantt_end, end_date, 30, false, 'line', :format => :html, :zoom => 4) # the rightmost date (Date.today + 14 days) - assert_select 'div.task_todo[style*="left:112px"]', 1, @output_buffer - assert_select 'div.task_todo[style*="width:2px"]', 1, @output_buffer + assert_select 'div.task_todo[style*="left:112px"][style*="width:4px"]', 1, @output_buffer end end test "#line todo line should be the total width" do create_gantt @output_buffer = @gantt.line(today - 7, today + 7, 30, false, 'line', :format => :html, :zoom => 4) - assert_select 'div.task_todo[style*="width:58px"]', 1 + assert_select 'div.task_todo[style*="width:60px"]', 1 end test "#line late line should start from the starting point on the left" do create_gantt @output_buffer = @gantt.line(today - 7, today + 7, 30, false, 'line', :format => :html, :zoom => 4) - assert_select 'div.task_late[style*="left:28px"]', 1 + assert_select 'div.task_todo[style*="left:28px"]' do + assert_select '> div.task_late', 1 + end end test "#line late line should be the total delayed width" do create_gantt @output_buffer = @gantt.line(today - 7, today + 7, 30, false, 'line', :format => :html, :zoom => 4) - assert_select 'div.task_late[style*="width:30px"]', 1 + assert_select 'div.task_late[style*="width:32px"]', 1 end test "#line late line should be the same width as task_todo if start date and end date are the same day" do create_gantt @output_buffer = @gantt.line(today - 7, today - 7, 0, false, 'line', :format => :html, :zoom => 4) - assert_select 'div.task_late[style*="width:2px"]', 1 - assert_select 'div.task_todo[style*="width:2px"]', 1 + assert_select 'div.task_todo[style*="width:4px"]' do + assert_select '> div.task_late[style*="width:4px"]', 1 + end end test "#line late line should be the same width as task_todo if start date and today are the same day" do create_gantt @output_buffer = @gantt.line(today, today, 0, false, 'line', :format => :html, :zoom => 4) - assert_select 'div.task_late[style*="width:2px"]', 1 - assert_select 'div.task_todo[style*="width:2px"]', 1 + assert_select 'div.task_todo[style*="width:4px"]' do + assert_select '> div.task_late[style*="width:4px"]', 1 + end end test "#line done line should start from the starting point on the left" do create_gantt @output_buffer = @gantt.line(today - 7, today + 7, 30, false, 'line', :format => :html, :zoom => 4) - assert_select 'div.task_done[style*="left:28px"]', 1 + assert_select 'div.task_todo[style*="left:28px"]' do + assert_select '> div.task_done', 1 + end end test "#line done line should be the width for the done ratio" do create_gantt @output_buffer = @gantt.line(today - 7, today + 7, 30, false, 'line', :format => :html, :zoom => 4) - # 15 days * 4 px * 30% - 2 px for borders = 16 px - assert_select 'div.task_done[style*="width:16px"]', 1 + # 15 days * 4 px * 30% = 18 px + assert_select 'div.task_done[style*="width:18px"]', 1 end test "#line done line should be the total width for 100% done ratio" do create_gantt @output_buffer = @gantt.line(today - 7, today + 7, 100, false, 'line', :format => :html, :zoom => 4) - # 15 days * 4 px - 2 px for borders = 58 px - assert_select 'div.task_done[style*="width:58px"]', 1 + # 15 days * 4 px = 60 px + assert_select 'div.task_done[style*="width:60px"]', 1 end test "#line done line should be the total width for 100% done ratio with same start and end dates" do create_gantt @output_buffer = @gantt.line(today + 7, today + 7, 100, false, 'line', :format => :html, :zoom => 4) - assert_select 'div.task_done[style*="width:2px"]', 1 + assert_select 'div.task_done[style*="width:4px"]', 1 end test "#line done line should not be the total done width if the gantt starts after start date" do create_gantt @output_buffer = @gantt.line(today - 16, today - 2, 30, false, 'line', :format => :html, :zoom => 4) - assert_select 'div.task_done[style*="left:0px"]', 1 - assert_select 'div.task_done[style*="width:8px"]', 1 + assert_select 'div.task_todo[style*="left:0px"]' do + assert_select '> div.task_done[style*="width:10px"]', 1 + end end test "#line starting marker should appear at the start date" do -- 2.24.3 (Apple Git-128)