diff options
-rw-r--r-- | app/controllers/issues_controller.rb | 21 | ||||
-rw-r--r-- | app/helpers/application_helper.rb | 16 | ||||
-rw-r--r-- | app/views/issues/_list.rhtml | 2 | ||||
-rw-r--r-- | app/views/issues/context_menu.rhtml | 38 | ||||
-rw-r--r-- | app/views/projects/list_issues.rhtml | 5 | ||||
-rw-r--r-- | lib/redmine.rb | 2 | ||||
-rw-r--r-- | public/images/sub.gif | bin | 0 -> 52 bytes | |||
-rw-r--r-- | public/javascripts/context_menu.js | 44 | ||||
-rw-r--r-- | public/stylesheets/application.css | 5 | ||||
-rw-r--r-- | public/stylesheets/context_menu.css | 52 |
10 files changed, 178 insertions, 7 deletions
diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index bad778e67..d3949cbee 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -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]) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 0833c43fa..3fc333aa9 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -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' });") diff --git a/app/views/issues/_list.rhtml b/app/views/issues/_list.rhtml index a3feb8c38..d8e3102df 100644 --- a/app/views/issues/_list.rhtml +++ b/app/views/issues/_list.rhtml @@ -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| %> diff --git a/app/views/issues/context_menu.rhtml b/app/views/issues/context_menu.rhtml new file mode 100644 index 000000000..caf6a76ea --- /dev/null +++ b/app/views/issues/context_menu.rhtml @@ -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> diff --git a/app/views/projects/list_issues.rhtml b/app/views/projects/list_issues.rhtml index 9f2ff870d..0c0aaa418 100644 --- a/app/views/projects/list_issues.rhtml +++ b/app/views/projects/list_issues.rhtml @@ -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({})' %> diff --git a/lib/redmine.rb b/lib/redmine.rb index ceb531551..bf6d63059 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -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 diff --git a/public/images/sub.gif b/public/images/sub.gif Binary files differnew file mode 100644 index 000000000..52e4065d5 --- /dev/null +++ b/public/images/sub.gif diff --git a/public/javascripts/context_menu.js b/public/javascripts/context_menu.js new file mode 100644 index 000000000..a6e39c512 --- /dev/null +++ b/public/javascripts/context_menu.js @@ -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})}}) + } +} diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index cf0c3baf5..d1041f162 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -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); } diff --git a/public/stylesheets/context_menu.css b/public/stylesheets/context_menu.css new file mode 100644 index 000000000..52cac79ee --- /dev/null +++ b/public/stylesheets/context_menu.css @@ -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; } |