Index: trunk/app/controllers/issues_controller.rb
===================================================================
--- trunk/app/controllers/issues_controller.rb	(revision 1129)
+++ trunk/app/controllers/issues_controller.rb	(revision 1130)
@@ -19,13 +19,14 @@
   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 @@
     @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 @@
     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 @@
   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 @@
     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
Index: trunk/app/controllers/projects_controller.rb
===================================================================
--- trunk/app/controllers/projects_controller.rb	(revision 1129)
+++ trunk/app/controllers/projects_controller.rb	(revision 1130)
@@ -22,7 +22,7 @@
   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 @@
   	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])
Index: trunk/app/views/issues/_bulk_edit_form.rhtml
===================================================================
--- trunk/app/views/issues/_bulk_edit_form.rhtml	(revision 1129)
+++ trunk/app/views/issues/_bulk_edit_form.rhtml	(nonexistent)
@@ -1,38 +0,0 @@
-<div id="bulk-edit-fields">
-<fieldset class="box"><legend><%= l(:label_bulk_edit_selected_issues) %></legend>
-
-<p>
-<% if @available_statuses.any? %>
-<label><%= l(:field_status) %>: 
-<%= select_tag('status_id', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@available_statuses, :id, :name)) %></label>
-<% end %>
-<label><%= l(:field_priority) %>: 
-<%= select_tag('priority_id', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(Enumeration.get_values('IPRI'), :id, :name)) %></label>
-<label><%= l(:field_category) %>: 
-<%= select_tag('category_id', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@project.issue_categories, :id, :name)) %></label>
-</p>
-<p>
-<label><%= l(:field_assigned_to) %>: 
-<%= select_tag('assigned_to_id', content_tag('option', l(:label_no_change_option), :value => '') +
-                                 content_tag('option', l(:label_nobody), :value => 'none') +
-                                 options_from_collection_for_select(@project.assignable_users, :id, :name)) %></label>
-<label><%= l(:field_fixed_version) %>: 
-<%= select_tag('fixed_version_id', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@project.versions, :id, :name)) %></label>
-</p>
-
-<p>
-<label><%= l(:field_start_date) %>: 
-<%= text_field_tag 'start_date', '', :size => 10 %><%= calendar_for('start_date') %></label>
-<label><%= l(:field_due_date) %>: 
-<%= text_field_tag 'due_date', '', :size => 10 %><%= calendar_for('due_date') %></label>
-<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>
-</div>

Property changes on: trunk/app/views/issues/_bulk_edit_form.rhtml
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: trunk/app/views/issues/_list.rhtml
===================================================================
--- trunk/app/views/issues/_list.rhtml	(revision 1129)
+++ trunk/app/views/issues/_list.rhtml	(revision 1130)
@@ -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')}')" %>
Index: trunk/app/views/issues/_list_simple.rhtml
===================================================================
--- trunk/app/views/issues/_list_simple.rhtml	(revision 1129)
+++ trunk/app/views/issues/_list_simple.rhtml	(revision 1130)
@@ -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 %>
Index: trunk/app/views/issues/bulk_edit.rhtml
===================================================================
--- trunk/app/views/issues/bulk_edit.rhtml	(nonexistent)
+++ trunk/app/views/issues/bulk_edit.rhtml	(revision 1130)
@@ -0,0 +1,45 @@
+<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) %>: 
+<%= select_tag('status_id', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@available_statuses, :id, :name)) %></label>
+<% end %>
+<label><%= l(:field_priority) %>: 
+<%= select_tag('priority_id', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(Enumeration.get_values('IPRI'), :id, :name)) %></label>
+<label><%= l(:field_category) %>: 
+<%= select_tag('category_id', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@project.issue_categories, :id, :name)) %></label>
+</p>
+<p>
+<label><%= l(:field_assigned_to) %>: 
+<%= select_tag('assigned_to_id', content_tag('option', l(:label_no_change_option), :value => '') +
+                                 content_tag('option', l(:label_nobody), :value => 'none') +
+                                 options_from_collection_for_select(@project.assignable_users, :id, :name)) %></label>
+<label><%= l(:field_fixed_version) %>: 
+<%= select_tag('fixed_version_id', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@project.versions, :id, :name)) %></label>
+</p>
+
+<p>
+<label><%= l(:field_start_date) %>: 
+<%= text_field_tag 'start_date', '', :size => 10 %><%= calendar_for('start_date') %></label>
+<label><%= l(:field_due_date) %>: 
+<%= text_field_tag 'due_date', '', :size => 10 %><%= calendar_for('due_date') %></label>
+<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>
+</fieldset>
+
+<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 %>

Property changes on: trunk/app/views/issues/bulk_edit.rhtml
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: trunk/app/views/issues/context_menu.rhtml
===================================================================
--- trunk/app/views/issues/context_menu.rhtml	(revision 1129)
+++ trunk/app/views/issues/context_menu.rhtml	(revision 1130)
@@ -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>
Index: trunk/app/views/issues/index.rhtml
===================================================================
--- trunk/app/views/issues/index.rhtml	(revision 1129)
+++ trunk/app/views/issues/index.rhtml	(revision 1130)
@@ -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({})' %>
Index: trunk/app/views/issues/move.rhtml
===================================================================
--- trunk/app/views/issues/move.rhtml	(nonexistent)
+++ trunk/app/views/issues/move.rhtml	(revision 1130)
@@ -0,0 +1,22 @@
+<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({}, :id => 'move_form') do %>
+<%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join %>
+
+<div class="box tabular">
+<p><label for="new_project_id"><%=l(:field_project)%> :</label>
+<%= select_tag "new_project_id",
+               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>
+
+<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 %>

Property changes on: trunk/app/views/issues/move.rhtml
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: trunk/app/views/issues/show.rhtml
===================================================================
--- trunk/app/views/issues/show.rhtml	(revision 1129)
+++ trunk/app/views/issues/show.rhtml	(revision 1130)
@@ -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>
 
Index: trunk/app/views/my/page.rhtml
===================================================================
--- trunk/app/views/my/page.rhtml	(revision 1129)
+++ trunk/app/views/my/page.rhtml	(revision 1130)
@@ -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)) -%>
Index: trunk/app/views/projects/move_issues.rhtml
===================================================================
--- trunk/app/views/projects/move_issues.rhtml	(revision 1129)
+++ trunk/app/views/projects/move_issues.rhtml	(nonexistent)
@@ -1,29 +0,0 @@
-<h2><%=l(:button_move)%></h2>
-
-
-<% form_tag({:action => 'move_issues', :id => @project}, :class => 'tabular', :id => 'move_form') do %>
-
-<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]-->
-<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},
-                                            :method => :get,
-                                            :update => 'content',
-                                            :with => "Form.serialize('move_form')") %></p>
-
-<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 %>

Property changes on: trunk/app/views/projects/move_issues.rhtml
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: trunk/lang/bg.yml
===================================================================
--- trunk/lang/bg.yml	(revision 1129)
+++ trunk/lang/bg.yml	(revision 1130)
@@ -568,3 +568,4 @@
 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) ?'
Index: trunk/lang/cs.yml
===================================================================
--- trunk/lang/cs.yml	(revision 1129)
+++ trunk/lang/cs.yml	(revision 1130)
@@ -568,3 +568,4 @@
 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) ?'
Index: trunk/lang/de.yml
===================================================================
--- trunk/lang/de.yml	(revision 1129)
+++ trunk/lang/de.yml	(revision 1130)
@@ -568,3 +568,4 @@
 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) ?'
Index: trunk/lang/en.yml
===================================================================
--- trunk/lang/en.yml	(revision 1129)
+++ trunk/lang/en.yml	(revision 1130)
@@ -542,6 +542,7 @@
 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
Index: trunk/lang/es.yml
===================================================================
--- trunk/lang/es.yml	(revision 1129)
+++ trunk/lang/es.yml	(revision 1130)
@@ -571,3 +571,4 @@
 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) ?'
Index: trunk/lang/fi.yml
===================================================================
--- trunk/lang/fi.yml	(revision 1129)
+++ trunk/lang/fi.yml	(revision 1130)
@@ -572,3 +572,4 @@
 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) ?'
Index: trunk/lang/fr.yml
===================================================================
--- trunk/lang/fr.yml	(revision 1129)
+++ trunk/lang/fr.yml	(revision 1130)
@@ -543,6 +543,7 @@
 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
Index: trunk/lang/he.yml
===================================================================
--- trunk/lang/he.yml	(revision 1129)
+++ trunk/lang/he.yml	(revision 1130)
@@ -568,3 +568,4 @@
 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) ?'
Index: trunk/lang/it.yml
===================================================================
--- trunk/lang/it.yml	(revision 1129)
+++ trunk/lang/it.yml	(revision 1130)
@@ -568,3 +568,4 @@
 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) ?'
