##// END OF EJS Templates
Issue list now supports bulk edit/move/delete (#563, #607). For now, issues from different projects can not be bulk edited/moved/deleted at once....
Jean-Philippe Lang -
r1116:4155c97222ce
parent child
Show More
@@ -19,13 +19,14 class IssuesController < ApplicationController
19 layout 'base'
19 layout 'base'
20 menu_item :new_issue, :only => :new
20 menu_item :new_issue, :only => :new
21
21
22 before_filter :find_issue, :except => [:index, :changes, :preview, :new, :update_form]
22 before_filter :find_issue, :only => [:show, :edit, :destroy_attachment]
23 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
23 before_filter :find_project, :only => [:new, :update_form]
24 before_filter :find_project, :only => [:new, :update_form]
24 before_filter :authorize, :except => [:index, :changes, :preview, :update_form]
25 before_filter :authorize, :except => [:index, :changes, :preview, :update_form, :context_menu]
25 before_filter :find_optional_project, :only => [:index, :changes]
26 before_filter :find_optional_project, :only => [:index, :changes]
26 accept_key_auth :index, :changes
27 accept_key_auth :index, :changes
27
28
28 cache_sweeper :issue_sweeper, :only => [ :new, :edit, :destroy ]
29 cache_sweeper :issue_sweeper, :only => [ :new, :edit, :bulk_edit, :destroy ]
29
30
30 helper :journals
31 helper :journals
31 helper :projects
32 helper :projects
@@ -152,18 +153,20 class IssuesController < ApplicationController
152 @priorities = Enumeration::get_values('IPRI')
153 @priorities = Enumeration::get_values('IPRI')
153 @custom_values = []
154 @custom_values = []
154 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
155 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
156
157 @notes = params[:notes]
158 journal = @issue.init_journal(User.current, @notes)
159 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
160 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
161 attrs = params[:issue].dup
162 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
163 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
164 @issue.attributes = attrs
165 end
166
155 if request.get?
167 if request.get?
156 @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) }
168 @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) }
157 else
169 else
158 @notes = params[:notes]
159 journal = @issue.init_journal(User.current, @notes)
160 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
161 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
162 attrs = params[:issue].dup
163 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
164 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
165 @issue.attributes = attrs
166 end
167 # Update custom fields if user has :edit permission
170 # Update custom fields if user has :edit permission
168 if @edit_allowed && params[:custom_fields]
171 if @edit_allowed && params[:custom_fields]
169 @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]) }
172 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) }
@@ -191,8 +194,78 class IssuesController < ApplicationController
191 flash.now[:error] = l(:notice_locking_conflict)
194 flash.now[:error] = l(:notice_locking_conflict)
192 end
195 end
193
196
197 # Bulk edit a set of issues
198 def bulk_edit
199 if request.post?
200 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
201 priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
202 assigned_to = params[:assigned_to_id].blank? ? nil : User.find_by_id(params[:assigned_to_id])
203 category = params[:category_id].blank? ? nil : @project.issue_categories.find_by_id(params[:category_id])
204 fixed_version = params[:fixed_version_id].blank? ? nil : @project.versions.find_by_id(params[:fixed_version_id])
205
206 unsaved_issue_ids = []
207 @issues.each do |issue|
208 journal = issue.init_journal(User.current, params[:notes])
209 issue.priority = priority if priority
210 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
211 issue.category = category if category
212 issue.fixed_version = fixed_version if fixed_version
213 issue.start_date = params[:start_date] unless params[:start_date].blank?
214 issue.due_date = params[:due_date] unless params[:due_date].blank?
215 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
216 # Don't save any change to the issue if the user is not authorized to apply the requested status
217 if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
218 # Send notification for each issue (if changed)
219 Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
220 else
221 # Keep unsaved issue ids to display them in flash error
222 unsaved_issue_ids << issue.id
223 end
224 end
225 if unsaved_issue_ids.empty?
226 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
227 else
228 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
229 end
230 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
231 return
232 end
233 # Find potential statuses the user could be allowed to switch issues to
234 @available_statuses = Workflow.find(:all, :include => :new_status,
235 :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq
236 end
237
238 def move
239 @allowed_projects = []
240 # find projects to which the user is allowed to move the issue
241 if User.current.admin?
242 # admin is allowed to move issues to any active (visible) project
243 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name')
244 else
245 User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)}
246 end
247 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
248 @target_project ||= @project
249 @trackers = @target_project.trackers
250 if request.post?
251 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
252 unsaved_issue_ids = []
253 @issues.each do |issue|
254 unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker)
255 end
256 if unsaved_issue_ids.empty?
257 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
258 else
259 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
260 end
261 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
262 return
263 end
264 render :layout => false if request.xhr?
265 end
266
194 def destroy
267 def destroy
195 @issue.destroy
268 @issues.each(&:destroy)
196 redirect_to :action => 'index', :project_id => @project
269 redirect_to :action => 'index', :project_id => @project
197 end
270 end
198
271
@@ -208,17 +281,27 class IssuesController < ApplicationController
208 end
281 end
209
282
210 def context_menu
283 def context_menu
284 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
285 if (@issues.size == 1)
286 @issue = @issues.first
287 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
288 @assignables = @issue.assignable_users
289 @assignables << @issue.assigned_to if @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
290 end
291 projects = @issues.collect(&:project).compact.uniq
292 @project = projects.first if projects.size == 1
293
294 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
295 :update => (@issue && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && !@allowed_statuses.empty?))),
296 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
297 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
298 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
299 }
300
211 @priorities = Enumeration.get_values('IPRI').reverse
301 @priorities = Enumeration.get_values('IPRI').reverse
212 @statuses = IssueStatus.find(:all, :order => 'position')
302 @statuses = IssueStatus.find(:all, :order => 'position')
213 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
303 @back = request.env['HTTP_REFERER']
214 @assignables = @issue.assignable_users
304
215 @assignables << @issue.assigned_to if @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
216 @can = {:edit => User.current.allowed_to?(:edit_issues, @project),
217 :assign => (@allowed_statuses.any? || User.current.allowed_to?(:edit_issues, @project)),
218 :add => User.current.allowed_to?(:add_issues, @project),
219 :move => User.current.allowed_to?(:move_issues, @project),
220 :copy => (@project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
221 :delete => User.current.allowed_to?(:delete_issues, @project)}
222 render :layout => false
305 render :layout => false
223 end
306 end
224
307
@@ -242,6 +325,21 private
242 render_404
325 render_404
243 end
326 end
244
327
328 # Filter for bulk operations
329 def find_issues
330 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
331 raise ActiveRecord::RecordNotFound if @issues.empty?
332 projects = @issues.collect(&:project).compact.uniq
333 if projects.size == 1
334 @project = projects.first
335 else
336 # TODO: let users bulk edit/move/destroy issues from different projects
337 render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
338 end
339 rescue ActiveRecord::RecordNotFound
340 render_404
341 end
342
245 def find_project
343 def find_project
246 @project = Project.find(params[:project_id])
344 @project = Project.find(params[:project_id])
247 rescue ActiveRecord::RecordNotFound
345 rescue ActiveRecord::RecordNotFound
@@ -22,7 +22,7 class ProjectsController < ApplicationController
22 menu_item :roadmap, :only => :roadmap
22 menu_item :roadmap, :only => :roadmap
23 menu_item :files, :only => [:list_files, :add_file]
23 menu_item :files, :only => [:list_files, :add_file]
24 menu_item :settings, :only => :settings
24 menu_item :settings, :only => :settings
25 menu_item :issues, :only => [:bulk_edit_issues, :changelog, :move_issues]
25 menu_item :issues, :only => [:changelog]
26
26
27 before_filter :find_project, :except => [ :index, :list, :add ]
27 before_filter :find_project, :except => [ :index, :list, :add ]
28 before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy ]
28 before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy ]
@@ -182,83 +182,6 class ProjectsController < ApplicationController
182 end
182 end
183 end
183 end
184
184
185 # Bulk edit issues
186 def bulk_edit_issues
187 if request.post?
188 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
189 priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
190 assigned_to = params[:assigned_to_id].blank? ? nil : User.find_by_id(params[:assigned_to_id])
191 category = params[:category_id].blank? ? nil : @project.issue_categories.find_by_id(params[:category_id])
192 fixed_version = params[:fixed_version_id].blank? ? nil : @project.versions.find_by_id(params[:fixed_version_id])
193 issues = @project.issues.find_all_by_id(params[:issue_ids])
194 unsaved_issue_ids = []
195 issues.each do |issue|
196 journal = issue.init_journal(User.current, params[:notes])
197 issue.priority = priority if priority
198 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
199 issue.category = category if category
200 issue.fixed_version = fixed_version if fixed_version
201 issue.start_date = params[:start_date] unless params[:start_date].blank?
202 issue.due_date = params[:due_date] unless params[:due_date].blank?
203 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
204 # Don't save any change to the issue if the user is not authorized to apply the requested status
205 if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
206 # Send notification for each issue (if changed)
207 Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
208 else
209 # Keep unsaved issue ids to display them in flash error
210 unsaved_issue_ids << issue.id
211 end
212 end
213 if unsaved_issue_ids.empty?
214 flash[:notice] = l(:notice_successful_update) unless issues.empty?
215 else
216 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, issues.size, '#' + unsaved_issue_ids.join(', #'))
217 end
218 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
219 return
220 end
221 # Find potential statuses the user could be allowed to switch issues to
222 @available_statuses = Workflow.find(:all, :include => :new_status,
223 :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq
224 render :update do |page|
225 page.hide 'query_form'
226 page.replace_html 'bulk-edit', :partial => 'issues/bulk_edit_form'
227 end
228 end
229
230 def move_issues
231 @issues = @project.issues.find(params[:issue_ids]) if params[:issue_ids]
232 redirect_to :controller => 'issues', :action => 'index', :project_id => @project and return unless @issues
233
234 @projects = []
235 # find projects to which the user is allowed to move the issue
236 if User.current.admin?
237 # admin is allowed to move issues to any active (visible) project
238 @projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name')
239 else
240 User.current.memberships.each {|m| @projects << m.project if m.role.allowed_to?(:move_issues)}
241 end
242 @target_project = @projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
243 @target_project ||= @project
244 @trackers = @target_project.trackers
245 if request.post?
246 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
247 unsaved_issue_ids = []
248 @issues.each do |issue|
249 unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker)
250 end
251 if unsaved_issue_ids.empty?
252 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
253 else
254 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
255 end
256 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
257 return
258 end
259 render :layout => false if request.xhr?
260 end
261
262 def add_file
185 def add_file
263 if request.post?
186 if request.post?
264 @version = @project.versions.find_by_id(params[:version_id])
187 @version = @project.versions.find_by_id(params[:version_id])
@@ -1,10 +1,7
1 <div id="bulk-edit"></div>
1 <% form_tag({}) do -%>
2 <table class="list">
2 <table class="list issues">
3 <thead><tr>
3 <thead><tr>
4 <th><%= link_to_remote(image_tag('edit.png'),
4 <th><%= link_to image_tag('edit.png'), {}, :onclick => 'toggleIssuesSelection(this.up("form")); return false;' %>
5 {:url => { :controller => 'projects', :action => 'bulk_edit_issues', :id => @project },
6 :method => :get},
7 {:title => l(:label_bulk_edit_selected_issues)}) if @project && User.current.allowed_to?(:edit_issues, @project) %>
8 </th>
5 </th>
9 <%= sort_header_tag("#{Issue.table_name}.id", :caption => '#', :default_order => 'desc') %>
6 <%= sort_header_tag("#{Issue.table_name}.id", :caption => '#', :default_order => 'desc') %>
10 <% query.columns.each do |column| %>
7 <% query.columns.each do |column| %>
@@ -12,14 +9,21
12 <% end %>
9 <% end %>
13 </tr></thead>
10 </tr></thead>
14 <tbody>
11 <tbody>
15 <% issues.each do |issue| %>
12 <% issues.each do |issue| -%>
16 <tr id="issue-<%= issue.id %>" class="issue hascontextmenu <%= cycle('odd', 'even') %> <%= "status-#{issue.status.position} priority-#{issue.priority.position}" %>">
13 <tr id="issue-<%= issue.id %>" class="issue hascontextmenu <%= cycle('odd', 'even') %> <%= "status-#{issue.status.position} priority-#{issue.priority.position}" %>">
17 <td class="checkbox"><%= check_box_tag("issue_ids[]", issue.id, false, :id => "issue_#{issue.id}", :disabled => (!@project || @project != issue.project)) %></td>
14 <td class="checkbox"><%= check_box_tag("ids[]", issue.id, false) %></td>
18 <td><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td>
15 <td><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td>
19 <% query.columns.each do |column| %>
16 <% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.name %><% end %>
20 <%= content_tag 'td', column_content(column, issue), :class => column.name %>
21 <% end %>
22 </tr>
17 </tr>
23 <% end %>
18 <% end -%>
24 </tbody>
19 </tbody>
25 </table>
20 </table>
21 <% end -%>
22
23 <% content_for :header_tags do -%>
24 <%= javascript_include_tag 'context_menu' %>
25 <%= stylesheet_link_tag 'context_menu' %>
26 <% end -%>
27
28 <div id="context-menu" style="display: none;"></div>
29 <%= javascript_tag "new ContextMenu('#{url_for(:controller => 'issues', :action => 'context_menu')}')" %>
@@ -1,5 +1,6
1 <% if issues.length > 0 %>
1 <% if issues && issues.any? %>
2 <table class="list">
2 <% form_tag({}) do %>
3 <table class="list issues">
3 <thead><tr>
4 <thead><tr>
4 <th>#</th>
5 <th>#</th>
5 <th><%=l(:field_tracker)%></th>
6 <th><%=l(:field_tracker)%></th>
@@ -9,6 +10,7
9 <% for issue in issues %>
10 <% for issue in issues %>
10 <tr id="issue-<%= issue.id %>" class="issue hascontextmenu <%= cycle('odd', 'even') %> <%= "status-#{issue.status.position} priority-#{issue.priority.position}" %>">
11 <tr id="issue-<%= issue.id %>" class="issue hascontextmenu <%= cycle('odd', 'even') %> <%= "status-#{issue.status.position} priority-#{issue.priority.position}" %>">
11 <td class="id">
12 <td class="id">
13 <%= check_box_tag("ids[]", issue.id, false, :style => 'display:none;') %>
12 <%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %>
14 <%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %>
13 </td>
15 </td>
14 <td><%=h issue.project.name %> - <%= issue.tracker.name %><br />
16 <td><%=h issue.project.name %> - <%= issue.tracker.name %><br />
@@ -20,6 +22,7
20 <% end %>
22 <% end %>
21 </tbody>
23 </tbody>
22 </table>
24 </table>
25 <% end %>
23 <% else %>
26 <% else %>
24 <i><%=l(:label_no_data)%></i>
27 <p class="nodata"><%= l(:label_no_data) %></p>
25 <% end %> No newline at end of file
28 <% end %>
@@ -1,6 +1,12
1 <div id="bulk-edit-fields">
1 <h2><%= l(:label_bulk_edit_selected_issues) %></h2>
2 <fieldset class="box"><legend><%= l(:label_bulk_edit_selected_issues) %></legend>
3
2
3 <ul><%= @issues.collect {|i| content_tag('li', link_to(h("#{i.tracker} ##{i.id}"), { :action => 'show', :id => i }) + h(": #{i.subject}")) }.join("\n") %></ul>
4
5 <% form_tag() do %>
6 <%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join %>
7 <div class="box">
8 <fieldset>
9 <legend><%= l(:label_change_properties) %></legend>
4 <p>
10 <p>
5 <% if @available_statuses.any? %>
11 <% if @available_statuses.any? %>
6 <label><%= l(:field_status) %>:
12 <label><%= l(:field_status) %>:
@@ -28,11 +34,12
28 <label><%= l(:field_done_ratio) %>:
34 <label><%= l(:field_done_ratio) %>:
29 <%= select_tag 'done_ratio', options_for_select([[l(:label_no_change_option), '']] + (0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></label>
35 <%= select_tag 'done_ratio', options_for_select([[l(:label_no_change_option), '']] + (0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></label>
30 </p>
36 </p>
31
32 <label for="notes"><%= l(:field_notes) %></label><br />
33 <%= text_area_tag 'notes', '', :cols => 80, :rows => 5 %>
34
35 </fieldset>
37 </fieldset>
36 <p><%= submit_tag l(:button_apply) %>
38
37 <%= link_to l(:button_cancel), {}, :onclick => 'Element.hide("bulk-edit-fields"); if ($("query_form")) {Element.show("query_form")}; return false;' %></p>
39 <fieldset><legend><%= l(:field_notes) %></legend>
40 <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
41 <%= wikitoolbar_for 'notes' %>
38 </div>
42 </div>
43
44 <p><%= submit_tag l(:button_submit) %>
45 <% end %>
@@ -1,40 +1,45
1 <% back_to = url_for(:controller => 'issues', :action => 'index', :project_id => @project) %>
2 <ul>
1 <ul>
2 <% if !@issue.nil? -%>
3 <li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'edit', :id => @issue},
3 <li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'edit', :id => @issue},
4 :class => 'icon-edit', :disabled => !@can[:edit] %></li>
4 :class => 'icon-edit', :disabled => !@can[:edit] %></li>
5 <li class="folder">
5 <li class="folder">
6 <a href="#" class="submenu" onclick="return false;"><%= l(:field_status) %></a>
6 <a href="#" class="submenu" onclick="return false;"><%= l(:field_status) %></a>
7 <ul>
7 <ul>
8 <% @statuses.each do |s| %>
8 <% @statuses.each do |s| -%>
9 <li><%= context_menu_link s.name, {:controller => 'issues', :action => 'edit', :id => @issue, :issue => {:status_id => s}},
9 <li><%= context_menu_link s.name, {:controller => 'issues', :action => 'edit', :id => @issue, :issue => {:status_id => s}},
10 :selected => (s == @issue.status), :disabled => !(@allowed_statuses.include?(s)) %></li>
10 :selected => (s == @issue.status), :disabled => !(@can[:update] && @allowed_statuses.include?(s)) %></li>
11 <% end %>
11 <% end -%>
12 </ul>
12 </ul>
13 </li>
13 </li>
14 <li class="folder">
14 <li class="folder">
15 <a href="#" class="submenu"><%= l(:field_priority) %></a>
15 <a href="#" class="submenu"><%= l(:field_priority) %></a>
16 <ul>
16 <ul>
17 <% @priorities.each do |p| %>
17 <% @priorities.each do |p| -%>
18 <li><%= context_menu_link p.name, {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[priority_id]' => p, :back_to => back_to}, :method => :post,
18 <li><%= context_menu_link p.name, {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[priority_id]' => p, :back_to => @back}, :method => :post,
19 :selected => (p == @issue.priority), :disabled => !@can[:edit] %></li>
19 :selected => (p == @issue.priority), :disabled => !@can[:edit] %></li>
20 <% end %>
20 <% end -%>
21 </ul>
21 </ul>
22 </li>
22 </li>
23 <li class="folder">
23 <li class="folder">
24 <a href="#" class="submenu"><%= l(:field_assigned_to) %></a>
24 <a href="#" class="submenu"><%= l(:field_assigned_to) %></a>
25 <ul>
25 <ul>
26 <% @assignables.each do |u| %>
26 <% @assignables.each do |u| -%>
27 <li><%= context_menu_link u.name, {:controller => 'issues', :action => 'edit', :id => @issue, :issue => {:assigned_to_id => u}, :back_to => back_to}, :method => :post,
27 <li><%= context_menu_link u.name, {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[assigned_to_id]' => u, :back_to => @back}, :method => :post,
28 :selected => (u == @issue.assigned_to), :disabled => !@can[:assign] %></li>
28 :selected => (u == @issue.assigned_to), :disabled => !@can[:update] %></li>
29 <% end %>
29 <% end -%>
30 <li><%= context_menu_link l(:label_nobody), {:controller => 'issues', :action => 'edit', :id => @issue, :issue => {:assigned_to_id => nil}, :back_to => back_to}, :method => :post,
30 <li><%= context_menu_link l(:label_nobody), {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[assigned_to_id]' => '', :back_to => @back}, :method => :post,
31 :selected => @issue.assigned_to.nil?, :disabled => !@can[:assign] %></li>
31 :selected => @issue.assigned_to.nil?, :disabled => !@can[:update] %></li>
32 </ul>
32 </ul>
33 </li>
33 </li>
34 <li><%= context_menu_link l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue},
34 <li><%= context_menu_link l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue},
35 :class => 'icon-copy', :disabled => !@can[:copy] %></li>
35 :class => 'icon-copy', :disabled => !@can[:copy] %></li>
36 <li><%= context_menu_link l(:button_move), {:controller => 'projects', :action => 'move_issues', :id => @project, "issue_ids[]" => @issue.id },
36 <% else -%>
37 :class => 'icon-move', :disabled => !@can[:move] %>
37 <li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id)},
38 <li><%= context_menu_link l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue},
38 :class => 'icon-edit', :disabled => !@can[:edit] %></li>
39 :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon-del', :disabled => !@can[:delete] %></li>
39 <% end -%>
40
41 <li><%= context_menu_link l(:button_move), {:controller => 'issues', :action => 'move', :ids => @issues.collect(&:id)},
42 :class => 'icon-move', :disabled => !@can[:move] %></li>
43 <li><%= context_menu_link l(:button_delete), {:controller => 'issues', :action => 'destroy', :ids => @issues.collect(&:id)},
44 :method => :post, :confirm => l(:text_issues_destroy_confirmation), :class => 'icon-del', :disabled => !@can[:delete] %></li>
40 </ul>
45 </ul>
@@ -31,7 +31,6
31 <%= link_to l(:button_delete), {:controller => 'queries', :action => 'destroy', :id => @query}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
31 <%= link_to l(:button_delete), {:controller => 'queries', :action => 'destroy', :id => @query}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
32 <% end %>
32 <% end %>
33 </div>
33 </div>
34
35 <h2><%=h @query.name %></h2>
34 <h2><%=h @query.name %></h2>
36 <div id="query_form"></div>
35 <div id="query_form"></div>
37 <% html_title @query.name %>
36 <% html_title @query.name %>
@@ -41,7 +40,6
41 <% if @issues.empty? %>
40 <% if @issues.empty? %>
42 <p class="nodata"><%= l(:label_no_data) %></p>
41 <p class="nodata"><%= l(:label_no_data) %></p>
43 <% else %>
42 <% else %>
44 <% 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 %>
45 <%= render :partial => 'issues/list', :locals => {:issues => @issues, :query => @query} %>
43 <%= render :partial => 'issues/list', :locals => {:issues => @issues, :query => @query} %>
46 <div class="contextual">
44 <div class="contextual">
47 <%= l(:label_export_to) %>
45 <%= l(:label_export_to) %>
@@ -51,7 +49,6
51 <p class="pagination"><%= pagination_links_full @issue_pages, @issue_count %></p>
49 <p class="pagination"><%= pagination_links_full @issue_pages, @issue_count %></p>
52 <% end %>
50 <% end %>
53 <% end %>
51 <% end %>
54 <% end %>
55
52
56 <% content_for :sidebar do %>
53 <% content_for :sidebar do %>
57 <%= render :partial => 'issues/sidebar' %>
54 <%= render :partial => 'issues/sidebar' %>
@@ -60,13 +57,4
60 <% content_for :header_tags do %>
57 <% content_for :header_tags do %>
61 <%= auto_discovery_link_tag(:atom, {:query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_issue_plural)) %>
58 <%= auto_discovery_link_tag(:atom, {:query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_issue_plural)) %>
62 <%= auto_discovery_link_tag(:atom, {:action => 'changes', :query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_changes_details)) %>
59 <%= auto_discovery_link_tag(:atom, {:action => 'changes', :query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_changes_details)) %>
63 <%= javascript_include_tag 'calendar/calendar' %>
64 <%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %>
65 <%= javascript_include_tag 'calendar/calendar-setup' %>
66 <%= stylesheet_link_tag 'calendar' %>
67 <%= javascript_include_tag 'context_menu' %>
68 <%= stylesheet_link_tag 'context_menu' %>
69 <% end %>
60 <% end %>
70
71 <div id="context-menu" style="display: none;"></div>
72 <%= javascript_tag 'new ContextMenu({})' %>
@@ -1,23 +1,15
1 <h2><%=l(:button_move)%></h2>
1 <h2><%= l(:button_move) %></h2>
2
2
3 <ul><%= @issues.collect {|i| content_tag('li', link_to(h("#{i.tracker} ##{i.id}"), { :action => 'show', :id => i }) + h(": #{i.subject}")) }.join("\n") %></ul>
3
4
4 <% form_tag({:action => 'move_issues', :id => @project}, :class => 'tabular', :id => 'move_form') do %>
5 <% form_tag({}, :id => 'move_form') do %>
6 <%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join %>
5
7
6 <div class="box">
8 <div class="box tabular">
7 <p><label><%= l(:label_issue_plural) %> :</label>
8 <% for issue in @issues %>
9 <%= link_to_issue issue %>: <%=h issue.subject %>
10 <%= hidden_field_tag "issue_ids[]", issue.id %><br />
11 <% end %>
12 <i>(<%= @issues.length%> <%= lwr(:label_issue, @issues.length)%>)</i></p>
13
14 &nbsp;
15
16 <!--[form:issue]-->
17 <p><label for="new_project_id"><%=l(:field_project)%> :</label>
9 <p><label for="new_project_id"><%=l(:field_project)%> :</label>
18 <%= select_tag "new_project_id",
10 <%= select_tag "new_project_id",
19 options_from_collection_for_select(@projects, 'id', 'name', @target_project.id),
11 options_from_collection_for_select(@allowed_projects, 'id', 'name', @target_project.id),
20 :onchange => remote_function(:url => {:action => 'move_issues' , :id => @project},
12 :onchange => remote_function(:url => {:action => 'move' , :id => @project},
21 :method => :get,
13 :method => :get,
22 :update => 'content',
14 :update => 'content',
23 :with => "Form.serialize('move_form')") %></p>
15 :with => "Form.serialize('move_form')") %></p>
@@ -25,5 +17,6
25 <p><label for="new_tracker_id"><%=l(:field_tracker)%> :</label>
17 <p><label for="new_tracker_id"><%=l(:field_tracker)%> :</label>
26 <%= select_tag "new_tracker_id", "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@trackers, "id", "name") %></p>
18 <%= select_tag "new_tracker_id", "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@trackers, "id", "name") %></p>
27 </div>
19 </div>
20
28 <%= submit_tag l(:button_move) %>
21 <%= submit_tag l(:button_move) %>
29 <% end %>
22 <% end %>
@@ -3,7 +3,7
3 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, :class => 'icon icon-time' %>
3 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, :class => 'icon icon-time' %>
4 <%= watcher_tag(@issue, User.current) %>
4 <%= watcher_tag(@issue, User.current) %>
5 <%= link_to_if_authorized l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue }, :class => 'icon icon-copy' %>
5 <%= link_to_if_authorized l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue }, :class => 'icon icon-copy' %>
6 <%= link_to_if_authorized l(:button_move), {:controller => 'projects', :action => 'move_issues', :id => @project, "issue_ids[]" => @issue.id }, :class => 'icon icon-move' %>
6 <%= link_to_if_authorized l(:button_move), {:controller => 'issues', :action => 'move', :id => @issue }, :class => 'icon icon-move' %>
7 <%= 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' %>
7 <%= 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' %>
8 </div>
8 </div>
9
9
@@ -37,6 +37,6
37 <% end %>
37 <% end %>
38
38
39 <div id="context-menu" style="display: none;"></div>
39 <div id="context-menu" style="display: none;"></div>
40 <%= javascript_tag 'new ContextMenu({})' %>
40 <%= javascript_tag "new ContextMenu('#{url_for(:controller => 'issues', :action => 'context_menu')}')" %>
41
41
42 <% html_title(l(:label_my_page)) -%>
42 <% html_title(l(:label_my_page)) -%>
@@ -568,3 +568,4 label_associated_revisions: Асоциирани ревизии
568 setting_user_format: Потребителски формат
568 setting_user_format: Потребителски формат
569 text_status_changed_by_changeset: Applied in changeset %s.
569 text_status_changed_by_changeset: Applied in changeset %s.
570 label_more: More
570 label_more: More
571 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
@@ -568,3 +568,4 label_associated_revisions: Associated revisions
568 setting_user_format: Users display format
568 setting_user_format: Users display format
569 text_status_changed_by_changeset: Applied in changeset %s.
569 text_status_changed_by_changeset: Applied in changeset %s.
570 label_more: More
570 label_more: More
571 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
@@ -568,3 +568,4 enumeration_doc_categories: Dokumentenkategorien
568 enumeration_activities: Aktivitäten (Zeiterfassung)
568 enumeration_activities: Aktivitäten (Zeiterfassung)
569 text_status_changed_by_changeset: Applied in changeset %s.
569 text_status_changed_by_changeset: Applied in changeset %s.
570 label_more: More
570 label_more: More
571 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
@@ -542,6 +542,7 text_user_mail_option: "For unselected projects, you will only receive notificat
542 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."
542 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."
543 text_load_default_configuration: Load the default configuration
543 text_load_default_configuration: Load the default configuration
544 text_status_changed_by_changeset: Applied in changeset %s.
544 text_status_changed_by_changeset: Applied in changeset %s.
545 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
545
546
546 default_role_manager: Manager
547 default_role_manager: Manager
547 default_role_developper: Developer
548 default_role_developper: Developer
@@ -571,3 +571,4 label_associated_revisions: Associated revisions
571 setting_user_format: Users display format
571 setting_user_format: Users display format
572 text_status_changed_by_changeset: Applied in changeset %s.
572 text_status_changed_by_changeset: Applied in changeset %s.
573 label_more: More
573 label_more: More
574 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
@@ -572,3 +572,4 label_associated_revisions: Liittyvät versiot
572 setting_user_format: Users display format
572 setting_user_format: Users display format
573 text_status_changed_by_changeset: Applied in changeset %s.
573 text_status_changed_by_changeset: Applied in changeset %s.
574 label_more: More
574 label_more: More
575 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
@@ -543,6 +543,7 text_user_mail_option: "Pour les projets non sélectionnés, vous recevrez seule
543 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é."
543 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é."
544 text_load_default_configuration: Charger le paramétrage par défaut
544 text_load_default_configuration: Charger le paramétrage par défaut
545 text_status_changed_by_changeset: Appliqué par commit %s.
545 text_status_changed_by_changeset: Appliqué par commit %s.
546 text_issues_destroy_confirmation: 'Etes-vous sûr de vouloir supprimer le(s) demandes(s) selectionnée(s) ?'
546
547
547 default_role_manager: Manager
548 default_role_manager: Manager
548 default_role_developper: Développeur
549 default_role_developper: Développeur
@@ -568,3 +568,4 label_associated_revisions: Associated revisions
568 setting_user_format: Users display format
568 setting_user_format: Users display format
569 text_status_changed_by_changeset: Applied in changeset %s.
569 text_status_changed_by_changeset: Applied in changeset %s.
570 label_more: More
570 label_more: More
571 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
@@ -568,3 +568,4 label_associated_revisions: Associated revisions
568 setting_user_format: Users display format
568 setting_user_format: Users display format
569 text_status_changed_by_changeset: Applied in changeset %s.
569 text_status_changed_by_changeset: Applied in changeset %s.
570 label_more: More
570 label_more: More
571 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
@@ -569,3 +569,4 label_associated_revisions: Associated revisions
569 setting_user_format: Users display format
569 setting_user_format: Users display format
570 text_status_changed_by_changeset: Applied in changeset %s.
570 text_status_changed_by_changeset: Applied in changeset %s.
571 label_more: More
571 label_more: More
572 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
@@ -568,3 +568,4 label_associated_revisions: Associated revisions
568 setting_user_format: Users display format
568 setting_user_format: Users display format
569 text_status_changed_by_changeset: Applied in changeset %s.
569 text_status_changed_by_changeset: Applied in changeset %s.
570 label_more: More
570 label_more: More
571 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
@@ -569,3 +569,4 label_associated_revisions: susijusios revizijos
569 setting_user_format: Vartotojo atvaizdavimo formatas
569 setting_user_format: Vartotojo atvaizdavimo formatas
570 text_status_changed_by_changeset: Applied in changeset %s.
570 text_status_changed_by_changeset: Applied in changeset %s.
571 label_more: More
571 label_more: More
572 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
@@ -569,3 +569,4 label_associated_revisions: Associated revisions
569 setting_user_format: Users display format
569 setting_user_format: Users display format
570 text_status_changed_by_changeset: Applied in changeset %s.
570 text_status_changed_by_changeset: Applied in changeset %s.
571 label_more: More
571 label_more: More
572 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
@@ -568,3 +568,4 label_associated_revisions: Associated revisions
568 setting_user_format: Users display format
568 setting_user_format: Users display format
569 text_status_changed_by_changeset: Applied in changeset %s.
569 text_status_changed_by_changeset: Applied in changeset %s.
570 label_more: More
570 label_more: More
571 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
@@ -568,3 +568,4 label_associated_revisions: Associated revisions
568 setting_user_format: Users display format
568 setting_user_format: Users display format
569 text_status_changed_by_changeset: Applied in changeset %s.
569 text_status_changed_by_changeset: Applied in changeset %s.
570 label_more: More
570 label_more: More
571 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
@@ -568,3 +568,4 label_associated_revisions: Associated revisions
568 setting_user_format: Users display format
568 setting_user_format: Users display format
569 text_status_changed_by_changeset: Applied in changeset %s.
569 text_status_changed_by_changeset: Applied in changeset %s.
570 label_more: More
570 label_more: More
571 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
@@ -568,3 +568,4 label_associated_revisions: Associated revisions
568 setting_user_format: Users display format
568 setting_user_format: Users display format
569 text_status_changed_by_changeset: Applied in changeset %s.
569 text_status_changed_by_changeset: Applied in changeset %s.
570 label_more: More
570 label_more: More
571 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
@@ -569,3 +569,4 enumeration_doc_categories: Категории документов
569 enumeration_activities: Действия (учет времени)
569 enumeration_activities: Действия (учет времени)
570 text_status_changed_by_changeset: Applied in changeset %s.
570 text_status_changed_by_changeset: Applied in changeset %s.
571 label_more: More
571 label_more: More
572 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
@@ -569,3 +569,4 label_associated_revisions: Associated revisions
569 setting_user_format: Users display format
569 setting_user_format: Users display format
570 text_status_changed_by_changeset: Applied in changeset %s.
570 text_status_changed_by_changeset: Applied in changeset %s.
571 label_more: More
571 label_more: More
572 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
@@ -569,3 +569,4 label_associated_revisions: Associated revisions
569 setting_user_format: Users display format
569 setting_user_format: Users display format
570 text_status_changed_by_changeset: Applied in changeset %s.
570 text_status_changed_by_changeset: Applied in changeset %s.
571 label_more: More
571 label_more: More
572 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
@@ -568,3 +568,4 enumeration_doc_categories: 文件分類
568 enumeration_activities: 活動 (time tracking)
568 enumeration_activities: 活動 (time tracking)
569 text_status_changed_by_changeset: Applied in changeset %s.
569 text_status_changed_by_changeset: Applied in changeset %s.
570 label_more: More
570 label_more: More
571 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
@@ -571,3 +571,4 label_associated_revisions: 相关的版本
571 setting_user_format: 用户显示格式
571 setting_user_format: 用户显示格式
572 text_status_changed_by_changeset: Applied in changeset %s.
572 text_status_changed_by_changeset: Applied in changeset %s.
573 label_more: More
573 label_more: More
574 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
@@ -31,11 +31,10 Redmine::AccessControl.map do |map|
31 :queries => :index,
31 :queries => :index,
32 :reports => :issue_report}, :public => true
32 :reports => :issue_report}, :public => true
33 map.permission :add_issues, {:issues => :new}
33 map.permission :add_issues, {:issues => :new}
34 map.permission :edit_issues, {:projects => :bulk_edit_issues,
34 map.permission :edit_issues, {:issues => [:edit, :bulk_edit, :destroy_attachment]}
35 :issues => [:edit, :destroy_attachment]}
36 map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
35 map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
37 map.permission :add_issue_notes, {:issues => :edit}
36 map.permission :add_issue_notes, {:issues => :edit}
38 map.permission :move_issues, {:projects => :move_issues}, :require => :loggedin
37 map.permission :move_issues, {:issues => :move}, :require => :loggedin
39 map.permission :delete_issues, {:issues => :destroy}, :require => :member
38 map.permission :delete_issues, {:issues => :destroy}, :require => :member
40 # Queries
39 # Queries
41 map.permission :manage_public_queries, {:queries => [:new, :edit, :destroy]}, :require => :member
40 map.permission :manage_public_queries, {:queries => [:new, :edit, :destroy]}, :require => :member
@@ -1,3 +1,6
1 /* redMine - project management software
2 Copyright (C) 2006-2008 Jean-Philippe Lang */
3
1 function checkAll (id, checked) {
4 function checkAll (id, checked) {
2 var el = document.getElementById(id);
5 var el = document.getElementById(id);
3 for (var i = 0; i < el.elements.length; i++) {
6 for (var i = 0; i < el.elements.length; i++) {
@@ -49,16 +52,6 function promptToRemote(text, param, url) {
49 }
52 }
50 }
53 }
51
54
52 /* checks that at least one checkbox is checked (used when submitting bulk edit form) */
53 function checkBulkEdit(form) {
54 for (var i = 0; i < form.elements.length; i++) {
55 if (form.elements[i].checked) {
56 return true;
57 }
58 }
59 return false;
60 }
61
62 function collapseScmEntry(id) {
55 function collapseScmEntry(id) {
63 var els = document.getElementsByClassName(id, 'browser');
56 var els = document.getElementsByClassName(id, 'browser');
64 for (var i = 0; i < els.length; i++) {
57 for (var i = 0; i < els.length; i++) {
@@ -1,47 +1,161
1 /* redMine - project management software
2 Copyright (C) 2006-2008 Jean-Philippe Lang */
3
4 var observingContextMenuClick;
5
1 ContextMenu = Class.create();
6 ContextMenu = Class.create();
2 ContextMenu.prototype = {
7 ContextMenu.prototype = {
3 initialize: function (options) {
8 initialize: function (url) {
4 this.options = Object.extend({selector: '.hascontextmenu'}, options || { });
9 this.url = url;
5
10
6 Event.observe(document, 'click', function(e){
11 // prevent selection when using Ctrl/Shit key
7 var t = Event.findElement(e, 'a');
12 var tables = $$('table.issues');
8 if ((t != document) && (Element.hasClassName(t, 'disabled') || Element.hasClassName(t, 'submenu'))) {
13 for (i=0; i<tables.length; i++) {
9 Event.stop(e);
14 tables[i].onselectstart = function () { return false; } // ie
10 } else {
15 tables[i].onmousedown = function () { return false; } // mozilla
11 $('context-menu').hide();
16 }
12 if (this.selection) {
17
13 this.selection.removeClassName('context-menu-selection');
18 if (!observingContextMenuClick) {
14 }
19 Event.observe(document, 'click', this.Click.bindAsEventListener(this));
15 }
20 Event.observe(document, (window.opera ? 'click' : 'contextmenu'), this.RightClick.bindAsEventListener(this));
16
21 observingContextMenuClick = true;
17 }.bind(this));
22 }
18
23
19 $$(this.options.selector).invoke('observe', (window.opera ? 'click' : 'contextmenu'), function(e){
24 this.unselectAll();
20 if (window.opera && !e.ctrlKey) {
25 this.lastSelected = null;
21 return;
22 }
23 this.show(e);
24 }.bind(this));
25
26 },
26 },
27 show: function(e) {
27
28 RightClick: function(e) {
29 this.hideMenu();
30 // do not show the context menu on links
31 if (Event.findElement(e, 'a') != document) { return; }
32 // right-click simulated by Alt+Click with Opera
33 if (window.opera && !e.altKey) { return; }
34 var tr = Event.findElement(e, 'tr');
35 if ((tr == document) || !tr.hasClassName('hascontextmenu')) { return; }
28 Event.stop(e);
36 Event.stop(e);
29 Element.hide('context-menu');
37 if (!this.isSelected(tr)) {
30 if (this.selection) {
38 this.unselectAll();
31 this.selection.removeClassName('context-menu-selection');
39 this.addSelection(tr);
40 this.lastSelected = tr;
32 }
41 }
42 this.showMenu(e);
43 },
44
45 Click: function(e) {
46 this.hideMenu();
47 if (Event.findElement(e, 'a') != document) { return; }
48 if (window.opera && e.altKey) { return; }
49 if (Event.isLeftClick(e) || (navigator.appVersion.match(/\bMSIE\b/))) {
50 var tr = Event.findElement(e, 'tr');
51 if (tr!=document && tr.hasClassName('hascontextmenu')) {
52 // a row was clicked, check if the click was on checkbox
53 var box = Event.findElement(e, 'input');
54 if (box!=document) {
55 // a checkbox may be clicked
56 if (box.checked) {
57 tr.addClassName('context-menu-selection');
58 } else {
59 tr.removeClassName('context-menu-selection');
60 }
61 } else {
62 if (e.ctrlKey) {
63 this.toggleSelection(tr);
64 } else if (e.shiftKey) {
65 if (this.lastSelected != null) {
66 var toggling = false;
67 var rows = $$('.hascontextmenu');
68 for (i=0; i<rows.length; i++) {
69 if (toggling || rows[i]==tr) {
70 this.addSelection(rows[i]);
71 }
72 if (rows[i]==tr || rows[i]==this.lastSelected) {
73 toggling = !toggling;
74 }
75 }
76 } else {
77 this.addSelection(tr);
78 }
79 } else {
80 this.unselectAll();
81 this.addSelection(tr);
82 }
83 this.lastSelected = tr;
84 }
85 } else {
86 // click is outside the rows
87 var t = Event.findElement(e, 'a');
88 if ((t != document) && (Element.hasClassName(t, 'disabled') || Element.hasClassName(t, 'submenu'))) {
89 Event.stop(e);
90 }
91 }
92 }
93 },
94
95 showMenu: function(e) {
33 $('context-menu').style['left'] = (Event.pointerX(e) + 'px');
96 $('context-menu').style['left'] = (Event.pointerX(e) + 'px');
34 $('context-menu').style['top'] = (Event.pointerY(e) + 'px');
97 $('context-menu').style['top'] = (Event.pointerY(e) + 'px');
35 Element.update('context-menu', '');
98 Element.update('context-menu', '');
99 new Ajax.Updater({success:'context-menu'}, this.url,
100 {asynchronous:true,
101 evalScripts:true,
102 parameters:Form.serialize(Event.findElement(e, 'form')),
103 onComplete:function(request){
104 Effect.Appear('context-menu', {duration: 0.20});
105 if (window.parseStylesheets) { window.parseStylesheets(); } // IE
106 }})
107 },
108
109 hideMenu: function() {
110 Element.hide('context-menu');
111 },
112
113 addSelection: function(tr) {
114 tr.addClassName('context-menu-selection');
115 this.checkSelectionBox(tr, true);
116 },
117
118 toggleSelection: function(tr) {
119 if (this.isSelected(tr)) {
120 this.removeSelection(tr);
121 } else {
122 this.addSelection(tr);
123 }
124 },
125
126 removeSelection: function(tr) {
127 tr.removeClassName('context-menu-selection');
128 this.checkSelectionBox(tr, false);
129 },
130
131 unselectAll: function() {
132 var rows = $$('.hascontextmenu');
133 for (i=0; i<rows.length; i++) {
134 this.removeSelection(rows[i]);
135 }
136 },
137
138 checkSelectionBox: function(tr, checked) {
139 var inputs = Element.getElementsBySelector(tr, 'input');
140 if (inputs.length > 0) { inputs[0].checked = checked; }
141 },
142
143 isSelected: function(tr) {
144 return Element.hasClassName(tr, 'context-menu-selection');
145 }
146 }
36
147
37 var tr = Event.findElement(e, 'tr');
148 function toggleIssuesSelection(el) {
38 tr.addClassName('context-menu-selection');
149 var boxes = el.getElementsBySelector('input[type=checkbox]');
39 this.selection = tr;
150 var all_checked = true;
40 var id = tr.id.substring(6, tr.id.length);
151 for (i = 0; i < boxes.length; i++) { if (boxes[i].checked == false) { all_checked = false; } }
41 /* TODO: do not hard code path */
152 for (i = 0; i < boxes.length; i++) {
42 new Ajax.Updater({success:'context-menu'}, '../../issues/context_menu/' + id, {asynchronous:true, evalScripts:true, onComplete:function(request){
153 if (all_checked) {
43 Effect.Appear('context-menu', {duration: 0.20});
154 boxes[i].checked = false;
44 if (window.parseStylesheets) { window.parseStylesheets(); }
155 boxes[i].up('tr').removeClassName('context-menu-selection');
45 }})
156 } else if (boxes[i].checked == false) {
157 boxes[i].checked = true;
158 boxes[i].up('tr').addClassName('context-menu-selection');
159 }
46 }
160 }
47 }
161 }
@@ -197,6 +197,28 class IssuesControllerTest < Test::Unit::TestCase
197 assert_not_nil assigns(:issue)
197 assert_not_nil assigns(:issue)
198 assert_equal Issue.find(1), assigns(:issue)
198 assert_equal Issue.find(1), assigns(:issue)
199 end
199 end
200
201 def test_get_edit_with_params
202 @request.session[:user_id] = 2
203 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
204 assert_response :success
205 assert_template 'edit'
206
207 issue = assigns(:issue)
208 assert_not_nil issue
209
210 assert_equal 5, issue.status_id
211 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
212 :child => { :tag => 'option',
213 :content => 'Closed',
214 :attributes => { :selected => 'selected' } }
215
216 assert_equal 7, issue.priority_id
217 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
218 :child => { :tag => 'option',
219 :content => 'Urgent',
220 :attributes => { :selected => 'selected' } }
221 end
200
222
201 def test_post_edit
223 def test_post_edit
202 @request.session[:user_id] = 2
224 @request.session[:user_id] = 2
@@ -305,12 +327,105 class IssuesControllerTest < Test::Unit::TestCase
305 # No email should be sent
327 # No email should be sent
306 assert ActionMailer::Base.deliveries.empty?
328 assert ActionMailer::Base.deliveries.empty?
307 end
329 end
330
331 def test_bulk_edit
332 @request.session[:user_id] = 2
333 # update issues priority
334 post :bulk_edit, :ids => [1, 2], :priority_id => 7, :notes => 'Bulk editing', :assigned_to_id => ''
335 assert_response 302
336 # check that the issues were updated
337 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
338 assert_equal 'Bulk editing', Issue.find(1).journals.find(:first, :order => 'created_on DESC').notes
339 end
340
341 def test_move_one_issue_to_another_project
342 @request.session[:user_id] = 1
343 post :move, :id => 1, :new_project_id => 2
344 assert_redirected_to 'projects/ecookbook/issues'
345 assert_equal 2, Issue.find(1).project_id
346 end
347
348 def test_bulk_move_to_another_project
349 @request.session[:user_id] = 1
350 post :move, :ids => [1, 2], :new_project_id => 2
351 assert_redirected_to 'projects/ecookbook/issues'
352 # Issues moved to project 2
353 assert_equal 2, Issue.find(1).project_id
354 assert_equal 2, Issue.find(2).project_id
355 # No tracker change
356 assert_equal 1, Issue.find(1).tracker_id
357 assert_equal 2, Issue.find(2).tracker_id
358 end
359
360 def test_bulk_move_to_another_tracker
361 @request.session[:user_id] = 1
362 post :move, :ids => [1, 2], :new_tracker_id => 2
363 assert_redirected_to 'projects/ecookbook/issues'
364 assert_equal 2, Issue.find(1).tracker_id
365 assert_equal 2, Issue.find(2).tracker_id
366 end
308
367
309 def test_context_menu
368 def test_context_menu_one_issue
369 @request.session[:user_id] = 2
370 get :context_menu, :ids => [1]
371 assert_response :success
372 assert_template 'context_menu'
373 assert_tag :tag => 'a', :content => 'Edit',
374 :attributes => { :href => '/issues/edit/1',
375 :class => 'icon-edit' }
376 assert_tag :tag => 'a', :content => 'Closed',
377 :attributes => { :href => '/issues/edit/1?issue%5Bstatus_id%5D=5',
378 :class => '' }
379 assert_tag :tag => 'a', :content => 'Immediate',
380 :attributes => { :href => '/issues/edit/1?issue%5Bpriority_id%5D=8',
381 :class => '' }
382 assert_tag :tag => 'a', :content => 'Dave Lopper',
383 :attributes => { :href => '/issues/edit/1?issue%5Bassigned_to_id%5D=3',
384 :class => '' }
385 assert_tag :tag => 'a', :content => 'Copy',
386 :attributes => { :href => '/projects/ecookbook/issues/new?copy_from=1',
387 :class => 'icon-copy' }
388 assert_tag :tag => 'a', :content => 'Move',
389 :attributes => { :href => '/issues/move?ids%5B%5D=1',
390 :class => 'icon-move' }
391 assert_tag :tag => 'a', :content => 'Delete',
392 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
393 :class => 'icon-del' }
394 end
395
396 def test_context_menu_one_issue_by_anonymous
397 get :context_menu, :ids => [1]
398 assert_response :success
399 assert_template 'context_menu'
400 assert_tag :tag => 'a', :content => 'Delete',
401 :attributes => { :href => '#',
402 :class => 'icon-del disabled' }
403 end
404
405 def test_context_menu_multiple_issues_of_same_project
406 @request.session[:user_id] = 2
407 get :context_menu, :ids => [1, 2]
408 assert_response :success
409 assert_template 'context_menu'
410 assert_tag :tag => 'a', :content => 'Edit',
411 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
412 :class => 'icon-edit' }
413 assert_tag :tag => 'a', :content => 'Move',
414 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
415 :class => 'icon-move' }
416 assert_tag :tag => 'a', :content => 'Delete',
417 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
418 :class => 'icon-del' }
419 end
420
421 def test_context_menu_multiple_issues_of_different_project
310 @request.session[:user_id] = 2
422 @request.session[:user_id] = 2
311 get :context_menu, :id => 1
423 get :context_menu, :ids => [1, 2, 4]
312 assert_response :success
424 assert_response :success
313 assert_template 'context_menu'
425 assert_template 'context_menu'
426 assert_tag :tag => 'a', :content => 'Delete',
427 :attributes => { :href => '#',
428 :class => 'icon-del disabled' }
314 end
429 end
315
430
316 def test_destroy
431 def test_destroy
@@ -93,32 +93,6 class ProjectsControllerTest < Test::Unit::TestCase
93 assert_nil Project.find_by_id(1)
93 assert_nil Project.find_by_id(1)
94 end
94 end
95
95
96 def test_bulk_edit_issues
97 @request.session[:user_id] = 2
98 # update issues priority
99 post :bulk_edit_issues, :id => 1, :issue_ids => [1, 2], :priority_id => 7, :notes => 'Bulk editing', :assigned_to_id => ''
100 assert_response 302
101 # check that the issues were updated
102 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
103 assert_equal 'Bulk editing', Issue.find(1).journals.find(:first, :order => 'created_on DESC').notes
104 end
105
106 def test_move_issues_to_another_project
107 @request.session[:user_id] = 1
108 post :move_issues, :id => 1, :issue_ids => [1, 2], :new_project_id => 2
109 assert_redirected_to 'projects/ecookbook/issues'
110 assert_equal 2, Issue.find(1).project_id
111 assert_equal 2, Issue.find(2).project_id
112 end
113
114 def test_move_issues_to_another_tracker
115 @request.session[:user_id] = 1
116 post :move_issues, :id => 1, :issue_ids => [1, 2], :new_tracker_id => 2
117 assert_redirected_to 'projects/ecookbook/issues'
118 assert_equal 2, Issue.find(1).tracker_id
119 assert_equal 2, Issue.find(2).tracker_id
120 end
121
122 def test_list_files
96 def test_list_files
123 get :list_files, :id => 1
97 get :list_files, :id => 1
124 assert_response :success
98 assert_response :success
General Comments 0
You need to be logged in to leave comments. Login now