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:
Go MAEDA
2019-05-20 22:26:30 +00:00
parent da78e654f1
commit 29403e710a
17 changed files with 328 additions and 3 deletions

View File

@@ -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']

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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 %>');

View File

@@ -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' %>

View File

@@ -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 %>

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 B

View File

@@ -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); }

View File

@@ -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'

View 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