summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGo MAEDA <maeda@farend.jp>2019-05-20 22:26:30 +0000
committerGo MAEDA <maeda@farend.jp>2019-05-20 22:26:30 +0000
commit29403e710a8d0d83c81387f236082f209f3ba7ed (patch)
tree5e396ddcc22d5f7f93b75970340e0197a145f825
parentda78e654f13b9b13e1584bac91dae9631c12f63e (diff)
downloadredmine-29403e710a8d0d83c81387f236082f209f3ba7ed.tar.gz
redmine-29403e710a8d0d83c81387f236082f209f3ba7ed.zip
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
-rw-r--r--app/controllers/application_controller.rb8
-rw-r--r--app/controllers/projects_controller.rb13
-rw-r--r--app/helpers/application_helper.rb28
-rw-r--r--app/helpers/projects_helper.rb20
-rw-r--r--app/models/user_preference.rb6
-rw-r--r--app/views/projects/bookmark.js.erb2
-rw-r--r--app/views/projects/show.html.erb1
-rw-r--r--app/views/users/_preferences.html.erb1
-rw-r--r--config/locales/en.yml6
-rw-r--r--config/routes.rb1
-rw-r--r--lib/redmine.rb2
-rw-r--r--lib/redmine/project_jump_box.rb94
-rw-r--r--public/images/tag_blue_add.pngbin0 -> 671 bytes
-rw-r--r--public/images/tag_blue_delete.pngbin0 -> 701 bytes
-rw-r--r--public/stylesheets/application.css2
-rw-r--r--test/functional/projects_controller_test.rb21
-rw-r--r--test/unit/lib/redmine/project_jump_box_test.rb126
17 files changed, 328 insertions, 3 deletions
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 082bab939..0aaf54b66 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -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']
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 1f0aba53f..85949216a 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -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)
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 7ae2c2d88..07e1c9fb7 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -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
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 5e91026e6..814eb0525 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -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
diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb
index d55b8ac58..7cb9b0ca6 100644
--- a/app/models/user_preference.rb
+++ b/app/models/user_preference.rb
@@ -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
diff --git a/app/views/projects/bookmark.js.erb b/app/views/projects/bookmark.js.erb
new file mode 100644
index 000000000..559585c16
--- /dev/null
+++ b/app/views/projects/bookmark.js.erb
@@ -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 %>');
diff --git a/app/views/projects/show.html.erb b/app/views/projects/show.html.erb
index 16645b759..06eca9970 100644
--- a/app/views/projects/show.html.erb
+++ b/app/views/projects/show.html.erb
@@ -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' %>
diff --git a/app/views/users/_preferences.html.erb b/app/views/users/_preferences.html.erb
index f8769125e..6c4e5337a 100644
--- a/app/views/users/_preferences.html.erb
+++ b/app/views/users/_preferences.html.erb
@@ -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 %>
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 813c00b93..5ade5a43c 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -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
diff --git a/config/routes.rb b/config/routes.rb
index 3c1fd0256..74257d3ea 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -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
diff --git a/lib/redmine.rb b/lib/redmine.rb
index 2de351c07..805418d3d 100644
--- a/lib/redmine.rb
+++ b/lib/redmine.rb
@@ -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
diff --git a/lib/redmine/project_jump_box.rb b/lib/redmine/project_jump_box.rb
new file mode 100644
index 000000000..5978dc19a
--- /dev/null
+++ b/lib/redmine/project_jump_box.rb
@@ -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
diff --git a/public/images/tag_blue_add.png b/public/images/tag_blue_add.png
new file mode 100644
index 000000000..f135248f8
--- /dev/null
+++ b/public/images/tag_blue_add.png
Binary files differ
diff --git a/public/images/tag_blue_delete.png b/public/images/tag_blue_delete.png
new file mode 100644
index 000000000..9fbae6725
--- /dev/null
+++ b/public/images/tag_blue_delete.png
Binary files differ
diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css
index 3950c3e05..03fd81441 100644
--- a/public/stylesheets/application.css
+++ b/public/stylesheets/application.css
@@ -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); }
diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb
index e1a17059b..508efa202 100644
--- a/test/functional/projects_controller_test.rb
+++ b/test/functional/projects_controller_test.rb
@@ -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'
diff --git a/test/unit/lib/redmine/project_jump_box_test.rb b/test/unit/lib/redmine/project_jump_box_test.rb
new file mode 100644
index 000000000..375375556
--- /dev/null
+++ b/test/unit/lib/redmine/project_jump_box_test.rb
@@ -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