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..113ee8c46 --- /dev/null +++ b/app/views/gantts/change_duration.js.erb @@ -0,0 +1,81 @@ +<% +@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 %>').each(function(_, task) { + var el_parent = $(task).parent(); + el_parent.html('<%= raw(todo_content) %>'); + var number_of_rows = el_parent.attr('data-number-of-rows'); + if(number_of_rows){ + el_parent.find('div[data-number-of-rows]').attr('data-number-of-rows', number_of_rows); + } + }); + $('<%= 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..d9f25fc05 100644 --- a/lib/redmine/helpers/gantt.rb +++ b/lib/redmine/helpers/gantt.rb @@ -347,6 +347,7 @@ module Redmine if options[:format] == :html data_options = {} data_options[:collapse_expand] = "issue-#{issue.id}" + data_options[:number_of_rows] = number_of_rows style = "position: absolute;top: #{options[:top]}px; font-size: 0.8em;" content = view.content_tag( @@ -771,6 +772,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 @@ -780,6 +782,7 @@ module Redmine :top_increment => params[:top_increment], :obj_id => "#{object.class}-#{object.id}".downcase, }, + :number_of_rows => number_of_rows, } end if has_children @@ -835,7 +838,10 @@ module Redmine def html_task(params, coords, markers, label, object) output = +'' data_options = {} - data_options[:collapse_expand] = "#{object.class}-#{object.id}".downcase if object + if object + data_options[:collapse_expand] = "#{object.class}-#{object.id}".downcase + data_options[:number_of_rows] = number_of_rows + end css = "task " + case object when Project @@ -849,15 +855,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 +877,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 +956,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/images/milestone_done.png b/public/images/milestone_done.png deleted file mode 100644 index 5fdcb415c..000000000 Binary files a/public/images/milestone_done.png and /dev/null differ diff --git a/public/images/milestone_late.png b/public/images/milestone_late.png deleted file mode 100644 index cf922e954..000000000 Binary files a/public/images/milestone_late.png and /dev/null differ diff --git a/public/images/milestone_todo.png b/public/images/milestone_todo.png deleted file mode 100644 index 4c051c857..000000000 Binary files a/public/images/milestone_todo.png and /dev/null differ diff --git a/public/images/project_marker.png b/public/images/project_marker.png deleted file mode 100644 index 4124787d0..000000000 Binary files a/public/images/project_marker.png and /dev/null differ diff --git a/public/images/task_done.png b/public/images/task_done.png deleted file mode 100644 index 5fdcb415c..000000000 Binary files a/public/images/task_done.png and /dev/null differ diff --git a/public/images/task_late.png b/public/images/task_late.png deleted file mode 100644 index 71fd2d70b..000000000 Binary files a/public/images/task_late.png and /dev/null differ diff --git a/public/images/task_parent_end.png b/public/images/task_parent_end.png deleted file mode 100644 index 9442b86a5..000000000 Binary files a/public/images/task_parent_end.png and /dev/null differ diff --git a/public/images/task_todo.png b/public/images/task_todo.png deleted file mode 100644 index 632406ee1..000000000 Binary files a/public/images/task_todo.png and /dev/null differ diff --git a/public/images/version_marker.png b/public/images/version_marker.png deleted file mode 100644 index 0368ca290..000000000 Binary files a/public/images/version_marker.png and /dev/null differ diff --git a/public/javascripts/gantt.js b/public/javascripts/gantt.js index da3b86b7a..d78da372a 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}); @@ -253,13 +255,16 @@ ganttEntryClick = function(e){ 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); - $('#gantt_area form > div[data-collapse-expand="' + json.obj_id + '"], td.gantt_selected_column div[data-collapse-expand="' + json.obj_id + '"]').each(function(_, el){ + $([el_task_bars, el_selected_columns].join()).each(function(_, el){ $(el).css('top', new_top_val); }); return true; @@ -272,15 +277,14 @@ ganttEntryClick = function(e){ total_height = 0; } if(is_shown == target_shown){ - $('#gantt_area form > div[data-collapse-expand="' + json.obj_id + '"]').each(function(_, task) { + $(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); }); - $('td.gantt_selected_column div[data-collapse-expand="' + json.obj_id + '"]' - ).each(function (_, attr) { + $(el_selected_columns).each(function (_, attr) { var el_attr = $(attr); if (!is_shown) el_attr.css('top', target_top + total_height); @@ -301,3 +305,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 = $('