]> source.dussan.org Git - redmine.git/commitdiff
Issue list now supports bulk edit/move/delete (#563, #607). For now, issues from...
authorJean-Philippe Lang <jp_lang@yahoo.fr>
Sun, 10 Feb 2008 13:17:41 +0000 (13:17 +0000)
committerJean-Philippe Lang <jp_lang@yahoo.fr>
Sun, 10 Feb 2008 13:17:41 +0000 (13:17 +0000)
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

39 files changed:
app/controllers/issues_controller.rb
app/controllers/projects_controller.rb
app/views/issues/_bulk_edit_form.rhtml [deleted file]
app/views/issues/_list.rhtml
app/views/issues/_list_simple.rhtml
app/views/issues/bulk_edit.rhtml [new file with mode: 0644]
app/views/issues/context_menu.rhtml
app/views/issues/index.rhtml
app/views/issues/move.rhtml [new file with mode: 0644]
app/views/issues/show.rhtml
app/views/my/page.rhtml
app/views/projects/move_issues.rhtml [deleted file]
lang/bg.yml
lang/cs.yml
lang/de.yml
lang/en.yml
lang/es.yml
lang/fi.yml
lang/fr.yml
lang/he.yml
lang/it.yml
lang/ja.yml
lang/ko.yml
lang/lt.yml
lang/nl.yml
lang/pl.yml
lang/pt-br.yml
lang/pt.yml
lang/ro.yml
lang/ru.yml
lang/sr.yml
lang/sv.yml
lang/zh-tw.yml
lang/zh.yml
lib/redmine.rb
public/javascripts/application.js
public/javascripts/context_menu.js
test/functional/issues_controller_test.rb
test/functional/projects_controller_test.rb

index dbb49405c54c8c2ead91372ed1675d520be5b3dc..85151e9055499caf29a53268b9f0a69f9d2faff5 100644 (file)
@@ -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
index 9560a451fe620f8daf594c3e2a8495091f7270de..cddfb6f8187df1bd468ccdc2c8a0941aef72bb90 100644 (file)
@@ -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/_bulk_edit_form.rhtml b/app/views/issues/_bulk_edit_form.rhtml
deleted file mode 100644 (file)
index e9e1cef..0000000
+++ /dev/null
@@ -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>
index ff91f34f71e3be102c0244e9fae99f8771010e61..1f54702004e45f91655a0f74fb4feb12a83f42e9 100644 (file)
@@ -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| %>
         <% 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 eb93f8ea1eb59f44ab9d7fc7c82ab47c2df9a1e1..8900b7359ee80861cf92b8070063b2990aaac265 100644 (file)
@@ -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.rhtml b/app/views/issues/bulk_edit.rhtml
new file mode 100644 (file)
index 0000000..d192622
--- /dev/null
@@ -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 %>
index 46b177067d5a2dfd8143cdf7204ea1090cb23e3a..b3a03b05d47e01040384bf7c278e1f1389249de1 100644 (file)
@@ -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 48697c505070325f92d98a7230ec8e54ac3123fa..c5f26dfb6f202be1b1cc9252106130a420e0bd77 100644 (file)
@@ -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' %>
 <% 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/issues/move.rhtml b/app/views/issues/move.rhtml
new file mode 100644 (file)
index 0000000..c74270f
--- /dev/null
@@ -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 %>
index a16dc60e033d6c6a6586473518988e297e526813..3392aef832778b87fa5e91748c29dfe275337e55 100644 (file)
@@ -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 26ee44fcc9cef210ba4a6712051cd719dd143fe7..4d4c921b6d3cf090f86da486781f4367ce051a1c 100644 (file)
@@ -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/app/views/projects/move_issues.rhtml b/app/views/projects/move_issues.rhtml
deleted file mode 100644 (file)
index 95eaf9d..0000000
+++ /dev/null
@@ -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 %>
index bc0e070c0aaa9db566c52b55e9a09ca720cdc000..164d676716cd0fab99830c22e97033e23d8a3212 100644 (file)
@@ -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) ?'
index 35bda2b6a8097c651971a43a574ddd275b5d4f72..86a111cc77957588e72eb0ecdd3f2478323b6185 100644 (file)
@@ -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) ?'
index fd296b6006eddf9131007e84f64aad070078f12b..c5a6cdf9579236adb5d1ac8c819422a2a55e879a 100644 (file)
@@ -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) ?'
index b86eeef2bdaec11e3605cbea8ab3141b5dbbf511..a37be93c7db519e872ffd09cdf5cd7cd0c4dce3b 100644 (file)
@@ -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
index 60e4a4d13d9c1c05aa0d58a485cf0038580260f7..1bc7d00fbda08f4d5e637ff97f513e628ce24eb8 100644 (file)
@@ -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) ?'
index 38cf19a4638159c357c029fdc58c8cad868d4d82..7a8848302a5db2eb35d6130a2c8d8f93d5452210 100644 (file)
@@ -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) ?'
index e428935afa0e44127d817457e5d6a0cdb3cd9096..8c569fc5acc26eae4f3e98b607d1731d66d174d9 100644 (file)
@@ -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
index ca577558a6db0a2a599582fa7846c205dfd89571..6631a457f983c608714cd5bcdf4bafed496a1726 100644 (file)
@@ -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) ?'
index f227c694205f59fa6292d19d2b45d47fcdbd0e74..3c77c97a62407af90022791dac533c03cd941b4b 100644 (file)
@@ -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) ?'
index 92b79b6efd3cb2ac6b07e833b9c29e18b8014d5b..50d23e80f5bf75109ea397e689ef677464293d5e 100644 (file)
@@ -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) ?'
index 097d7478803f2c52f46748d80b99104b3ce54142..62ec95ef34a98f372f3bd74f73b5f8750768509b 100644 (file)
@@ -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) ?'
index 3106bd764c7aaa540989329f11eff6c413d31679..a7b12736a7e67debce30612e925741fc545af0c9 100644 (file)
@@ -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) ?'
index 1c76180be7391d7489cf7375ea89c1be07f63a2b..7b5f6bf3a776c0ad290836335c0a584b27a7c6c6 100644 (file)
@@ -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) ?'
index 3ebce3e92732a00376bd98437a6243059dffa61c..5c12c658fd554f3db855f259fa74f46da2626ab2 100644 (file)
@@ -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) ?'
index 688ce0cfd1fcb9be6f13ea8c5782a38baa478c37..4616e83a7dbfa04caacae28f3330f706f77cac69 100644 (file)
@@ -568,3 +568,4 @@ label_associated_revisions: Associated revisions
 setting_user_format: Users display format\r
 text_status_changed_by_changeset: Applied in changeset %s.\r
 label_more: More\r
+text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'\r
index 4734c66fc1c40ddbbc18d098dd9cf175a4def046..31df179f2f43504878e8a31a8f7900905c8ae6e8 100644 (file)
@@ -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) ?'
index be566b9596f2ee3f65a942e4f8d46ba73a248d71..52e856683672722cfac684d253cc58e5c65bebe0 100644 (file)
@@ -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) ?'
index 6921169ce7a2f257f32fd4838a00df12df475d4b..c720dfb722200ee35155ee505653b2f58e51fe51 100644 (file)
@@ -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) ?'
index ddf84a5d37fa31879ec7d4896c2da0ce7a8f204a..c32394dc3598ba99c7947259891a6bcf96d51178 100644 (file)
@@ -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) ?'
index 4bec394ce6373735dc0733b71a6d5cb441303d98..40a1ce8f668b341c87b02230031efb777a8beaa1 100644 (file)
@@ -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) ?'
index 50a6384f2646e9dcc7925c46001a9404d6c1b541..601f5c2ec5130a1ad08e98f6eb4c801cc1191357 100644 (file)
@@ -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) ?'
index dc431f5c076788307abe9755f6c801eb7ccd2269..fd513a96a4668478d3c0eb827f1887ec010e8697 100644 (file)
@@ -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) ?'
index 69151012bfa9afa91798c453e2aac55eec09991c..9bec55409e9edb162896dcb50b90948ef2a98980 100644 (file)
@@ -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
index 5ad04e91d5aa5e2586f5abd735c37b600c2768b2..d77362a060e06c565f4a8f98e59effc71c17d896 100644 (file)
@@ -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++) {
index 11754cde840937f5a0565a1f5c84b4ce5e300e19..e3f128d8906758e039f15ec59862e4820f32f491 100644 (file)
+/* 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 6fdbb0341754f34577156b697c0ea246da3be7c6..4f28ab22452e491262ff65da173bbdc6e1c4c4e9 100644 (file)
@@ -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
index 71a3e3dfe2fc655f03c0a887651ba6d22d958e2a..92ac6f09a03235f2f5c3896b8837e9808faca7f9 100644 (file)
@@ -93,32 +93,6 @@ class ProjectsControllerTest < Test::Unit::TestCase
     assert_nil Project.find_by_id(1)
   end
   \r
-  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\r
-
-  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
-  \r
   def test_list_files\r
     get :list_files, :id => 1\r
     assert_response :success\r