mirror of
https://github.com/redmine/redmine.git
synced 2025-11-01 02:46:13 +01:00
Adds favorites and recently used projects lists to project jump box (#31355).
Patch by Jens Krämer. git-svn-id: http://svn.redmine.org/redmine/trunk@18181 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
@@ -57,6 +57,7 @@ class ApplicationController < ActionController::Base
|
||||
end
|
||||
|
||||
before_action :session_expiration, :user_setup, :check_if_login_required, :set_localization, :check_password_change
|
||||
after_action :record_project_usage
|
||||
|
||||
rescue_from ::Unauthorized, :with => :deny_access
|
||||
rescue_from ::ActionView::MissingTemplate, :with => :missing_template
|
||||
@@ -403,6 +404,13 @@ class ApplicationController < ActionController::Base
|
||||
end
|
||||
end
|
||||
|
||||
def record_project_usage
|
||||
if @project && @project.id && User.current.logged? && User.current.allowed_to?(:view_project, @project)
|
||||
Redmine::ProjectJumpBox.new(User.current).project_used(@project)
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
def back_url
|
||||
url = params[:back_url]
|
||||
if url.nil? && referer = request.env['HTTP_REFERER']
|
||||
|
||||
@@ -221,6 +221,19 @@ class ProjectsController < ApplicationController
|
||||
redirect_to_referer_or admin_projects_path(:status => params[:status])
|
||||
end
|
||||
|
||||
def bookmark
|
||||
jump_box = Redmine::ProjectJumpBox.new User.current
|
||||
if request.delete?
|
||||
jump_box.delete_project_bookmark @project
|
||||
elsif request.post?
|
||||
jump_box.bookmark_project @project
|
||||
end
|
||||
respond_to do |format|
|
||||
format.js
|
||||
format.html { redirect_to project_path(@project) }
|
||||
end
|
||||
end
|
||||
|
||||
def close
|
||||
@project.close
|
||||
redirect_to project_path(@project)
|
||||
|
||||
@@ -425,12 +425,38 @@ module ApplicationHelper
|
||||
end
|
||||
|
||||
def render_projects_for_jump_box(projects, selected=nil)
|
||||
jump_box = Redmine::ProjectJumpBox.new User.current
|
||||
bookmarked = jump_box.bookmarked_projects(params[:q])
|
||||
recents = jump_box.recently_used_projects(params[:q])
|
||||
projects = projects - (recents + bookmarked)
|
||||
|
||||
projects_label = (bookmarked.any? || recents.any?) ? :label_optgroup_others : :label_project_plural
|
||||
|
||||
jump = params[:jump].presence || current_menu_item
|
||||
s = (+'').html_safe
|
||||
project_tree(projects) do |project, level|
|
||||
|
||||
build_project_link = ->(project, level = 0){
|
||||
padding = level * 16
|
||||
text = content_tag('span', project.name, :style => "padding-left:#{padding}px;")
|
||||
s << link_to(text, project_path(project, :jump => jump), :title => project.name, :class => (project == selected ? 'selected' : nil))
|
||||
}
|
||||
|
||||
[
|
||||
[bookmarked, :label_optgroup_bookmarks, true],
|
||||
[recents, :label_optgroup_recents, false],
|
||||
[projects, projects_label, true]
|
||||
].each do |projects, label, is_tree|
|
||||
|
||||
next if projects.blank?
|
||||
|
||||
s << content_tag(:strong, l(label))
|
||||
if is_tree
|
||||
project_tree(projects, &build_project_link)
|
||||
else
|
||||
# we do not want to render recently used projects as a tree, but in the
|
||||
# order they were used (most recent first)
|
||||
projects.each(&build_project_link)
|
||||
end
|
||||
end
|
||||
s
|
||||
end
|
||||
|
||||
@@ -138,4 +138,24 @@ module ProjectsHelper
|
||||
end
|
||||
end if include_in_api_response?('enabled_modules')
|
||||
end
|
||||
|
||||
def bookmark_link(project, user = User.current)
|
||||
return '' unless user && user.logged?
|
||||
@jump_box ||= Redmine::ProjectJumpBox.new user
|
||||
bookmarked = @jump_box.bookmark?(project)
|
||||
css = +"icon bookmark "
|
||||
|
||||
if bookmarked
|
||||
css << "icon-bookmark"
|
||||
method = "delete"
|
||||
text = l(:button_project_bookmark_delete)
|
||||
else
|
||||
css << "icon-bookmark-off"
|
||||
method = "post"
|
||||
text = l(:button_project_bookmark)
|
||||
end
|
||||
|
||||
url = bookmark_project_url(project)
|
||||
link_to text, url, remote: true, method: method, class: css
|
||||
end
|
||||
end
|
||||
|
||||
@@ -32,7 +32,8 @@ class UserPreference < ActiveRecord::Base
|
||||
'comments_sorting',
|
||||
'warn_on_leaving_unsaved',
|
||||
'no_self_notified',
|
||||
'textarea_font'
|
||||
'textarea_font',
|
||||
'recently_used_projects'
|
||||
|
||||
TEXTAREA_FONT_OPTIONS = ['monospace', 'proportional']
|
||||
|
||||
@@ -90,6 +91,9 @@ class UserPreference < ActiveRecord::Base
|
||||
def textarea_font; self[:textarea_font] end
|
||||
def textarea_font=(value); self[:textarea_font]=value; end
|
||||
|
||||
def recently_used_projects; (self[:recently_used_projects] || 3).to_i; end
|
||||
def recently_used_projects=(value); self[:recently_used_projects] = value.to_i; end
|
||||
|
||||
# Returns the names of groups that are displayed on user's page
|
||||
# Example:
|
||||
# preferences.my_page_groups
|
||||
|
||||
2
app/views/projects/bookmark.js.erb
Normal file
2
app/views/projects/bookmark.js.erb
Normal file
@@ -0,0 +1,2 @@
|
||||
$('#project-jump div.drdn-items.projects').html('<%= j render_projects_for_jump_box(projects_for_jump_box(User.current), @project) %>');
|
||||
$('.contextual a.icon.bookmark').replaceWith('<%= j bookmark_link @project %>');
|
||||
@@ -2,6 +2,7 @@
|
||||
<% if User.current.allowed_to?(:add_subprojects, @project) %>
|
||||
<%= link_to l(:label_subproject_new), new_project_path(:parent_id => @project), :class => 'icon icon-add' %>
|
||||
<% end %>
|
||||
<%= bookmark_link @project %>
|
||||
<% if User.current.allowed_to?(:close_project, @project) %>
|
||||
<% if @project.active? %>
|
||||
<%= link_to l(:button_close), close_project_path(@project), :data => {:confirm => l(:text_are_you_sure)}, :method => :post, :class => 'icon icon-lock' %>
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
<p><%= pref_fields.select :comments_sorting, [[l(:label_chronological_order), 'asc'], [l(:label_reverse_chronological_order), 'desc']] %></p>
|
||||
<p><%= pref_fields.check_box :warn_on_leaving_unsaved %></p>
|
||||
<p><%= pref_fields.select :textarea_font, textarea_font_options %></p>
|
||||
<p><%= pref_fields.text_field :recently_used_projects, :size => 2 %></p>
|
||||
<% end %>
|
||||
|
||||
@@ -382,6 +382,7 @@ en:
|
||||
field_full_width_layout: Full width layout
|
||||
field_digest: Checksum
|
||||
field_default_assigned_to: Default assignee
|
||||
field_recently_used_projects: Number of recently used projects in jump box
|
||||
|
||||
setting_app_title: Application title
|
||||
setting_welcome_text: Welcome text
|
||||
@@ -1044,6 +1045,9 @@ en:
|
||||
label_font_default: Default font
|
||||
label_font_monospace: Monospaced font
|
||||
label_font_proportional: Proportional font
|
||||
label_optgroup_bookmarks: Bookmarks
|
||||
label_optgroup_others: Other projects
|
||||
label_optgroup_recents: Recently used
|
||||
label_last_notes: Last notes
|
||||
label_nothing_to_preview: Nothing to preview
|
||||
label_inherited_from_parent_project: "Inherited from parent project"
|
||||
@@ -1106,6 +1110,8 @@ en:
|
||||
button_close: Close
|
||||
button_reopen: Reopen
|
||||
button_import: Import
|
||||
button_project_bookmark: Add bookmark
|
||||
button_project_bookmark_delete: Remove bookmark
|
||||
button_filter: Filter
|
||||
button_actions: Actions
|
||||
|
||||
|
||||
@@ -113,6 +113,7 @@ Rails.application.routes.draw do
|
||||
post 'close'
|
||||
post 'reopen'
|
||||
match 'copy', :via => [:get, :post]
|
||||
match 'bookmark', :via => [:delete, :post]
|
||||
end
|
||||
|
||||
shallow do
|
||||
|
||||
@@ -76,7 +76,7 @@ Redmine::Scm::Base.add "Filesystem"
|
||||
|
||||
# Permissions
|
||||
Redmine::AccessControl.map do |map|
|
||||
map.permission :view_project, {:projects => [:show], :activities => [:index]}, :public => true, :read => true
|
||||
map.permission :view_project, {:projects => [:show, :bookmark], :activities => [:index]}, :public => true, :read => true
|
||||
map.permission :search_project, {:search => :index}, :public => true, :read => true
|
||||
map.permission :add_project, {:projects => [:new, :create]}, :require => :loggedin
|
||||
map.permission :edit_project, {:projects => [:settings, :edit, :update]}, :require => :member
|
||||
|
||||
94
lib/redmine/project_jump_box.rb
Normal file
94
lib/redmine/project_jump_box.rb
Normal file
@@ -0,0 +1,94 @@
|
||||
module Redmine
|
||||
class ProjectJumpBox
|
||||
def initialize(user)
|
||||
@user = user
|
||||
@pref_project_ids = {}
|
||||
end
|
||||
|
||||
def recent_projects_count
|
||||
@user.pref.recently_used_projects
|
||||
end
|
||||
|
||||
def recently_used_projects(query = nil)
|
||||
project_ids = recently_used_project_ids
|
||||
projects = Project.where(id: project_ids)
|
||||
if query
|
||||
projects = projects.like(query)
|
||||
end
|
||||
projects.
|
||||
index_by(&:id).
|
||||
values_at(*project_ids). # sort according to stored order
|
||||
compact
|
||||
end
|
||||
|
||||
def bookmarked_projects(query = nil)
|
||||
projects = Project.where(id: bookmarked_project_ids).visible
|
||||
if query
|
||||
projects = projects.like(query)
|
||||
end
|
||||
projects.to_a
|
||||
end
|
||||
|
||||
def project_used(project)
|
||||
return if project.blank? || project.id.blank?
|
||||
|
||||
id_array = recently_used_project_ids
|
||||
id_array.reject!{ |i| i == project.id }
|
||||
# we dont want bookmarks in the recently used list:
|
||||
id_array.unshift(project.id) unless bookmark?(project)
|
||||
self.recently_used_project_ids = id_array[0, recent_projects_count]
|
||||
end
|
||||
|
||||
def bookmark_project(project)
|
||||
self.recently_used_project_ids = recently_used_project_ids.reject{|id| id == project.id}
|
||||
self.bookmarked_project_ids = (bookmarked_project_ids << project.id)
|
||||
end
|
||||
|
||||
def delete_project_bookmark(project)
|
||||
self.bookmarked_project_ids = bookmarked_project_ids.reject do |id|
|
||||
id == project.id
|
||||
end
|
||||
end
|
||||
|
||||
def bookmark?(project)
|
||||
project && project.id && bookmarked_project_ids.include?(project.id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def bookmarked_project_ids
|
||||
pref_project_ids :bookmarked_project_ids
|
||||
end
|
||||
|
||||
def bookmarked_project_ids=(new_ids)
|
||||
set_pref_project_ids :bookmarked_project_ids, new_ids
|
||||
end
|
||||
|
||||
def recently_used_project_ids
|
||||
pref_project_ids(:recently_used_project_ids)[0,recent_projects_count]
|
||||
end
|
||||
|
||||
def recently_used_project_ids=(new_ids)
|
||||
set_pref_project_ids :recently_used_project_ids, new_ids
|
||||
end
|
||||
|
||||
def pref_project_ids(key)
|
||||
return [] unless @user.logged?
|
||||
|
||||
@pref_project_ids[key] ||= (@user.pref[key] || '').split(',').map(&:to_i)
|
||||
end
|
||||
|
||||
def set_pref_project_ids(key, new_values)
|
||||
return nil unless @user.logged?
|
||||
|
||||
old_value = @user.pref[key]
|
||||
new_value = new_values.uniq.join(',')
|
||||
if old_value != new_value
|
||||
@user.pref[key] = new_value
|
||||
@user.pref.save
|
||||
end
|
||||
@pref_project_ids.delete key
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
BIN
public/images/tag_blue_add.png
Normal file
BIN
public/images/tag_blue_add.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 671 B |
BIN
public/images/tag_blue_delete.png
Normal file
BIN
public/images/tag_blue_delete.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 701 B |
@@ -1497,6 +1497,8 @@ td.gantt_selected_column .gantt_hdr,.gantt_selected_column_container {
|
||||
.icon-add-bullet { background-image: url(../images/bullet_add.png); }
|
||||
.icon-shared { background-image: url(../images/link.png); }
|
||||
.icon-actions { background-image: url(../images/3_bullets.png); }
|
||||
.icon-bookmark { background-image: url(../images/tag_blue_delete.png); }
|
||||
.icon-bookmark-off { background-image: url(../images/tag_blue_add.png); }
|
||||
|
||||
.icon-file { background-image: url(../images/files/default.png); }
|
||||
.icon-file.text-plain { background-image: url(../images/files/text.png); }
|
||||
|
||||
@@ -1000,6 +1000,27 @@ class ProjectsControllerTest < Redmine::ControllerTest
|
||||
assert_select_error /Identifier cannot be blank/
|
||||
end
|
||||
|
||||
def test_bookmark_should_create_bookmark
|
||||
@request.session[:user_id] = 3
|
||||
post :bookmark, params: { id: 'ecookbook' }
|
||||
assert_redirected_to controller: 'projects', action: 'show', id: 'ecookbook'
|
||||
jb = Redmine::ProjectJumpBox.new(User.find(3))
|
||||
assert jb.bookmark?(Project.find('ecookbook'))
|
||||
refute jb.bookmark?(Project.find('onlinestore'))
|
||||
end
|
||||
|
||||
def test_bookmark_should_delete_bookmark
|
||||
@request.session[:user_id] = 3
|
||||
jb = Redmine::ProjectJumpBox.new(User.find(3))
|
||||
project = Project.find('ecookbook')
|
||||
jb.bookmark_project project
|
||||
delete :bookmark, params: { id: 'ecookbook' }
|
||||
assert_redirected_to controller: 'projects', action: 'show', id: 'ecookbook'
|
||||
|
||||
jb = Redmine::ProjectJumpBox.new(User.find(3))
|
||||
refute jb.bookmark?(Project.find('ecookbook'))
|
||||
end
|
||||
|
||||
def test_jump_without_project_id_should_redirect_to_active_tab
|
||||
get :index, :params => {
|
||||
:jump => 'issues'
|
||||
|
||||
126
test/unit/lib/redmine/project_jump_box_test.rb
Normal file
126
test/unit/lib/redmine/project_jump_box_test.rb
Normal file
@@ -0,0 +1,126 @@
|
||||
require File.expand_path('../../../../test_helper', __FILE__)
|
||||
|
||||
class Redmine::ProjectJumpBoxTest < ActiveSupport::TestCase
|
||||
fixtures :users, :projects, :user_preferences
|
||||
|
||||
def setup
|
||||
@user = User.find_by_login 'dlopper'
|
||||
@ecookbook = Project.find 'ecookbook'
|
||||
@onlinestore = Project.find 'onlinestore'
|
||||
end
|
||||
|
||||
def test_should_filter_bookmarked_projects
|
||||
pjb = Redmine::ProjectJumpBox.new @user
|
||||
pjb.bookmark_project @ecookbook
|
||||
|
||||
assert_equal 1, pjb.bookmarked_projects.size
|
||||
assert_equal 0, pjb.bookmarked_projects('online').size
|
||||
assert_equal 1, pjb.bookmarked_projects('ecook').size
|
||||
end
|
||||
|
||||
def test_should_not_include_bookmark_in_recently_used_list
|
||||
pjb = Redmine::ProjectJumpBox.new @user
|
||||
pjb.project_used @ecookbook
|
||||
|
||||
assert_equal 1, pjb.recently_used_projects.size
|
||||
|
||||
pjb.bookmark_project @ecookbook
|
||||
assert_equal 0, pjb.recently_used_projects.size
|
||||
end
|
||||
|
||||
def test_should_filter_recently_used_projects
|
||||
pjb = Redmine::ProjectJumpBox.new @user
|
||||
pjb.project_used @ecookbook
|
||||
|
||||
assert_equal 1, pjb.recently_used_projects.size
|
||||
assert_equal 0, pjb.recently_used_projects('online').size
|
||||
assert_equal 1, pjb.recently_used_projects('ecook').size
|
||||
end
|
||||
|
||||
def test_should_limit_recently_used_projects
|
||||
pjb = Redmine::ProjectJumpBox.new @user
|
||||
pjb.project_used @ecookbook
|
||||
pjb.project_used Project.find 'onlinestore'
|
||||
|
||||
@user.pref.recently_used_projects = 1
|
||||
|
||||
assert_equal 1, pjb.recently_used_projects.size
|
||||
assert_equal 1, pjb.recently_used_projects('online').size
|
||||
assert_equal 0, pjb.recently_used_projects('ecook').size
|
||||
end
|
||||
|
||||
def test_should_record_recently_used_projects_order
|
||||
pjb = Redmine::ProjectJumpBox.new @user
|
||||
other = Project.find 'onlinestore'
|
||||
pjb.project_used @ecookbook
|
||||
pjb.project_used other
|
||||
|
||||
pjb = Redmine::ProjectJumpBox.new @user
|
||||
assert_equal 2, pjb.recently_used_projects.size
|
||||
assert_equal [other, @ecookbook], pjb.recently_used_projects
|
||||
|
||||
pjb.project_used other
|
||||
|
||||
pjb = Redmine::ProjectJumpBox.new @user
|
||||
assert_equal 2, pjb.recently_used_projects.size
|
||||
assert_equal [other, @ecookbook], pjb.recently_used_projects
|
||||
|
||||
pjb.project_used @ecookbook
|
||||
pjb = Redmine::ProjectJumpBox.new @user
|
||||
assert_equal 2, pjb.recently_used_projects.size
|
||||
assert_equal [@ecookbook, other], pjb.recently_used_projects
|
||||
end
|
||||
|
||||
def test_should_unbookmark_project
|
||||
pjb = Redmine::ProjectJumpBox.new @user
|
||||
assert pjb.bookmarked_projects.blank?
|
||||
|
||||
# same instance should reflect new data
|
||||
pjb.bookmark_project @ecookbook
|
||||
assert pjb.bookmark?(@ecookbook)
|
||||
refute pjb.bookmark?(@onlinestore)
|
||||
assert_equal 1, pjb.bookmarked_projects.size
|
||||
assert_equal @ecookbook, pjb.bookmarked_projects.first
|
||||
|
||||
# new instance should reflect new data as well
|
||||
pjb = Redmine::ProjectJumpBox.new @user
|
||||
assert pjb.bookmark?(@ecookbook)
|
||||
refute pjb.bookmark?(@onlinestore)
|
||||
assert_equal 1, pjb.bookmarked_projects.size
|
||||
assert_equal @ecookbook, pjb.bookmarked_projects.first
|
||||
|
||||
pjb.bookmark_project @ecookbook
|
||||
pjb = Redmine::ProjectJumpBox.new @user
|
||||
assert_equal 1, pjb.bookmarked_projects.size
|
||||
assert_equal @ecookbook, pjb.bookmarked_projects.first
|
||||
|
||||
pjb.delete_project_bookmark @onlinestore
|
||||
pjb = Redmine::ProjectJumpBox.new @user
|
||||
assert_equal 1, pjb.bookmarked_projects.size
|
||||
assert_equal @ecookbook, pjb.bookmarked_projects.first
|
||||
|
||||
pjb.delete_project_bookmark @ecookbook
|
||||
pjb = Redmine::ProjectJumpBox.new @user
|
||||
assert pjb.bookmarked_projects.blank?
|
||||
end
|
||||
|
||||
def test_should_update_recents_list
|
||||
pjb = Redmine::ProjectJumpBox.new @user
|
||||
assert pjb.recently_used_projects.blank?
|
||||
|
||||
pjb.project_used @ecookbook
|
||||
pjb = Redmine::ProjectJumpBox.new @user
|
||||
assert_equal 1, pjb.recently_used_projects.size
|
||||
assert_equal @ecookbook, pjb.recently_used_projects.first
|
||||
|
||||
pjb.project_used @ecookbook
|
||||
pjb = Redmine::ProjectJumpBox.new @user
|
||||
assert_equal 1, pjb.recently_used_projects.size
|
||||
assert_equal @ecookbook, pjb.recently_used_projects.first
|
||||
|
||||
pjb.project_used @onlinestore
|
||||
assert_equal 2, pjb.recently_used_projects.size
|
||||
assert_equal @onlinestore, pjb.recently_used_projects.first
|
||||
assert_equal @ecookbook, pjb.recently_used_projects.last
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user