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
begin
@issue.init_journal(self.logged_in_user)
# 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]) }
@issue.custom_values = @custom_values
if 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]) }
@issue.custom_values = @custom_values
end
@issue.attributes = params[:issue]
if @issue.save
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'show', :id => @issue
redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
end
rescue ActiveRecord::StaleObjectError
# Optimistic locking exception
......@@ -163,6 +165,19 @@ class IssuesController < ApplicationController
journal.save
redirect_to :action => 'show', :id => @issue
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
issue = Issue.find_by_id(params[:id])
......
......@@ -296,6 +296,22 @@ module ApplicationHelper
link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
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)
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' });")
......
......@@ -13,7 +13,7 @@
</tr></thead>
<tbody>
<% 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><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td>
<% 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 @@
<%= 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({})' %>
......@@ -26,7 +26,7 @@ Redmine::AccessControl.map do |map|
map.permission :manage_categories, {:projects => [:settings, :add_issue_category], :issue_categories => [:edit, :destroy]}, :require => :member
# Issues
map.permission :view_issues, {:projects => [:list_issues, :export_issues_csv, :export_issues_pdf, :changelog, :roadmap],
:issues => :show,
:issues => [:show, :context_menu],
:queries => :index,
:reports => :issue_report}, :public => true
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
#sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
* 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>body #content {
height: auto;
......@@ -154,7 +154,7 @@ width: 200px;
div.attachments p { margin:4px 0 2px 0; }
/***** Flash & error messages ****/
#errorExplanation, div.flash, div.nodata {
#errorExplanation, div.flash, .nodata {
padding: 4px 4px 4px 30px;
margin-bottom: 12px;
font-size: 1.1em;
......@@ -454,6 +454,7 @@ vertical-align: middle;
.icon-lock { background-image: url(../images/locked.png); }
.icon-unlock { background-image: url(../images/unlock.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-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