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
This commit is contained in:
parent
d9e6359a83
commit
bb4acc02d0
|
@ -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
|
||||||
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
|
||||||
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 @@
|
||||||
</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| %>
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
<% 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/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|
|
||||||
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
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 52 B |
|
@ -0,0 +1,44 @@
|
||||||
|
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; }
|
#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;
|
||||||
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;
|
||||||
.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); }
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
#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; }
|
Loading…
Reference in New Issue