Index: trunk/lang/ja.yml
===================================================================
--- trunk/lang/ja.yml	(revision 1129)
+++ trunk/lang/ja.yml	(revision 1130)
@@ -569,3 +569,4 @@
 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) ?'
Index: trunk/lang/ko.yml
===================================================================
--- trunk/lang/ko.yml	(revision 1129)
+++ trunk/lang/ko.yml	(revision 1130)
@@ -568,3 +568,4 @@
 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) ?'
Index: trunk/lang/lt.yml
===================================================================
--- trunk/lang/lt.yml	(revision 1129)
+++ trunk/lang/lt.yml	(revision 1130)
@@ -569,3 +569,4 @@
 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) ?'
Index: trunk/lang/nl.yml
===================================================================
--- trunk/lang/nl.yml	(revision 1129)
+++ trunk/lang/nl.yml	(revision 1130)
@@ -569,3 +569,4 @@
 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) ?'
Index: trunk/lang/pl.yml
===================================================================
--- trunk/lang/pl.yml	(revision 1129)
+++ trunk/lang/pl.yml	(revision 1130)
@@ -568,3 +568,4 @@
 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) ?'
Index: trunk/lang/pt-br.yml
===================================================================
--- trunk/lang/pt-br.yml	(revision 1129)
+++ trunk/lang/pt-br.yml	(revision 1130)
@@ -568,3 +568,4 @@
 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) ?'
Index: trunk/lang/pt.yml
===================================================================
--- trunk/lang/pt.yml	(revision 1129)
+++ trunk/lang/pt.yml	(revision 1130)
@@ -568,3 +568,4 @@
 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) ?'
Index: trunk/lang/ro.yml
===================================================================
--- trunk/lang/ro.yml	(revision 1129)
+++ trunk/lang/ro.yml	(revision 1130)
@@ -568,3 +568,4 @@
 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) ?'
Index: trunk/lang/ru.yml
===================================================================
--- trunk/lang/ru.yml	(revision 1129)
+++ trunk/lang/ru.yml	(revision 1130)
@@ -569,3 +569,4 @@
 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) ?'
Index: trunk/lang/sr.yml
===================================================================
--- trunk/lang/sr.yml	(revision 1129)
+++ trunk/lang/sr.yml	(revision 1130)
@@ -569,3 +569,4 @@
 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) ?'
Index: trunk/lang/sv.yml
===================================================================
--- trunk/lang/sv.yml	(revision 1129)
+++ trunk/lang/sv.yml	(revision 1130)
@@ -569,3 +569,4 @@
 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) ?'
Index: trunk/lang/zh-tw.yml
===================================================================
--- trunk/lang/zh-tw.yml	(revision 1129)
+++ trunk/lang/zh-tw.yml	(revision 1130)
@@ -568,3 +568,4 @@
 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) ?'
Index: trunk/lang/zh.yml
===================================================================
--- trunk/lang/zh.yml	(revision 1129)
+++ trunk/lang/zh.yml	(revision 1130)
@@ -571,3 +571,4 @@
 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) ?'
Index: trunk/lib/redmine.rb
===================================================================
--- trunk/lib/redmine.rb	(revision 1129)
+++ trunk/lib/redmine.rb	(revision 1130)
@@ -31,11 +31,10 @@
                                   :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
Index: trunk/public/javascripts/application.js
===================================================================
--- trunk/public/javascripts/application.js	(revision 1129)
+++ trunk/public/javascripts/application.js	(revision 1130)
@@ -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 @@
     }
 }
 
-/* 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++) {
Index: trunk/public/javascripts/context_menu.js
===================================================================
--- trunk/public/javascripts/context_menu.js	(revision 1129)
+++ trunk/public/javascripts/context_menu.js	(revision 1130)
@@ -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');
+		}
 	}
 }
Index: trunk/test/functional/issues_controller_test.rb
===================================================================
--- trunk/test/functional/issues_controller_test.rb	(revision 1129)
+++ trunk/test/functional/issues_controller_test.rb	(revision 1130)
@@ -197,6 +197,28 @@
     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,14 +327,107 @@
     # 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, :id => 1
+    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, :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
     @request.session[:user_id] = 2
     post :destroy, :id => 1
Index: trunk/test/functional/projects_controller_test.rb
===================================================================
--- trunk/test/functional/projects_controller_test.rb	(revision 1129)
+++ trunk/test/functional/projects_controller_test.rb	(revision 1130)
@@ -93,32 +93,6 @@
     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
