Commit 4155c972 authored by Jean-Philippe Lang's avatar Jean-Philippe Lang

Issue list now supports bulk edit/move/delete (#563, #607). For now, issues from…

Issue list now supports bulk edit/move/delete (#563, #607). For now, issues from different projects can not be bulk edited/moved/deleted at once.

There are 2 ways to select a set of issues on the issue list:
* by using checkbox and/or the little pencil that will select/unselect all issues (#567)
* by clicking on the rows (but not on the links), Ctrl and Shift keys can be used to select multiple issues

Context menu was disabled on links so that the default context menu of the browser is displayed when right-clicking on a link (#545).
All this was tested with Firefox 2, IE 6/7, Opera 8 (use Alt+Click instead of Right-click) and Safari 2/3.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1130 e93f8b46-1217-0410-a6f0-8f06a7374b81
parent 43a6f312
...@@ -19,13 +19,14 @@ class IssuesController < ApplicationController ...@@ -19,13 +19,14 @@ class IssuesController < ApplicationController
layout 'base' layout 'base'
menu_item :new_issue, :only => :new menu_item :new_issue, :only => :new
before_filter :find_issue, :except => [:index, :changes, :preview, :new, :update_form] before_filter :find_issue, :only => [:show, :edit, :destroy_attachment]
before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
before_filter :find_project, :only => [:new, :update_form] before_filter :find_project, :only => [:new, :update_form]
before_filter :authorize, :except => [:index, :changes, :preview, :update_form] before_filter :authorize, :except => [:index, :changes, :preview, :update_form, :context_menu]
before_filter :find_optional_project, :only => [:index, :changes] before_filter :find_optional_project, :only => [:index, :changes]
accept_key_auth :index, :changes accept_key_auth :index, :changes
cache_sweeper :issue_sweeper, :only => [ :new, :edit, :destroy ] cache_sweeper :issue_sweeper, :only => [ :new, :edit, :bulk_edit, :destroy ]
helper :journals helper :journals
helper :projects helper :projects
...@@ -152,18 +153,20 @@ class IssuesController < ApplicationController ...@@ -152,18 +153,20 @@ class IssuesController < ApplicationController
@priorities = Enumeration::get_values('IPRI') @priorities = Enumeration::get_values('IPRI')
@custom_values = [] @custom_values = []
@edit_allowed = User.current.allowed_to?(:edit_issues, @project) @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
@notes = params[:notes]
journal = @issue.init_journal(User.current, @notes)
# User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
attrs = params[:issue].dup
attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
@issue.attributes = attrs
end
if request.get? if request.get?
@custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| @issue.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x, :customized => @issue) } @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| @issue.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x, :customized => @issue) }
else else
@notes = params[:notes]
journal = @issue.init_journal(User.current, @notes)
# User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
attrs = params[:issue].dup
attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
@issue.attributes = attrs
end
# Update custom fields if user has :edit permission # Update custom fields if user has :edit permission
if @edit_allowed && params[:custom_fields] if @edit_allowed && params[:custom_fields]
@custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) } @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) }
...@@ -191,8 +194,78 @@ class IssuesController < ApplicationController ...@@ -191,8 +194,78 @@ class IssuesController < ApplicationController
flash.now[:error] = l(:notice_locking_conflict) flash.now[:error] = l(:notice_locking_conflict)
end end
# Bulk edit a set of issues
def bulk_edit
if request.post?
status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
assigned_to = params[:assigned_to_id].blank? ? nil : User.find_by_id(params[:assigned_to_id])
category = params[:category_id].blank? ? nil : @project.issue_categories.find_by_id(params[:category_id])
fixed_version = params[:fixed_version_id].blank? ? nil : @project.versions.find_by_id(params[:fixed_version_id])
unsaved_issue_ids = []
@issues.each do |issue|
journal = issue.init_journal(User.current, params[:notes])
issue.priority = priority if priority
issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
issue.category = category if category
issue.fixed_version = fixed_version if fixed_version
issue.start_date = params[:start_date] unless params[:start_date].blank?
issue.due_date = params[:due_date] unless params[:due_date].blank?
issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
# Don't save any change to the issue if the user is not authorized to apply the requested status
if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
# Send notification for each issue (if changed)
Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
else
# Keep unsaved issue ids to display them in flash error
unsaved_issue_ids << issue.id
end
end
if unsaved_issue_ids.empty?
flash[:notice] = l(:notice_successful_update) unless @issues.empty?
else
flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
end
redirect_to :controller => 'issues', :action => 'index', :project_id => @project
return
end
# Find potential statuses the user could be allowed to switch issues to
@available_statuses = Workflow.find(:all, :include => :new_status,
:conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq
end
def move
@allowed_projects = []
# find projects to which the user is allowed to move the issue
if User.current.admin?
# admin is allowed to move issues to any active (visible) project
@allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name')
else
User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)}
end
@target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
@target_project ||= @project
@trackers = @target_project.trackers
if request.post?
new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
unsaved_issue_ids = []
@issues.each do |issue|
unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker)
end
if unsaved_issue_ids.empty?
flash[:notice] = l(:notice_successful_update) unless @issues.empty?
else
flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
end
redirect_to :controller => 'issues', :action => 'index', :project_id => @project
return
end
render :layout => false if request.xhr?
end
def destroy def destroy
@issue.destroy @issues.each(&:destroy)
redirect_to :action => 'index', :project_id => @project redirect_to :action => 'index', :project_id => @project
end end
...@@ -208,17 +281,27 @@ class IssuesController < ApplicationController ...@@ -208,17 +281,27 @@ class IssuesController < ApplicationController
end end
def context_menu def context_menu
@issues = Issue.find_all_by_id(params[:ids], :include => :project)
if (@issues.size == 1)
@issue = @issues.first
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
@assignables = @issue.assignable_users
@assignables << @issue.assigned_to if @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
end
projects = @issues.collect(&:project).compact.uniq
@project = projects.first if projects.size == 1
@can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
:update => (@issue && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && !@allowed_statuses.empty?))),
:move => (@project && User.current.allowed_to?(:move_issues, @project)),
:copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
:delete => (@project && User.current.allowed_to?(:delete_issues, @project))
}
@priorities = Enumeration.get_values('IPRI').reverse @priorities = Enumeration.get_values('IPRI').reverse
@statuses = IssueStatus.find(:all, :order => 'position') @statuses = IssueStatus.find(:all, :order => 'position')
@allowed_statuses = @issue.new_statuses_allowed_to(User.current) @back = request.env['HTTP_REFERER']
@assignables = @issue.assignable_users
@assignables << @issue.assigned_to if @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
@can = {:edit => User.current.allowed_to?(:edit_issues, @project),
:assign => (@allowed_statuses.any? || User.current.allowed_to?(:edit_issues, @project)),
:add => User.current.allowed_to?(:add_issues, @project),
:move => User.current.allowed_to?(:move_issues, @project),
:copy => (@project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
:delete => User.current.allowed_to?(:delete_issues, @project)}
render :layout => false render :layout => false
end end
...@@ -242,6 +325,21 @@ private ...@@ -242,6 +325,21 @@ private
render_404 render_404
end end
# Filter for bulk operations
def find_issues
@issues = Issue.find_all_by_id(params[:id] || params[:ids])
raise ActiveRecord::RecordNotFound if @issues.empty?
projects = @issues.collect(&:project).compact.uniq
if projects.size == 1
@project = projects.first
else
# TODO: let users bulk edit/move/destroy issues from different projects
render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
end
rescue ActiveRecord::RecordNotFound
render_404
end
def find_project def find_project
@project = Project.find(params[:project_id]) @project = Project.find(params[:project_id])
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
......
...@@ -22,7 +22,7 @@ class ProjectsController < ApplicationController ...@@ -22,7 +22,7 @@ class ProjectsController < ApplicationController
menu_item :roadmap, :only => :roadmap menu_item :roadmap, :only => :roadmap
menu_item :files, :only => [:list_files, :add_file] menu_item :files, :only => [:list_files, :add_file]
menu_item :settings, :only => :settings menu_item :settings, :only => :settings
menu_item :issues, :only => [:bulk_edit_issues, :changelog, :move_issues] menu_item :issues, :only => [:changelog]
before_filter :find_project, :except => [ :index, :list, :add ] before_filter :find_project, :except => [ :index, :list, :add ]
before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy ] before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy ]
...@@ -182,83 +182,6 @@ class ProjectsController < ApplicationController ...@@ -182,83 +182,6 @@ class ProjectsController < ApplicationController
end end
end end
# Bulk edit issues
def bulk_edit_issues
if request.post?
status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
assigned_to = params[:assigned_to_id].blank? ? nil : User.find_by_id(params[:assigned_to_id])
category = params[:category_id].blank? ? nil : @project.issue_categories.find_by_id(params[:category_id])
fixed_version = params[:fixed_version_id].blank? ? nil : @project.versions.find_by_id(params[:fixed_version_id])
issues = @project.issues.find_all_by_id(params[:issue_ids])
unsaved_issue_ids = []
issues.each do |issue|
journal = issue.init_journal(User.current, params[:notes])
issue.priority = priority if priority
issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
issue.category = category if category
issue.fixed_version = fixed_version if fixed_version
issue.start_date = params[:start_date] unless params[:start_date].blank?
issue.due_date = params[:due_date] unless params[:due_date].blank?
issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
# Don't save any change to the issue if the user is not authorized to apply the requested status
if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
# Send notification for each issue (if changed)
Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
else
# Keep unsaved issue ids to display them in flash error
unsaved_issue_ids << issue.id
end
end
if unsaved_issue_ids.empty?
flash[:notice] = l(:notice_successful_update) unless issues.empty?
else
flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, issues.size, '#' + unsaved_issue_ids.join(', #'))
end
redirect_to :controller => 'issues', :action => 'index', :project_id => @project
return
end
# Find potential statuses the user could be allowed to switch issues to
@available_statuses = Workflow.find(:all, :include => :new_status,
:conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq
render :update do |page|
page.hide 'query_form'
page.replace_html 'bulk-edit', :partial => 'issues/bulk_edit_form'
end
end
def move_issues
@issues = @project.issues.find(params[:issue_ids]) if params[:issue_ids]
redirect_to :controller => 'issues', :action => 'index', :project_id => @project and return unless @issues
@projects = []
# find projects to which the user is allowed to move the issue
if User.current.admin?
# admin is allowed to move issues to any active (visible) project
@projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name')
else
User.current.memberships.each {|m| @projects << m.project if m.role.allowed_to?(:move_issues)}
end
@target_project = @projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
@target_project ||= @project
@trackers = @target_project.trackers
if request.post?
new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
unsaved_issue_ids = []
@issues.each do |issue|
unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker)
end
if unsaved_issue_ids.empty?
flash[:notice] = l(:notice_successful_update) unless @issues.empty?
else
flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
end
redirect_to :controller => 'issues', :action => 'index', :project_id => @project
return
end
render :layout => false if request.xhr?
end
def add_file def add_file
if request.post? if request.post?
@version = @project.versions.find_by_id(params[:version_id]) @version = @project.versions.find_by_id(params[:version_id])
......
<div id="bulk-edit"></div> <% form_tag({}) do -%>
<table class="list"> <table class="list issues">
<thead><tr> <thead><tr>
<th><%= link_to_remote(image_tag('edit.png'), <th><%= link_to image_tag('edit.png'), {}, :onclick => 'toggleIssuesSelection(this.up("form")); return false;' %>
{:url => { :controller => 'projects', :action => 'bulk_edit_issues', :id => @project },
:method => :get},
{:title => l(:label_bulk_edit_selected_issues)}) if @project && User.current.allowed_to?(:edit_issues, @project) %>
</th> </th>
<%= sort_header_tag("#{Issue.table_name}.id", :caption => '#', :default_order => 'desc') %> <%= sort_header_tag("#{Issue.table_name}.id", :caption => '#', :default_order => 'desc') %>
<% query.columns.each do |column| %> <% query.columns.each do |column| %>
...@@ -12,14 +9,21 @@ ...@@ -12,14 +9,21 @@
<% end %> <% end %>
</tr></thead> </tr></thead>
<tbody> <tbody>
<% issues.each do |issue| %> <% issues.each do |issue| -%>
<tr id="issue-<%= issue.id %>" class="issue hascontextmenu <%= cycle('odd', 'even') %> <%= "status-#{issue.status.position} priority-#{issue.priority.position}" %>"> <tr id="issue-<%= issue.id %>" class="issue hascontextmenu <%= cycle('odd', 'even') %> <%= "status-#{issue.status.position} priority-#{issue.priority.position}" %>">
<td class="checkbox"><%= check_box_tag("issue_ids[]", issue.id, false, :id => "issue_#{issue.id}", :disabled => (!@project || @project != issue.project)) %></td> <td class="checkbox"><%= check_box_tag("ids[]", issue.id, false) %></td>
<td><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td> <td><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td>
<% query.columns.each do |column| %> <% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.name %><% end %>
<%= content_tag 'td', column_content(column, issue), :class => column.name %>
<% end %>
</tr> </tr>
<% end %> <% end -%>
</tbody> </tbody>
</table> </table>
<% end -%>
<% content_for :header_tags do -%>
<%= javascript_include_tag 'context_menu' %>
<%= stylesheet_link_tag 'context_menu' %>
<% end -%>
<div id="context-menu" style="display: none;"></div>
<%= javascript_tag "new ContextMenu('#{url_for(:controller => 'issues', :action => 'context_menu')}')" %>
<% if issues.length > 0 %> <% if issues && issues.any? %>
<table class="list"> <% form_tag({}) do %>
<table class="list issues">
<thead><tr> <thead><tr>
<th>#</th> <th>#</th>
<th><%=l(:field_tracker)%></th> <th><%=l(:field_tracker)%></th>
...@@ -9,6 +10,7 @@ ...@@ -9,6 +10,7 @@
<% for issue in issues %> <% for issue in issues %>
<tr id="issue-<%= issue.id %>" class="issue hascontextmenu <%= cycle('odd', 'even') %> <%= "status-#{issue.status.position} priority-#{issue.priority.position}" %>"> <tr id="issue-<%= issue.id %>" class="issue hascontextmenu <%= cycle('odd', 'even') %> <%= "status-#{issue.status.position} priority-#{issue.priority.position}" %>">
<td class="id"> <td class="id">
<%= check_box_tag("ids[]", issue.id, false, :style => 'display:none;') %>
<%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %> <%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %>
</td> </td>
<td><%=h issue.project.name %> - <%= issue.tracker.name %><br /> <td><%=h issue.project.name %> - <%= issue.tracker.name %><br />
...@@ -20,6 +22,7 @@ ...@@ -20,6 +22,7 @@
<% end %> <% end %>
</tbody> </tbody>
</table> </table>
<% end %>
<% else %> <% else %>
<i><%=l(:label_no_data)%></i> <p class="nodata"><%= l(:label_no_data) %></p>
<% end %> <% end %>
\ No newline at end of file
<div id="bulk-edit-fields"> <h2><%= l(:label_bulk_edit_selected_issues) %></h2>
<fieldset class="box"><legend><%= l(:label_bulk_edit_selected_issues) %></legend>
<ul><%= @issues.collect {|i| content_tag('li', link_to(h("#{i.tracker} ##{i.id}"), { :action => 'show', :id => i }) + h(": #{i.subject}")) }.join("\n") %></ul>
<% form_tag() do %>
<%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join %>
<div class="box">
<fieldset>
<legend><%= l(:label_change_properties) %></legend>
<p> <p>
<% if @available_statuses.any? %> <% if @available_statuses.any? %>
<label><%= l(:field_status) %>: <label><%= l(:field_status) %>:
...@@ -28,11 +34,12 @@ ...@@ -28,11 +34,12 @@
<label><%= l(:field_done_ratio) %>: <label><%= l(:field_done_ratio) %>:
<%= select_tag 'done_ratio', options_for_select([[l(:label_no_change_option), '']] + (0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></label> <%= select_tag 'done_ratio', options_for_select([[l(:label_no_change_option), '']] + (0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></label>
</p> </p>
<label for="notes"><%= l(:field_notes) %></label><br />
<%= text_area_tag 'notes', '', :cols => 80, :rows => 5 %>
</fieldset> </fieldset>
<p><%= submit_tag l(:button_apply) %>
<%= link_to l(:button_cancel), {}, :onclick => 'Element.hide("bulk-edit-fields"); if ($("query_form")) {Element.show("query_form")}; return false;' %></p> <fieldset><legend><%= l(:field_notes) %></legend>
<%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
<%= wikitoolbar_for 'notes' %>
</div> </div>
<p><%= submit_tag l(:button_submit) %>
<% end %>
<% back_to = url_for(:controller => 'issues', :action => 'index', :project_id => @project) %>
<ul> <ul>
<% if !@issue.nil? -%>
<li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'edit', :id => @issue}, <li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'edit', :id => @issue},
:class => 'icon-edit', :disabled => !@can[:edit] %></li> :class => 'icon-edit', :disabled => !@can[:edit] %></li>
<li class="folder"> <li class="folder">
<a href="#" class="submenu" onclick="return false;"><%= l(:field_status) %></a> <a href="#" class="submenu" onclick="return false;"><%= l(:field_status) %></a>
<ul> <ul>
<% @statuses.each do |s| %> <% @statuses.each do |s| -%>
<li><%= context_menu_link s.name, {:controller => 'issues', :action => 'edit', :id => @issue, :issue => {:status_id => s}}, <li><%= context_menu_link s.name, {:controller => 'issues', :action => 'edit', :id => @issue, :issue => {:status_id => s}},
:selected => (s == @issue.status), :disabled => !(@allowed_statuses.include?(s)) %></li> :selected => (s == @issue.status), :disabled => !(@can[:update] && @allowed_statuses.include?(s)) %></li>
<% end %> <% end -%>
</ul> </ul>
</li> </li>
<li class="folder"> <li class="folder">
<a href="#" class="submenu"><%= l(:field_priority) %></a> <a href="#" class="submenu"><%= l(:field_priority) %></a>
<ul> <ul>
<% @priorities.each do |p| %> <% @priorities.each do |p| -%>
<li><%= context_menu_link p.name, {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[priority_id]' => p, :back_to => back_to}, :method => :post, <li><%= context_menu_link p.name, {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[priority_id]' => p, :back_to => @back}, :method => :post,
:selected => (p == @issue.priority), :disabled => !@can[:edit] %></li> :selected => (p == @issue.priority), :disabled => !@can[:edit] %></li>
<% end %> <% end -%>
</ul> </ul>
</li> </li>
<li class="folder"> <li class="folder">
<a href="#" class="submenu"><%= l(:field_assigned_to) %></a> <a href="#" class="submenu"><%= l(:field_assigned_to) %></a>
<ul> <ul>
<% @assignables.each do |u| %> <% @assignables.each do |u| -%>
<li><%= context_menu_link u.name, {:controller => 'issues', :action => 'edit', :id => @issue, :issue => {:assigned_to_id => u}, :back_to => back_to}, :method => :post, <li><%= context_menu_link u.name, {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[assigned_to_id]' => u, :back_to => @back}, :method => :post,
:selected => (u == @issue.assigned_to), :disabled => !@can[:assign] %></li> :selected => (u == @issue.assigned_to), :disabled => !@can[:update] %></li>
<% end %> <% end -%>
<li><%= context_menu_link l(:label_nobody), {:controller => 'issues', :action => 'edit', :id => @issue, :issue => {:assigned_to_id => nil}, :back_to => back_to}, :method => :post, <li><%= context_menu_link l(:label_nobody), {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[assigned_to_id]' => '', :back_to => @back}, :method => :post,
:selected => @issue.assigned_to.nil?, :disabled => !@can[:assign] %></li> :selected => @issue.assigned_to.nil?, :disabled => !@can[:update] %></li>
</ul> </ul>
</li> </li>
<li><%= context_menu_link l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue}, <li><%= context_menu_link l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue},
:class => 'icon-copy', :disabled => !@can[:copy] %></li> :class => 'icon-copy', :disabled => !@can[:copy] %></li>
<li><%= context_menu_link l(:button_move), {:controller => 'projects', :action => 'move_issues', :id => @project, "issue_ids[]" => @issue.id }, <% else -%>
:class => 'icon-move', :disabled => !@can[:move] %> <li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id)},
<li><%= context_menu_link l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue}, :class => 'icon-edit', :disabled => !@can[:edit] %></li>
:method => :post, :confirm => l(:text_are_you_sure), :class => 'icon-del', :disabled => !@can[:delete] %></li> <% end -%>
<li><%= context_menu_link l(:button_move), {:controller => 'issues', :action => 'move', :ids => @issues.collect(&:id)},
:class => 'icon-move', :disabled => !@can[:move] %></li>
<li><%= context_menu_link l(:button_delete), {:controller => 'issues', :action => 'destroy', :ids => @issues.collect(&:id)},
:method => :post, :confirm => l(:text_issues_destroy_confirmation), :class => 'icon-del', :disabled => !@can[:delete] %></li>
</ul> </ul>
...@@ -31,7 +31,6 @@ ...@@ -31,7 +31,6 @@
<%= link_to l(:button_delete), {:controller => 'queries', :action => 'destroy', :id => @query}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %> <%= link_to l(:button_delete), {:controller => 'queries', :action => 'destroy', :id => @query}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
<% end %> <% end %>
</div> </div>
<h2><%=h @query.name %></h2> <h2><%=h @query.name %></h2>
<div id="query_form"></div> <div id="query_form"></div>
<% html_title @query.name %> <% html_title @query.name %>
...@@ -41,7 +40,6 @@ ...@@ -41,7 +40,6 @@
<% if @issues.empty? %> <% if @issues.empty? %>
<p class="nodata"><%= l(:label_no_data) %></p> <p class="nodata"><%= l(:label_no_data) %></p>
<% else %> <% else %>
<% form_tag({:controller => 'projects', :action => 'bulk_edit_issues', :id => @project}, :id => 'issues_form', :onsubmit => "if (!checkBulkEdit(this)) {alert('#{l(:notice_no_issue_selected)}'); return false;}" ) do %>
<%= render :partial => 'issues/list', :locals => {:issues => @issues, :query => @query} %> <%= render :partial => 'issues/list', :locals => {:issues => @issues, :query => @query} %>
<div class="contextual"> <div class="contextual">
<%= l(:label_export_to) %> <%= l(:label_export_to) %>
...@@ -51,7 +49,6 @@ ...@@ -51,7 +49,6 @@
<p class="pagination"><%= pagination_links_full @issue_pages, @issue_count %></p> <p class="pagination"><%= pagination_links_full @issue_pages, @issue_count %></p>
<% end %> <% end %>
<% end %> <% end %>
<% end %>
<% content_for :sidebar do %> <% content_for :sidebar do %>
<%= render :partial => 'issues/sidebar' %> <%= render :partial => 'issues/sidebar' %>
...@@ -60,13 +57,4 @@ ...@@ -60,13 +57,4 @@
<% content_for :header_tags do %> <% content_for :header_tags do %>
<%= auto_discovery_link_tag(:atom, {:query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_issue_plural)) %> <%= auto_discovery_link_tag(:atom, {:query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_issue_plural)) %>
<%= auto_discovery_link_tag(:atom, {:action => 'changes', :query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_changes_details)) %> <%= auto_discovery_link_tag(:atom, {:action => 'changes', :query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_changes_details)) %>
<%= javascript_include_tag 'calendar/calendar' %>
<%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %>
<%= javascript_include_tag 'calendar/calendar-setup' %>
<%= stylesheet_link_tag 'calendar' %>
<%= javascript_include_tag 'context_menu' %>
<%= stylesheet_link_tag 'context_menu' %>
<% end %> <% end %>
<div id="context-menu" style="display: none;"></div>
<%= javascript_tag 'new ContextMenu({})' %>
<h2><%=l(:button_move)%></h2> <h2><%= l(:button_move) %></h2>
<ul><%= @issues.collect {|i| content_tag('li', link_to(h("#{i.tracker} ##{i.id}"), { :action => 'show', :id => i }) + h(": #{i.subject}")) }.join("\n") %></ul>
<% form_tag({:action => 'move_issues', :id => @project}, :class => 'tabular', :id => 'move_form') do %> <% form_tag({}, :id => 'move_form') do %>
<%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join %>
<div class="box"> <div class="box tabular">
<p><label><%= l(:label_issue_plural) %> :</label>
<% for issue in @issues %>
<%= link_to_issue issue %>: <%=h issue.subject %>
<%= hidden_field_tag "issue_ids[]", issue.id %><br />
<% end %>
<i>(<%= @issues.length%> <%= lwr(:label_issue, @issues.length)%>)</i></p>
&nbsp;
<!--[form:issue]-->
<p><label for="new_project_id"><%=l(:field_project)%> :</label> <p><label for="new_project_id"><%=l(:field_project)%> :</label>
<%= select_tag "new_project_id", <%= select_tag "new_project_id",
options_from_collection_for_select(@projects, 'id', 'name', @target_project.id), options_from_collection_for_select(@allowed_projects, 'id', 'name', @target_project.id),
:onchange => remote_function(:url => {:action => 'move_issues' , :id => @project}, :onchange => remote_function(:url => {:action => 'move' , :id => @project},
:method => :get, :method => :get,
:update => 'content', :update => 'content',
:with => "Form.serialize('move_form')") %></p> :with => "Form.serialize('move_form')") %></p>
...@@ -25,5 +17,6 @@ ...@@ -25,5 +17,6 @@
<p><label for="new_tracker_id"><%=l(:field_tracker)%> :</label> <p><label for="new_tracker_id"><%=l(:field_tracker)%> :</label>
<%= select_tag "new_tracker_id", "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@trackers, "id", "name") %></p> <%= select_tag "new_tracker_id", "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@trackers, "id", "name") %></p>
</div> </div>
<%= submit_tag l(:button_move) %> <%= submit_tag l(:button_move) %>
<% end %> <% end %>
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, :class => 'icon icon-time' %> <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, :class => 'icon icon-time' %>
<%= watcher_tag(@issue, User.current) %> <%= watcher_tag(@issue, User.current) %>
<%= link_to_if_authorized l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue }, :class => 'icon icon-copy' %> <%= link_to_if_authorized l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue }, :class => 'icon icon-copy' %>
<%= link_to_if_authorized l(:button_move), {:controller => 'projects', :action => 'move_issues', :id => @project, "issue_ids[]" => @issue.id }, :class => 'icon icon-move' %> <%= link_to_if_authorized l(:button_move), {:controller => 'issues', :action => 'move', :id => @issue }, :class => 'icon icon-move' %>
<%= link_to_if_authorized l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %> <%= link_to_if_authorized l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
</div> </div>
......
...@@ -37,6 +37,6 @@ ...@@ -37,6 +37,6 @@
<% end %> <% end %>
<div id="context-menu" style="display: none;"></div> <div id="context-menu" style="display: none;"></div>
<%= javascript_tag 'new ContextMenu({})' %> <%= javascript_tag "new ContextMenu('#{url_for(:controller => 'issues', :action => 'context_menu')}')" %>
<% html_title(l(:label_my_page)) -%> <% html_title(l(:label_my_page)) -%>
...@@ -568,3 +568,4 @@ label_associated_revisions: Асоциирани ревизии ...@@ -568,3 +568,4 @@ label_associated_revisions: Асоциирани ревизии
setting_user_format: Потребителски формат setting_user_format: Потребителски формат
text_status_changed_by_changeset: Applied in changeset %s. text_status_changed_by_changeset: Applied in changeset %s.
label_more: More label_more: More
text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
...@@ -568,3 +568,4 @@ label_associated_revisions: Associated revisions ...@@ -568,3 +568,4 @@ label_associated_revisions: Associated revisions
setting_user_format: Users display format setting_user_format: Users display format
text_status_changed_by_changeset: Applied in changeset %s. text_status_changed_by_changeset: Applied in changeset %s.
label_more: More label_more: More
text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
...@@ -568,3 +568,4 @@ enumeration_doc_categories: Dokumentenkategorien ...@@ -568,3 +568,4 @@ enumeration_doc_categories: Dokumentenkategorien
enumeration_activities: Aktivitäten (Zeiterfassung) enumeration_activities: Aktivitäten (Zeiterfassung)
text_status_changed_by_changeset: Applied in changeset %s. text_status_changed_by_changeset: Applied in changeset %s.
label_more: More label_more: More
text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
...@@ -542,6 +542,7 @@ text_user_mail_option: "For unselected projects, you will only receive notificat ...@@ -542,6 +542,7 @@ text_user_mail_option: "For unselected projects, you will only receive notificat
text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded." text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded."
text_load_default_configuration: Load the default configuration text_load_default_configuration: Load the default configuration
text_status_changed_by_changeset: Applied in changeset %s. text_status_changed_by_changeset: Applied in changeset %s.
text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
default_role_manager: Manager default_role_manager: Manager
default_role_developper: Developer default_role_developper: Developer
......
...@@ -571,3 +571,4 @@ label_associated_revisions: Associated revisions ...@@ -571,3 +571,4 @@ label_associated_revisions: Associated revisions
setting_user_format: Users display format setting_user_format: Users display format
text_status_changed_by_changeset: Applied in changeset %s. text_status_changed_by_changeset: Applied in changeset %s.
label_more: More label_more: More
text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
...@@ -572,3 +572,4 @@ label_associated_revisions: Liittyvät versiot ...@@ -572,3 +572,4 @@ label_associated_revisions: Liittyvät versiot
setting_user_format: Users display format setting_user_format: Users display format
text_status_changed_by_changeset: Applied in changeset %s. text_status_changed_by_changeset: Applied in changeset %s.
label_more: More label_more: More
text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
...@@ -543,6 +543,7 @@ text_user_mail_option: "Pour les projets non sélectionnés, vous recevrez seule ...@@ -543,6 +543,7 @@ text_user_mail_option: "Pour les projets non sélectionnés, vous recevrez seule
text_no_configuration_data: "Les rôles, trackers, statuts et le workflow ne sont pas encore paramétrés.\nIl est vivement recommandé de charger le paramétrage par defaut. Vous pourrez le modifier une fois chargé." text_no_configuration_data: "Les rôles, trackers, statuts et le workflow ne sont pas encore paramétrés.\nIl est vivement recommandé de charger le paramétrage par defaut. Vous pourrez le modifier une fois chargé."
text_load_default_configuration: Charger le paramétrage par défaut text_load_default_configuration: Charger le paramétrage par défaut
text_status_changed_by_changeset: Appliqué par commit %s. text_status_changed_by_changeset: Appliqué par commit %s.
text_issues_destroy_confirmation: 'Etes-vous sûr de vouloir supprimer le(s) demandes(s) selectionnée(s) ?'
default_role_manager: Manager default_role_manager: Manager
default_role_developper: Développeur default_role_developper: Développeur
......
...@@ -568,3 +568,4 @@ label_associated_revisions: Associated revisions ...@@ -568,3 +568,4 @@ label_associated_revisions: Associated revisions
setting_user_format: Users display format setting_user_format: Users display format
text_status_changed_by_changeset: Applied in changeset %s. text_status_changed_by_changeset: Applied in changeset %s.
label_more: More label_more: More
text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
...@@ -568,3 +568,4 @@ label_associated_revisions: Associated revisions ...@@ -568,3 +568,4 @@ label_associated_revisions: Associated revisions
setting_user_format: Users display format setting_user_format: Users display format
text_status_changed_by_changeset: Applied in changeset %s. text_status_changed_by_changeset: Applied in changeset %s.
label_more: More label_more: More
text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
...@@ -569,3 +569,4 @@ label_associated_revisions: Associated revisions ...@@ -569,3 +569,4 @@ label_associated_revisions: Associated revisions
setting_user_format: Users display format setting_user_format: Users display format
text_status_changed_by_changeset: Applied in changeset %s. text_status_changed_by_changeset: Applied in changeset %s.
label_more: More label_more: More
text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
...@@ -568,3 +568,4 @@ label_associated_revisions: Associated revisions ...@@ -568,3 +568,4 @@ label_associated_revisions: Associated revisions
setting_user_format: Users display format setting_user_format: Users display format
text_status_changed_by_changeset: Applied in changeset %s. text_status_changed_by_changeset: Applied in changeset %s.
label_more: More label_more: More
text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
...@@ -569,3 +569,4 @@ label_associated_revisions: susijusios revizijos ...@@ -569,3 +569,4 @@ label_associated_revisions: susijusios revizijos
setting_user_format: Vartotojo atvaizdavimo formatas setting_user_format: Vartotojo atvaizdavimo formatas
text_status_changed_by_changeset: Applied in changeset %s. text_status_changed_by_changeset: Applied in changeset %s.
label_more: More label_more: More
text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
...@@ -569,3 +569,4 @@ label_associated_revisions: Associated revisions ...@@ -569,3 +569,4 @@ label_associated_revisions: Associated revisions
setting_user_format: Users display format setting_user_format: Users display format
text_status_changed_by_changeset: Applied in changeset %s. text_status_changed_by_changeset: Applied in changeset %s.
label_more: More label_more: More
text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
...@@ -568,3 +568,4 @@ label_associated_revisions: Associated revisions ...@@ -568,3 +568,4 @@ label_associated_revisions: Associated revisions
setting_user_format: Users display format setting_user_format: Users display format
text_status_changed_by_changeset: Applied in changeset %s. text_status_changed_by_changeset: Applied in changeset %s.
label_more: More label_more: More
text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
...@@ -568,3 +568,4 @@ label_associated_revisions: Associated revisions ...@@ -568,3 +568,4 @@ label_associated_revisions: Associated revisions
setting_user_format: Users display format setting_user_format: Users display format
text_status_changed_by_changeset: Applied in changeset %s. text_status_changed_by_changeset: Applied in changeset %s.
label_more: More label_more: More
text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
...@@ -568,3 +568,4 @@ label_associated_revisions: Associated revisions ...@@ -568,3 +568,4 @@ label_associated_revisions: Associated revisions
setting_user_format: Users display format setting_user_format: Users display format
text_status_changed_by_changeset: Applied in changeset %s. text_status_changed_by_changeset: Applied in changeset %s.
label_more: More label_more: More
text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
...@@ -568,3 +568,4 @@ label_associated_revisions: Associated revisions ...@@ -568,3 +568,4 @@ label_associated_revisions: Associated revisions
setting_user_format: Users display format setting_user_format: Users display format
text_status_changed_by_changeset: Applied in changeset %s. text_status_changed_by_changeset: Applied in changeset %s.
label_more: More label_more: More
text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
...@@ -569,3 +569,4 @@ enumeration_doc_categories: Категории документов ...@@ -569,3 +569,4 @@ enumeration_doc_categories: Категории документов
enumeration_activities: Действия (учет времени) enumeration_activities: Действия (учет времени)
text_status_changed_by_changeset: Applied in changeset %s. text_status_changed_by_changeset: Applied in changeset %s.
label_more: More label_more: More
text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
...@@ -569,3 +569,4 @@ label_associated_revisions: Associated revisions ...@@ -569,3 +569,4 @@ label_associated_revisions: Associated revisions
setting_user_format: Users display format setting_user_format: Users display format
text_status_changed_by_changeset: Applied in changeset %s. text_status_changed_by_changeset: Applied in changeset %s.
label_more: More label_more: More
text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
...@@ -569,3 +569,4 @@ label_associated_revisions: Associated revisions ...@@ -569,3 +569,4 @@ label_associated_revisions: Associated revisions
setting_user_format: Users display format setting_user_format: Users display format
text_status_changed_by_changeset: Applied in changeset %s. text_status_changed_by_changeset: Applied in changeset %s.
label_more: More label_more: More
text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
...@@ -568,3 +568,4 @@ enumeration_doc_categories: 文件分類 ...@@ -568,3 +568,4 @@ enumeration_doc_categories: 文件分類
enumeration_activities: 活動 (time tracking) enumeration_activities: 活動 (time tracking)
text_status_changed_by_changeset: Applied in changeset %s. text_status_changed_by_changeset: Applied in changeset %s.
label_more: More label_more: More
text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
...@@ -571,3 +571,4 @@ label_associated_revisions: 相关的版本 ...@@ -571,3 +571,4 @@ label_associated_revisions: 相关的版本
setting_user_format: 用户显示格式 setting_user_format: 用户显示格式
text_status_changed_by_changeset: Applied in changeset %s. text_status_changed_by_changeset: Applied in changeset %s.
label_more: More label_more: More
text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
...@@ -31,11 +31,10 @@ Redmine::AccessControl.map do |map| ...@@ -31,11 +31,10 @@ Redmine::AccessControl.map do |map|
:queries => :index, :queries => :index,
:reports => :issue_report}, :public => true :reports => :issue_report}, :public => true
map.permission :add_issues, {:issues => :new} map.permission :add_issues, {:issues => :new}
map.permission :edit_issues, {:projects => :bulk_edit_issues, map.permission :edit_issues, {:issues => [:edit, :bulk_edit, :destroy_attachment]}
:issues => [:edit, :destroy_attachment]}
map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]} map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
map.permission :add_issue_notes, {:issues => :edit} map.permission :add_issue_notes, {:issues => :edit}
map.permission :move_issues, {:projects => :move_issues}, :require => :loggedin map.permission :move_issues, {:issues => :move}, :require => :loggedin
map.permission :delete_issues, {:issues => :destroy}, :require => :member map.permission :delete_issues, {:issues => :destroy}, :require => :member
# Queries # Queries
map.permission :manage_public_queries, {:queries => [:new, :edit, :destroy]}, :require => :member map.permission :manage_public_queries, {:queries => [:new, :edit, :destroy]}, :require => :member
......
/* redMine - project management software
Copyright (C) 2006-2008 Jean-Philippe Lang */
function checkAll (id, checked) { function checkAll (id, checked) {
var el = document.getElementById(id); var el = document.getElementById(id);
for (var i = 0; i < el.elements.length; i++) { for (var i = 0; i < el.elements.length; i++) {
...@@ -49,16 +52,6 @@ function promptToRemote(text, param, url) { ...@@ -49,16 +52,6 @@ function promptToRemote(text, param, url) {
} }
} }
/* checks that at least one checkbox is checked (used when submitting bulk edit form) */
function checkBulkEdit(form) {
for (var i = 0; i < form.elements.length; i++) {
if (form.elements[i].checked) {
return true;
}
}
return false;
}
function collapseScmEntry(id) { function collapseScmEntry(id) {
var els = document.getElementsByClassName(id, 'browser'); var els = document.getElementsByClassName(id, 'browser');
for (var i = 0; i < els.length; i++) { for (var i = 0; i < els.length; i++) {
......
/* redMine - project management software
Copyright (C) 2006-2008 Jean-Philippe Lang */
var observingContextMenuClick;
ContextMenu = Class.create(); ContextMenu = Class.create();
ContextMenu.prototype = { ContextMenu.prototype = {
initialize: function (options) { initialize: function (url) {
this.options = Object.extend({selector: '.hascontextmenu'}, options || { }); this.url = url;
Event.observe(document, 'click', function(e){ // prevent selection when using Ctrl/Shit key
var t = Event.findElement(e, 'a'); var tables = $$('table.issues');
if ((t != document) && (Element.hasClassName(t, 'disabled') || Element.hasClassName(t, 'submenu'))) { for (i=0; i<tables.length; i++) {
Event.stop(e); tables[i].onselectstart = function () { return false; } // ie
} else { tables[i].onmousedown = function () { return false; } // mozilla
$('context-menu').hide(); }
if (this.selection) {
this.selection.removeClassName('context-menu-selection'); if (!observingContextMenuClick) {
} Event.observe(document, 'click', this.Click.bindAsEventListener(this));
} Event.observe(document, (window.opera ? 'click' : 'contextmenu'), this.RightClick.bindAsEventListener(this));
observingContextMenuClick = true;
}.bind(this)); }
$$(this.options.selector).invoke('observe', (window.opera ? 'click' : 'contextmenu'), function(e){ this.unselectAll();
if (window.opera && !e.ctrlKey) { this.lastSelected = null;
return;
}
this.show(e);
}.bind(this));
}, },
show: function(e) {
RightClick: function(e) {
this.hideMenu();
// do not show the context menu on links
if (Event.findElement(e, 'a') != document) { return; }
// right-click simulated by Alt+Click with Opera
if (window.opera && !e.altKey) { return; }
var tr = Event.findElement(e, 'tr');
if ((tr == document) || !tr.hasClassName('hascontextmenu')) { return; }
Event.stop(e); Event.stop(e);
Element.hide('context-menu'); if (!this.isSelected(tr)) {
if (this.selection) { this.unselectAll();
this.selection.removeClassName('context-menu-selection'); this.addSelection(tr);
this.lastSelected = tr;
} }
this.showMenu(e);
},
Click: function(e) {
this.hideMenu();
if (Event.findElement(e, 'a') != document) { return; }
if (window.opera && e.altKey) { return; }
if (Event.isLeftClick(e) || (navigator.appVersion.match(/\bMSIE\b/))) {
var tr = Event.findElement(e, 'tr');
if (tr!=document && tr.hasClassName('hascontextmenu')) {
// a row was clicked, check if the click was on checkbox
var box = Event.findElement(e, 'input');
if (box!=document) {
// a checkbox may be clicked
if (box.checked) {
tr.addClassName('context-menu-selection');
} else {
tr.removeClassName('context-menu-selection');
}
} else {
if (e.ctrlKey) {
this.toggleSelection(tr);
} else if (e.shiftKey) {
if (this.lastSelected != null) {
var toggling = false;
var rows = $$('.hascontextmenu');
for (i=0; i<rows.length; i++) {
if (toggling || rows[i]==tr) {
this.addSelection(rows[i]);
}
if (rows[i]==tr || rows[i]==this.lastSelected) {
toggling = !toggling;
}
}
} else {
this.addSelection(tr);
}
} else {
this.unselectAll();
this.addSelection(tr);
}
this.lastSelected = tr;
}
} else {
// click is outside the rows
var t = Event.findElement(e, 'a');
if ((t != document) && (Element.hasClassName(t, 'disabled') || Element.hasClassName(t, 'submenu'))) {
Event.stop(e);
}
}
}
},
showMenu: function(e) {
$('context-menu').style['left'] = (Event.pointerX(e) + 'px'); $('context-menu').style['left'] = (Event.pointerX(e) + 'px');
$('context-menu').style['top'] = (Event.pointerY(e) + 'px'); $('context-menu').style['top'] = (Event.pointerY(e) + 'px');
Element.update('context-menu', ''); Element.update('context-menu', '');
new Ajax.Updater({success:'context-menu'}, this.url,
{asynchronous:true,
evalScripts:true,
parameters:Form.serialize(Event.findElement(e, 'form')),
onComplete:function(request){
Effect.Appear('context-menu', {duration: 0.20});
if (window.parseStylesheets) { window.parseStylesheets(); } // IE
}})
},
hideMenu: function() {
Element.hide('context-menu');
},
addSelection: function(tr) {
tr.addClassName('context-menu-selection');
this.checkSelectionBox(tr, true);
},
toggleSelection: function(tr) {
if (this.isSelected(tr)) {
this.removeSelection(tr);
} else {
this.addSelection(tr);
}
},
removeSelection: function(tr) {
tr.removeClassName('context-menu-selection');
this.checkSelectionBox(tr, false);
},
unselectAll: function() {
var rows = $$('.hascontextmenu');
for (i=0; i<rows.length; i++) {
this.removeSelection(rows[i]);
}
},
checkSelectionBox: function(tr, checked) {
var inputs = Element.getElementsBySelector(tr, 'input');
if (inputs.length > 0) { inputs[0].checked = checked; }
},
isSelected: function(tr) {
return Element.hasClassName(tr, 'context-menu-selection');
}
}
var tr = Event.findElement(e, 'tr'); function toggleIssuesSelection(el) {
tr.addClassName('context-menu-selection'); var boxes = el.getElementsBySelector('input[type=checkbox]');
this.selection = tr; var all_checked = true;
var id = tr.id.substring(6, tr.id.length); for (i = 0; i < boxes.length; i++) { if (boxes[i].checked == false) { all_checked = false; } }
/* TODO: do not hard code path */ for (i = 0; i < boxes.length; i++) {
new Ajax.Updater({success:'context-menu'}, '../../issues/context_menu/' + id, {asynchronous:true, evalScripts:true, onComplete:function(request){ if (all_checked) {
Effect.Appear('context-menu', {duration: 0.20}); boxes[i].checked = false;
if (window.parseStylesheets) { window.parseStylesheets(); } boxes[i].up('tr').removeClassName('context-menu-selection');
}}) } else if (boxes[i].checked == false) {
boxes[i].checked = true;
boxes[i].up('tr').addClassName('context-menu-selection');
}
} }
} }
...@@ -197,6 +197,28 @@ class IssuesControllerTest < Test::Unit::TestCase ...@@ -197,6 +197,28 @@ class IssuesControllerTest < Test::Unit::TestCase
assert_not_nil assigns(:issue) assert_not_nil assigns(:issue)
assert_equal Issue.find(1), assigns(:issue) assert_equal Issue.find(1), assigns(:issue)
end end
def test_get_edit_with_params
@request.session[:user_id] = 2
get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
assert_response :success
assert_template 'edit'
issue = assigns(:issue)
assert_not_nil issue
assert_equal 5, issue.status_id
assert_tag :select, :attributes => { :name => 'issue[status_id]' },
:child => { :tag => 'option',
:content => 'Closed',
:attributes => { :selected => 'selected' } }
assert_equal 7, issue.priority_id
assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
:child => { :tag => 'option',
:content => 'Urgent',
:attributes => { :selected => 'selected' } }
end
def test_post_edit def test_post_edit
@request.session[:user_id] = 2 @request.session[:user_id] = 2
...@@ -305,12 +327,105 @@ class IssuesControllerTest < Test::Unit::TestCase ...@@ -305,12 +327,105 @@ class IssuesControllerTest < Test::Unit::TestCase
# No email should be sent # No email should be sent
assert ActionMailer::Base.deliveries.empty? assert ActionMailer::Base.deliveries.empty?
end end
def test_bulk_edit
@request.session[:user_id] = 2
# update issues priority
post :bulk_edit, :ids => [1, 2], :priority_id => 7, :notes => 'Bulk editing', :assigned_to_id => ''
assert_response 302
# check that the issues were updated
assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
assert_equal 'Bulk editing', Issue.find(1).journals.find(:first, :order => 'created_on DESC').notes
end
def test_move_one_issue_to_another_project
@request.session[:user_id] = 1
post :move, :id => 1, :new_project_id => 2
assert_redirected_to 'projects/ecookbook/issues'
assert_equal 2, Issue.find(1).project_id
end
def test_bulk_move_to_another_project
@request.session[:user_id] = 1
post :move, :ids => [1, 2], :new_project_id => 2
assert_redirected_to 'projects/ecookbook/issues'
# Issues moved to project 2
assert_equal 2, Issue.find(1).project_id
assert_equal 2, Issue.find(2).project_id
# No tracker change
assert_equal 1, Issue.find(1).tracker_id
assert_equal 2, Issue.find(2).tracker_id
end
def test_bulk_move_to_another_tracker
@request.session[:user_id] = 1
post :move, :ids => [1, 2], :new_tracker_id => 2
assert_redirected_to 'projects/ecookbook/issues'
assert_equal 2, Issue.find(1).tracker_id
assert_equal 2, Issue.find(2).tracker_id
end
def test_context_menu def test_context_menu_one_issue
@request.session[:user_id] = 2
get :context_menu, :ids => [1]
assert_response :success
assert_template 'context_menu'
assert_tag :tag => 'a', :content => 'Edit',
:attributes => { :href => '/issues/edit/1',
:class => 'icon-edit' }
assert_tag :tag => 'a', :content => 'Closed',
:attributes => { :href => '/issues/edit/1?issue%5Bstatus_id%5D=5',
:class => '' }
assert_tag :tag => 'a', :content => 'Immediate',
:attributes => { :href => '/issues/edit/1?issue%5Bpriority_id%5D=8',
:class => '' }
assert_tag :tag => 'a', :content => 'Dave Lopper',
:attributes => { :href => '/issues/edit/1?issue%5Bassigned_to_id%5D=3',
:class => '' }
assert_tag :tag => 'a', :content => 'Copy',
:attributes => { :href => '/projects/ecookbook/issues/new?copy_from=1',
:class => 'icon-copy' }
assert_tag :tag => 'a', :content => 'Move',
:attributes => { :href => '/issues/move?ids%5B%5D=1',
:class => 'icon-move' }
assert_tag :tag => 'a', :content => 'Delete',
:attributes => { :href => '/issues/destroy?ids%5B%5D=1',
:class => 'icon-del' }
end
def test_context_menu_one_issue_by_anonymous
get :context_menu, :ids => [1]
assert_response :success
assert_template 'context_menu'
assert_tag :tag => 'a', :content => 'Delete',
:attributes => { :href => '#',
:class => 'icon-del disabled' }
end
def test_context_menu_multiple_issues_of_same_project
@request.session[:user_id] = 2
get :context_menu, :ids => [1, 2]
assert_response :success
assert_template 'context_menu'
assert_tag :tag => 'a', :content => 'Edit',
:attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
:class => 'icon-edit' }
assert_tag :tag => 'a', :content => 'Move',
:attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
:class => 'icon-move' }
assert_tag :tag => 'a', :content => 'Delete',
:attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
:class => 'icon-del' }
end
def test_context_menu_multiple_issues_of_different_project
@request.session[:user_id] = 2 @request.session[:user_id] = 2
get :context_menu, :id => 1 get :context_menu, :ids => [1, 2, 4]
assert_response :success assert_response :success
assert_template 'context_menu' assert_template 'context_menu'
assert_tag :tag => 'a', :content => 'Delete',
:attributes => { :href => '#',
:class => 'icon-del disabled' }
end end
def test_destroy def test_destroy
......
...@@ -93,32 +93,6 @@ class ProjectsControllerTest < Test::Unit::TestCase ...@@ -93,32 +93,6 @@ class ProjectsControllerTest < Test::Unit::TestCase
assert_nil Project.find_by_id(1) assert_nil Project.find_by_id(1)
end end
def test_bulk_edit_issues
@request.session[:user_id] = 2
# update issues priority
post :bulk_edit_issues, :id => 1, :issue_ids => [1, 2], :priority_id => 7, :notes => 'Bulk editing', :assigned_to_id => ''
assert_response 302
# check that the issues were updated
assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
assert_equal 'Bulk editing', Issue.find(1).journals.find(:first, :order => 'created_on DESC').notes
end
def test_move_issues_to_another_project
@request.session[:user_id] = 1
post :move_issues, :id => 1, :issue_ids => [1, 2], :new_project_id => 2
assert_redirected_to 'projects/ecookbook/issues'
assert_equal 2, Issue.find(1).project_id
assert_equal 2, Issue.find(2).project_id
end
def test_move_issues_to_another_tracker
@request.session[:user_id] = 1
post :move_issues, :id => 1, :issue_ids => [1, 2], :new_tracker_id => 2
assert_redirected_to 'projects/ecookbook/issues'
assert_equal 2, Issue.find(1).tracker_id
assert_equal 2, Issue.find(2).tracker_id
end
def test_list_files def test_list_files
get :list_files, :id => 1 get :list_files, :id => 1
assert_response :success assert_response :success
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment