Commit bb4acc02 authored by Jean-Philippe Lang's avatar Jean-Philippe Lang

Added AJAX based context menu on the project issue list that provide shortcuts…

Added AJAX based context menu on the project issue list that provide shortcuts for editing, re-assigning, changing the status or the priority, moving or deleting an issue.
The context menu shows up when right-clicking an issue (Opera users have to use Ctrl + left-click instead since right-click can't be reassigned for this browser).
Works with Firefox 2, IE 7 (not perfect), Opera 9 and Safari 2. IE 6 doesn't display submenus.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@872 e93f8b46-1217-0410-a6f0-8f06a7374b81
parent d9e6359a
...@@ -79,12 +79,14 @@ class IssuesController < ApplicationController ...@@ -79,12 +79,14 @@ class IssuesController < ApplicationController
begin begin
@issue.init_journal(self.logged_in_user) @issue.init_journal(self.logged_in_user)
# Retrieve custom fields and values # Retrieve custom fields and values
@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]) } if params["custom_fields"]
@issue.custom_values = @custom_values @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]) }
@issue.custom_values = @custom_values
end
@issue.attributes = params[:issue] @issue.attributes = params[:issue]
if @issue.save if @issue.save
flash[:notice] = l(:notice_successful_update) flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'show', :id => @issue redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
end end
rescue ActiveRecord::StaleObjectError rescue ActiveRecord::StaleObjectError
# Optimistic locking exception # Optimistic locking exception
...@@ -163,6 +165,19 @@ class IssuesController < ApplicationController ...@@ -163,6 +165,19 @@ class IssuesController < ApplicationController
journal.save journal.save
redirect_to :action => 'show', :id => @issue redirect_to :action => 'show', :id => @issue
end end
def context_menu
@priorities = Enumeration.get_values('IPRI').reverse
@statuses = IssueStatus.find(:all, :order => 'position')
@allowed_statuses = @issue.status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)
@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),
:change_status => User.current.allowed_to?(:change_issue_status, @project),
:move => User.current.allowed_to?(:move_issues, @project),
:delete => User.current.allowed_to?(:delete_issues, @project)}
render :layout => false
end
def preview def preview
issue = Issue.find_by_id(params[:id]) issue = Issue.find_by_id(params[:id])
......
...@@ -296,6 +296,22 @@ module ApplicationHelper ...@@ -296,6 +296,22 @@ module ApplicationHelper
link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)") link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
end end
def context_menu_link(name, url, options={})
options[:class] ||= ''
if options.delete(:selected)
options[:class] << ' icon-checked disabled'
options[:disabled] = true
end
if options.delete(:disabled)
options.delete(:method)
options.delete(:confirm)
options.delete(:onclick)
options[:class] << ' disabled'
url = '#'
end
link_to name, url, options
end
def calendar_for(field_id) def calendar_for(field_id)
image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) + image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });") javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
</tr></thead> </tr></thead>
<tbody> <tbody>
<% issues.each do |issue| %> <% issues.each do |issue| %>
<tr class="issue <%= cycle('odd', 'even') %> <%= "status-#{issue.status.position} priority-#{issue.priority.position}" %>"> <tr id="issue-<%= issue.id %>" class="issue hascontextmenu <%= cycle('odd', 'even') %> <%= "status-#{issue.status.position} priority-#{issue.priority.position}" %>">
<td class="checkbox"><%= check_box_tag("issue_ids[]", issue.id, false, :id => "issue_#{issue.id}", :disabled => (!@project || @project != issue.project)) %></td> <td class="checkbox"><%= check_box_tag("issue_ids[]", issue.id, false, :id => "issue_#{issue.id}", :disabled => (!@project || @project != issue.project)) %></td>
<td><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td> <td><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td>
<% query.columns.each do |column| %> <% query.columns.each do |column| %>
......
<% back_to = url_for(:controller => 'projects', :action => 'list_issues', :id => @project) %>
<ul>
<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| %>
<li><%= context_menu_link s.name, {:controller => 'issues', :action => 'change_status', :id => @issue, :new_status_id => s},
:selected => (s == @issue.status), :disabled => !(@can[:change_status] && @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,
:selected => (p == @issue.priority), :disabled => !@can[:edit] %></li>
<% 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[:edit] || @can[:change_status]) %></li>
<% end %>
<li><%= context_menu_link l(:label_nobody), {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[assigned_to_id]' => '', :back_to => back_to}, :method => :post,
:selected => @issue.assigned_to.nil?, :disabled => !(@can[:edit] || @can[:change_status]) %></li>
</ul>
</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>
</ul>
...@@ -64,4 +64,9 @@ ...@@ -64,4 +64,9 @@
<%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %> <%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %>
<%= javascript_include_tag 'calendar/calendar-setup' %> <%= javascript_include_tag 'calendar/calendar-setup' %>
<%= stylesheet_link_tag 'calendar' %> <%= stylesheet_link_tag 'calendar' %>
<%= javascript_include_tag 'context_menu' %>
<%= stylesheet_link_tag 'context_menu' %>
<% end %> <% end %>
<div id="context-menu" style="display: none;"></div>
<%= javascript_tag 'new ContextMenu({})' %>
...@@ -26,7 +26,7 @@ Redmine::AccessControl.map do |map| ...@@ -26,7 +26,7 @@ Redmine::AccessControl.map do |map|
map.permission :manage_categories, {:projects => [:settings, :add_issue_category], :issue_categories => [:edit, :destroy]}, :require => :member map.permission :manage_categories, {:projects => [:settings, :add_issue_category], :issue_categories => [:edit, :destroy]}, :require => :member
# Issues # Issues
map.permission :view_issues, {:projects => [:list_issues, :export_issues_csv, :export_issues_pdf, :changelog, :roadmap], map.permission :view_issues, {:projects => [:list_issues, :export_issues_csv, :export_issues_pdf, :changelog, :roadmap],
:issues => :show, :issues => [:show, :context_menu],
:queries => :index, :queries => :index,
:reports => :issue_report}, :public => true :reports => :issue_report}, :public => true
map.permission :add_issues, {:projects => :add_issue}, :require => :loggedin map.permission :add_issues, {:projects => :add_issue}, :require => :loggedin
......
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));
},
show: function(e) {
Event.stop(e);
Element.hide('context-menu');
if (this.selection) {
this.selection.removeClassName('context-menu-selection');
}
$('context-menu').style['left'] = (Event.pointerX(e) + 'px');
$('context-menu').style['top'] = (Event.pointerY(e) + 'px');
Element.update('context-menu', '');
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('context-menu', '../../issues/context_menu/' + id, {asynchronous:true, evalScripts:true, onComplete:function(request){Effect.Appear('context-menu', {duration: 0.20})}})
}
}
...@@ -42,7 +42,7 @@ h4, .wiki h3 {font-size: 12px;padding: 2px 10px 1px 0px;margin-bottom: 5px; bord ...@@ -42,7 +42,7 @@ h4, .wiki h3 {font-size: 12px;padding: 2px 10px 1px 0px;margin-bottom: 5px; bord
#sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; } #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
* html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; } * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
#content { width: 80%; background: url(../images/contentbg.png) repeat-x; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; position: relative; z-index: 10; height:600px; min-height: 600px;} #content { width: 80%; background: url(../images/contentbg.png) repeat-x; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; height:600px; min-height: 600px;}
* html #content{ width: 80%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;} * html #content{ width: 80%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
html>body #content { html>body #content {
height: auto; height: auto;
...@@ -154,7 +154,7 @@ width: 200px; ...@@ -154,7 +154,7 @@ width: 200px;
div.attachments p { margin:4px 0 2px 0; } div.attachments p { margin:4px 0 2px 0; }
/***** Flash & error messages ****/ /***** Flash & error messages ****/
#errorExplanation, div.flash, div.nodata { #errorExplanation, div.flash, .nodata {
padding: 4px 4px 4px 30px; padding: 4px 4px 4px 30px;
margin-bottom: 12px; margin-bottom: 12px;
font-size: 1.1em; font-size: 1.1em;
...@@ -454,6 +454,7 @@ vertical-align: middle; ...@@ -454,6 +454,7 @@ vertical-align: middle;
.icon-lock { background-image: url(../images/locked.png); } .icon-lock { background-image: url(../images/locked.png); }
.icon-unlock { background-image: url(../images/unlock.png); } .icon-unlock { background-image: url(../images/unlock.png); }
.icon-note { background-image: url(../images/note.png); } .icon-note { background-image: url(../images/note.png); }
.icon-checked { background-image: url(../images/true.png); }
.icon22-projects { background-image: url(../images/22x22/projects.png); } .icon22-projects { background-image: url(../images/22x22/projects.png); }
.icon22-users { background-image: url(../images/22x22/users.png); } .icon22-users { background-image: url(../images/22x22/users.png); }
......
#context-menu { position: absolute; }
#context-menu ul, #context-menu li, #context-menu a {
display:block;
margin:0;
padding:0;
border:0;
}
#context-menu ul {
width:150px;
border-top:1px solid #ddd;
border-left:1px solid #ddd;
border-bottom:1px solid #777;
border-right:1px solid #777;
background:white;
list-style:none;
}
#context-menu li {
position:relative;
padding:1px;
z-index:9;
}
#context-menu li.folder ul {
position:absolute;
left:128px; /* IE */
top:-2px;
}
#context-menu li.folder>ul { left:148px; }
#context-menu a {
border:1px solid white;
text-decoration:none;
background-repeat: no-repeat;
background-position: 1px 50%;
padding: 2px 0px 2px 20px;
width:100%; /* IE */
}
#context-menu li>a { width:auto; } /* others */
#context-menu a.disabled, #context-menu a.disabled:hover {color: #ccc;}
#context-menu li a.submenu { background:url("../images/sub.gif") right no-repeat; }
#context-menu a:hover { border-color:gray; background-color:#eee; color:#2A5685; }
#context-menu li.folder a:hover { background-color:#eee; }
#context-menu li.folder:hover { z-index:10; }
#context-menu ul ul, #context-menu li:hover ul ul { display:none; }
#context-menu li:hover ul, #context-menu li:hover li:hover ul { display:block; }
/* selected element */
.context-menu-selection { background-color:#507AAA !important; color:#f8f8f8 !important; }
.context-menu-selection a, .context-menu-selection a:hover { color:#f8f8f8 !important; }
.context-menu-selection:hover { background-color:#507AAA !important; color:#f8f8f8 !important; }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment