summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJean-Philippe Lang <jp_lang@yahoo.fr>2008-02-10 13:17:41 +0000
committerJean-Philippe Lang <jp_lang@yahoo.fr>2008-02-10 13:17:41 +0000
commit4155c97222cea69406060979efca3ebaaed9dcec (patch)
tree17bc056c97c1af47914bb2599995d2901749ce35
parent43a6f312edde2399c9c986ed61b1e9b0e1066db6 (diff)
downloadredmine-4155c97222cea69406060979efca3ebaaed9dcec.tar.gz
redmine-4155c97222cea69406060979efca3ebaaed9dcec.zip
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
-rw-r--r--app/controllers/issues_controller.rb142
-rw-r--r--app/controllers/projects_controller.rb79
-rw-r--r--app/views/issues/_list.rhtml28
-rw-r--r--app/views/issues/_list_simple.rhtml11
-rw-r--r--app/views/issues/bulk_edit.rhtml (renamed from app/views/issues/_bulk_edit_form.rhtml)23
-rw-r--r--app/views/issues/context_menu.rhtml39
-rw-r--r--app/views/issues/index.rhtml12
-rw-r--r--app/views/issues/move.rhtml (renamed from app/views/projects/move_issues.rhtml)23
-rw-r--r--app/views/issues/show.rhtml2
-rw-r--r--app/views/my/page.rhtml2
-rw-r--r--lang/bg.yml1
-rw-r--r--lang/cs.yml1
-rw-r--r--lang/de.yml1
-rw-r--r--lang/en.yml1
-rw-r--r--lang/es.yml1
-rw-r--r--lang/fi.yml1
-rw-r--r--lang/fr.yml1
-rw-r--r--lang/he.yml1
-rw-r--r--lang/it.yml1
-rw-r--r--lang/ja.yml1
-rw-r--r--lang/ko.yml1
-rw-r--r--lang/lt.yml1
-rw-r--r--lang/nl.yml1
-rw-r--r--lang/pl.yml1
-rw-r--r--lang/pt-br.yml1
-rw-r--r--lang/pt.yml1
-rw-r--r--lang/ro.yml1
-rw-r--r--lang/ru.yml1
-rw-r--r--lang/sr.yml1
-rw-r--r--lang/sv.yml1
-rw-r--r--lang/zh-tw.yml1
-rw-r--r--lang/zh.yml1
-rw-r--r--lib/redmine.rb5
-rw-r--r--public/javascripts/application.js13
-rw-r--r--public/javascripts/context_menu.js186
-rw-r--r--test/functional/issues_controller_test.rb119
-rw-r--r--test/functional/projects_controller_test.rb26
37 files changed, 485 insertions, 247 deletions
diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb
index dbb49405c..85151e905 100644
--- a/app/controllers/issues_controller.rb
+++ b/app/controllers/issues_controller.rb
@@ -19,13 +19,14 @@ class IssuesController < ApplicationController
layout 'base'
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 :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]
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 :projects
@@ -152,18 +153,20 @@ class IssuesController < ApplicationController
@priorities = Enumeration::get_values('IPRI')
@custom_values = []
@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?
@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
- @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
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]) }
@@ -191,8 +194,78 @@ class IssuesController < ApplicationController
flash.now[:error] = l(:notice_locking_conflict)
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
- @issue.destroy
+ @issues.each(&:destroy)
redirect_to :action => 'index', :project_id => @project
end
@@ -208,17 +281,27 @@ class IssuesController < ApplicationController
end
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
@statuses = IssueStatus.find(:all, :order => 'position')
- @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)
- @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)}
+ @back = request.env['HTTP_REFERER']
+
render :layout => false
end
@@ -242,6 +325,21 @@ private
render_404
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
@project = Project.find(params[:project_id])
rescue ActiveRecord::RecordNotFound
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 9560a451f..cddfb6f81 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -22,7 +22,7 @@ class ProjectsController < ApplicationController
menu_item :roadmap, :only => :roadmap
menu_item :files, :only => [:list_files, :add_file]
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 :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy ]
@@ -182,83 +182,6 @@ class ProjectsController < ApplicationController
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
if request.post?
@version = @project.versions.find_by_id(params[:version_id])
diff --git a/app/views/issues/_list.rhtml b/app/views/issues/_list.rhtml
index ff91f34f7..1f5470200 100644
--- a/app/views/issues/_list.rhtml
+++ b/app/views/issues/_list.rhtml
@@ -1,10 +1,7 @@
-<div id="bulk-edit"></div>
-<table class="list">
+<% form_tag({}) do -%>
+<table class="list issues">
<thead><tr>
- <th><%= link_to_remote(image_tag('edit.png'),
- {: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><%= link_to image_tag('edit.png'), {}, :onclick => 'toggleIssuesSelection(this.up("form")); return false;' %>
</th>
<%= sort_header_tag("#{Issue.table_name}.id", :caption => '#', :default_order => 'desc') %>
<% query.columns.each do |column| %>
@@ -12,14 +9,21 @@
<% end %>
</tr></thead>
<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}" %>">
- <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>
- <% query.columns.each do |column| %>
- <%= content_tag 'td', column_content(column, issue), :class => column.name %>
- <% end %>
+ <% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.name %><% end %>
</tr>
- <% end %>
+ <% end -%>
</tbody>
</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')}')" %>
diff --git a/app/views/issues/_list_simple.rhtml b/app/views/issues/_list_simple.rhtml
index eb93f8ea1..8900b7359 100644
--- a/app/views/issues/_list_simple.rhtml
+++ b/app/views/issues/_list_simple.rhtml
@@ -1,5 +1,6 @@
-<% if issues.length > 0 %>
- <table class="list">
+<% if issues && issues.any? %>
+<% form_tag({}) do %>
+ <table class="list issues">
<thead><tr>
<th>#</th>
<th><%=l(:field_tracker)%></th>
@@ -9,6 +10,7 @@
<% for issue in issues %>
<tr id="issue-<%= issue.id %>" class="issue hascontextmenu <%= cycle('odd', 'even') %> <%= "status-#{issue.status.position} priority-#{issue.priority.position}" %>">
<td class="id">
+ <%= check_box_tag("ids[]", issue.id, false, :style => 'display:none;') %>
<%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %>
</td>
<td><%=h issue.project.name %> - <%= issue.tracker.name %><br />
@@ -20,6 +22,7 @@
<% end %>
</tbody>
</table>
+<% end %>
<% else %>
- <i><%=l(:label_no_data)%></i>
-<% end %> \ No newline at end of file
+ <p class="nodata"><%= l(:label_no_data) %></p>
+<% end %>
diff --git a/app/views/issues/_bulk_edit_form.rhtml b/app/views/issues/bulk_edit.rhtml
index e9e1cef86..d19262271 100644
--- a/app/views/issues/_bulk_edit_form.rhtml
+++ b/app/views/issues/bulk_edit.rhtml
@@ -1,6 +1,12 @@
-<div id="bulk-edit-fields">
-<fieldset class="box"><legend><%= l(:label_bulk_edit_selected_issues) %></legend>
+<h2><%= l(:label_bulk_edit_selected_issues) %></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() do %>
+<%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join %>
+<div class="box">
+<fieldset>
+<legend><%= l(:label_change_properties) %></legend>
<p>
<% if @available_statuses.any? %>
<label><%= l(:field_status) %>:
@@ -28,11 +34,12 @@
<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>
</p>
-
-<label for="notes"><%= l(:field_notes) %></label><br />
-<%= text_area_tag 'notes', '', :cols => 80, :rows => 5 %>
-
</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>
+
+<p><%= submit_tag l(:button_submit) %>
+<% end %>
diff --git a/app/views/issues/context_menu.rhtml b/app/views/issues/context_menu.rhtml
index 46b177067..b3a03b05d 100644
--- a/app/views/issues/context_menu.rhtml
+++ b/app/views/issues/context_menu.rhtml
@@ -1,40 +1,45 @@
-<% back_to = url_for(:controller => 'issues', :action => 'index', :project_id => @project) %>
<ul>
+<% if !@issue.nil? -%>
<li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'edit', :id => @issue},
:class => 'icon-edit', :disabled => !@can[:edit] %></li>
<li class="folder">
<a href="#" class="submenu" onclick="return false;"><%= l(:field_status) %></a>
<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}},
- :selected => (s == @issue.status), :disabled => !(@allowed_statuses.include?(s)) %></li>
- <% end %>
+ :selected => (s == @issue.status), :disabled => !(@can[:update] && @allowed_statuses.include?(s)) %></li>
+ <% end -%>
</ul>
</li>
<li class="folder">
<a href="#" class="submenu"><%= l(:field_priority) %></a>
<ul>
- <% @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,
+ <% @priorities.each do |p| -%>
+ <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>
- <% end %>
+ <% end -%>
</ul>
</li>
<li class="folder">
<a href="#" class="submenu"><%= l(:field_assigned_to) %></a>
<ul>
- <% @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,
- :selected => (u == @issue.assigned_to), :disabled => !@can[:assign] %></li>
- <% 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,
- :selected => @issue.assigned_to.nil?, :disabled => !@can[:assign] %></li>
+ <% @assignables.each do |u| -%>
+ <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[:update] %></li>
+ <% end -%>
+ <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[:update] %></li>
</ul>
</li>
<li><%= context_menu_link l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue},
: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 },
- :class => 'icon-move', :disabled => !@can[:move] %>
- <li><%= context_menu_link l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue},
- :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon-del', :disabled => !@can[:delete] %></li>
+<% else -%>
+ <li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id)},
+ :class => 'icon-edit', :disabled => !@can[:edit] %></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>
diff --git a/app/views/issues/index.rhtml b/app/views/issues/index.rhtml
index 48697c505..c5f26dfb6 100644
--- a/app/views/issues/index.rhtml
+++ b/app/views/issues/index.rhtml
@@ -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' %>
<% end %>
</div>
-
<h2><%=h @query.name %></h2>
<div id="query_form"></div>
<% html_title @query.name %>
@@ -41,7 +40,6 @@
<% if @issues.empty? %>
<p class="nodata"><%= l(:label_no_data) %></p>
<% 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} %>
<div class="contextual">
<%= l(:label_export_to) %>
@@ -51,7 +49,6 @@
<p class="pagination"><%= pagination_links_full @issue_pages, @issue_count %></p>
<% end %>
<% end %>
-<% end %>
<% content_for :sidebar do %>
<%= render :partial => 'issues/sidebar' %>
@@ -60,13 +57,4 @@
<% 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, {: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 %>
-
-<div id="context-menu" style="display: none;"></div>
-<%= javascript_tag 'new ContextMenu({})' %>
diff --git a/app/views/projects/move_issues.rhtml b/app/views/issues/move.rhtml
index 95eaf9dec..c74270f1a 100644
--- a/app/views/projects/move_issues.rhtml
+++ b/app/views/issues/move.rhtml
@@ -1,23 +1,15 @@
-<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">
-<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]-->
+<div class="box tabular">
<p><label for="new_project_id"><%=l(:field_project)%> :</label>
<%= select_tag "new_project_id",
- options_from_collection_for_select(@projects, 'id', 'name', @target_project.id),
- :onchange => remote_function(:url => {:action => 'move_issues' , :id => @project},
+ options_from_collection_for_select(@allowed_projects, 'id', 'name', @target_project.id),
+ :onchange => remote_function(:url => {:action => 'move' , :id => @project},
:method => :get,
:update => 'content',
:with => "Form.serialize('move_form')") %></p>
@@ -25,5 +17,6 @@
<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>
</div>
+
<%= submit_tag l(:button_move) %>
<% end %>
diff --git a/app/views/issues/show.rhtml b/app/views/issues/show.rhtml
index a16dc60e0..3392aef83 100644
--- a/app/views/issues/show.rhtml
+++ b/app/views/issues/show.rhtml
@@ -3,7 +3,7 @@
<%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, :class => 'icon icon-time' %>
<%= 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_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' %>
</div>
diff --git a/app/views/my/page.rhtml b/app/views/my/page.rhtml
index 26ee44fcc..4d4c921b6 100644
--- a/app/views/my/page.rhtml
+++ b/app/views/my/page.rhtml
@@ -37,6 +37,6 @@
<% end %>
<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)) -%>
diff --git a/lang/bg.yml b/lang/bg.yml
index bc0e070c0..164d67671 100644
--- a/lang/bg.yml
+++ b/lang/bg.yml
@@ -568,3 +568,4 @@ label_associated_revisions: Асоциирани ревизии
setting_user_format: Потребителски формат
text_status_changed_by_changeset: Applied in changeset %s.
label_more: More
+text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
diff --git a/lang/cs.yml b/lang/cs.yml
index 35bda2b6a..86a111cc7 100644
--- a/lang/cs.yml
+++ b/lang/cs.yml
@@ -568,3 +568,4 @@ label_associated_revisions: Associated revisions
setting_user_format: Users display format
text_status_changed_by_changeset: Applied in changeset %s.
label_more: More
+text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
diff --git a/lang/de.yml b/lang/de.yml
index fd296b600..c5a6cdf95 100644
--- a/lang/de.yml
+++ b/lang/de.yml
@@ -568,3 +568,4 @@ enumeration_doc_categories: Dokumentenkategorien
enumeration_activities: Aktivitäten (Zeiterfassung)
text_status_changed_by_changeset: Applied in changeset %s.
label_more: More
+text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
diff --git a/lang/en.yml b/lang/en.yml
index b86eeef2b..a37be93c7 100644
--- a/lang/en.yml
+++ b/lang/en.yml
@@ -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_load_default_configuration: Load the default configuration
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_developper: Developer
diff --git a/lang/es.yml b/lang/es.yml
index 60e4a4d13..1bc7d00fb 100644
--- a/lang/es.yml
+++ b/lang/es.yml
@@ -571,3 +571,4 @@ label_associated_revisions: Associated revisions
setting_user_format: Users display format
text_status_changed_by_changeset: Applied in changeset %s.
label_more: More
+text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
diff --git a/lang/fi.yml b/lang/fi.yml
index 38cf19a46..7a8848302 100644
--- a/lang/fi.yml
+++ b/lang/fi.yml
@@ -572,3 +572,4 @@ label_associated_revisions: Liittyvät versiot
setting_user_format: Users display format
text_status_changed_by_changeset: Applied in changeset %s.
label_more: More
+text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
diff --git a/lang/fr.yml b/lang/fr.yml
index e428935af..8c569fc5a 100644
--- a/lang/fr.yml
+++ b/lang/fr.yml
@@ -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_load_default_configuration: Charger le paramétrage par défaut
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_developper: Développeur
diff --git a/lang/he.yml b/lang/he.yml
index ca577558a..6631a457f 100644
--- a/lang/he.yml
+++ b/lang/he.yml
@@ -568,3 +568,4 @@ label_associated_revisions: Associated revisions
setting_user_format: Users display format
text_status_changed_by_changeset: Applied in changeset %s.
label_more: More
+text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
diff --git a/lang/it.yml b/lang/it.yml
index f227c6942..3c77c97a6 100644
--- a/lang/it.yml
+++ b/lang/it.yml
@@ -568,3 +568,4 @@ label_associated_revisions: Associated revisions
setting_user_format: Users display format
text_status_changed_by_changeset: Applied in changeset %s.
label_more: More
+text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
diff --git a/lang/ja.yml b/lang/ja.yml
index 92b79b6ef..50d23e80f 100644
--- a/lang/ja.yml
+++ b/lang/ja.yml
@@ -569,3 +569,4 @@ label_associated_revisions: Associated revisions
setting_user_format: Users display format
text_status_changed_by_changeset: Applied in changeset %s.
label_more: More
+text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
diff --git a/lang/ko.yml b/lang/ko.yml
index 097d74788..62ec95ef3 100644
--- a/lang/ko.yml
+++ b/lang/ko.yml
@@ -568,3 +568,4 @@ label_associated_revisions: Associated revisions
setting_user_format: Users display format
text_status_changed_by_changeset: Applied in changeset %s.
label_more: More
+text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
diff --git a/lang/lt.yml b/lang/lt.yml
index 3106bd764..a7b12736a 100644
--- a/lang/lt.yml
+++ b/lang/lt.yml
@@ -569,3 +569,4 @@ label_associated_revisions: susijusios revizijos
setting_user_format: Vartotojo atvaizdavimo formatas
text_status_changed_by_changeset: Applied in changeset %s.
label_more: More
+text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
diff --git a/lang/nl.yml b/lang/nl.yml
index 1c76180be..7b5f6bf3a 100644
--- a/lang/nl.yml
+++ b/lang/nl.yml
@@ -569,3 +569,4 @@ label_associated_revisions: Associated revisions
setting_user_format: Users display format
text_status_changed_by_changeset: Applied in changeset %s.
label_more: More
+text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
diff --git a/lang/pl.yml b/lang/pl.yml
index 3ebce3e92..5c12c658f 100644
--- a/lang/pl.yml
+++ b/lang/pl.yml
@@ -568,3 +568,4 @@ label_associated_revisions: Associated revisions
setting_user_format: Users display format
text_status_changed_by_changeset: Applied in changeset %s.
label_more: More
+text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
diff --git a/lang/pt-br.yml b/lang/pt-br.yml
index 688ce0cfd..4616e83a7 100644
--- a/lang/pt-br.yml
+++ b/lang/pt-br.yml
@@ -568,3 +568,4 @@ label_associated_revisions: Associated revisions
setting_user_format: Users display format
text_status_changed_by_changeset: Applied in changeset %s.
label_more: More
+text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
diff --git a/lang/pt.yml b/lang/pt.yml
index 4734c66fc..31df179f2 100644
--- a/lang/pt.yml
+++ b/lang/pt.yml
@@ -568,3 +568,4 @@ label_associated_revisions: Associated revisions
setting_user_format: Users display format
text_status_changed_by_changeset: Applied in changeset %s.
label_more: More
+text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
diff --git a/lang/ro.yml b/lang/ro.yml
index be566b959..52e856683 100644
--- a/lang/ro.yml
+++ b/lang/ro.yml
@@ -568,3 +568,4 @@ label_associated_revisions: Associated revisions
setting_user_format: Users display format
text_status_changed_by_changeset: Applied in changeset %s.
label_more: More
+text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
diff --git a/lang/ru.yml b/lang/ru.yml
index 6921169ce..c720dfb72 100644
--- a/lang/ru.yml
+++ b/lang/ru.yml
@@ -569,3 +569,4 @@ enumeration_doc_categories: Категории документов
enumeration_activities: Действия (учет времени)
text_status_changed_by_changeset: Applied in changeset %s.
label_more: More
+text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
diff --git a/lang/sr.yml b/lang/sr.yml
index ddf84a5d3..c32394dc3 100644
--- a/lang/sr.yml
+++ b/lang/sr.yml
@@ -569,3 +569,4 @@ label_associated_revisions: Associated revisions
setting_user_format: Users display format
text_status_changed_by_changeset: Applied in changeset %s.
label_more: More
+text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
diff --git a/lang/sv.yml b/lang/sv.yml
index 4bec394ce..40a1ce8f6 100644
--- a/lang/sv.yml
+++ b/lang/sv.yml
@@ -569,3 +569,4 @@ label_associated_revisions: Associated revisions
setting_user_format: Users display format
text_status_changed_by_changeset: Applied in changeset %s.
label_more: More
+text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
diff --git a/lang/zh-tw.yml b/lang/zh-tw.yml
index 50a6384f2..601f5c2ec 100644
--- a/lang/zh-tw.yml
+++ b/lang/zh-tw.yml
@@ -568,3 +568,4 @@ enumeration_doc_categories: 文件分類
enumeration_activities: 活動 (time tracking)
text_status_changed_by_changeset: Applied in changeset %s.
label_more: More
+text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
diff --git a/lang/zh.yml b/lang/zh.yml
index dc431f5c0..fd513a96a 100644
--- a/lang/zh.yml
+++ b/lang/zh.yml
@@ -571,3 +571,4 @@ label_associated_revisions: 相关的版本
setting_user_format: 用户显示格式
text_status_changed_by_changeset: Applied in changeset %s.
label_more: More
+text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
diff --git a/lib/redmine.rb b/lib/redmine.rb
index 69151012b..9bec55409 100644
--- a/lib/redmine.rb
+++ b/lib/redmine.rb
@@ -31,11 +31,10 @@ Redmine::AccessControl.map do |map|
:queries => :index,
:reports => :issue_report}, :public => true
map.permission :add_issues, {:issues => :new}
- map.permission :edit_issues, {:projects => :bulk_edit_issues,
- :issues => [:edit, :destroy_attachment]}
+ map.permission :edit_issues, {:issues => [:edit, :bulk_edit, :destroy_attachment]}
map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
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
# Queries
map.permission :manage_public_queries, {:queries => [:new, :edit, :destroy]}, :require => :member
diff --git a/public/javascripts/application.js b/public/javascripts/application.js
index 5ad04e91d..d77362a06 100644
--- a/public/javascripts/application.js
+++ b/public/javascripts/application.js
@@ -1,3 +1,6 @@
+/* redMine - project management software
+ Copyright (C) 2006-2008 Jean-Philippe Lang */
+
function checkAll (id, checked) {
var el = document.getElementById(id);
for (var i = 0; i < el.elements.length; i++) {
@@ -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) {
var els = document.getElementsByClassName(id, 'browser');
for (var i = 0; i < els.length; i++) {
diff --git a/public/javascripts/context_menu.js b/public/javascripts/context_menu.js
index 11754cde8..e3f128d89 100644
--- a/public/javascripts/context_menu.js
+++ b/public/javascripts/context_menu.js
@@ -1,47 +1,161 @@
+/* redMine - project management software
+ Copyright (C) 2006-2008 Jean-Philippe Lang */
+
+var observingContextMenuClick;
+
ContextMenu = Class.create();
ContextMenu.prototype = {
- initialize: function (options) {
- this.options = Object.extend({selector: '.hascontextmenu'}, options || { });
-
- Event.observe(document, 'click', function(e){
- var t = Event.findElement(e, 'a');
- if ((t != document) && (Element.hasClassName(t, 'disabled') || Element.hasClassName(t, 'submenu'))) {
- Event.stop(e);
- } else {
- $('context-menu').hide();
- if (this.selection) {
- this.selection.removeClassName('context-menu-selection');
- }
- }
-
- }.bind(this));
-
- $$(this.options.selector).invoke('observe', (window.opera ? 'click' : 'contextmenu'), function(e){
- if (window.opera && !e.ctrlKey) {
- return;
- }
- this.show(e);
- }.bind(this));
-
+ initialize: function (url) {
+ this.url = url;
+
+ // prevent selection when using Ctrl/Shit key
+ var tables = $$('table.issues');
+ for (i=0; i<tables.length; i++) {
+ tables[i].onselectstart = function () { return false; } // ie
+ tables[i].onmousedown = function () { return false; } // mozilla
+ }
+
+ if (!observingContextMenuClick) {
+ Event.observe(document, 'click', this.Click.bindAsEventListener(this));
+ Event.observe(document, (window.opera ? 'click' : 'contextmenu'), this.RightClick.bindAsEventListener(this));
+ observingContextMenuClick = true;
+ }
+
+ this.unselectAll();
+ this.lastSelected = null;
},
- 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);
- Element.hide('context-menu');
- if (this.selection) {
- this.selection.removeClassName('context-menu-selection');
+ if (!this.isSelected(tr)) {
+ this.unselectAll();
+ 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['top'] = (Event.pointerY(e) + 'px');
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');
- tr.addClassName('context-menu-selection');
- this.selection = tr;
- var id = tr.id.substring(6, tr.id.length);
- /* TODO: do not hard code path */
- new Ajax.Updater({success:'context-menu'}, '../../issues/context_menu/' + id, {asynchronous:true, evalScripts:true, onComplete:function(request){
- Effect.Appear('context-menu', {duration: 0.20});
- if (window.parseStylesheets) { window.parseStylesheets(); }
- }})
+function toggleIssuesSelection(el) {
+ var boxes = el.getElementsBySelector('input[type=checkbox]');
+ var all_checked = true;
+ for (i = 0; i < boxes.length; i++) { if (boxes[i].checked == false) { all_checked = false; } }
+ for (i = 0; i < boxes.length; i++) {
+ if (all_checked) {
+ boxes[i].checked = false;
+ 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');
+ }
}
}
diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb
index 6fdbb0341..4f28ab224 100644
--- a/test/functional/issues_controller_test.rb
+++ b/test/functional/issues_controller_test.rb
@@ -197,6 +197,28 @@ class IssuesControllerTest < Test::Unit::TestCase
assert_not_nil assigns(:issue)
assert_equal Issue.find(1), assigns(:issue)
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
@request.session[:user_id] = 2
@@ -305,12 +327,105 @@ class IssuesControllerTest < Test::Unit::TestCase
# No email should be sent
assert ActionMailer::Base.deliveries.empty?
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
- get :context_menu, :id => 1
+ get :context_menu, :ids => [1, 2, 4]
assert_response :success
assert_template 'context_menu'
+ assert_tag :tag => 'a', :content => 'Delete',
+ :attributes => { :href => '#',
+ :class => 'icon-del disabled' }
end
def test_destroy
diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb
index 71a3e3dfe..92ac6f09a 100644
--- a/test/functional/projects_controller_test.rb
+++ b/test/functional/projects_controller_test.rb
@@ -93,32 +93,6 @@ class ProjectsControllerTest < Test::Unit::TestCase
assert_nil Project.find_by_id(1)
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
get :list_files, :id => 1
assert_response :success