]> source.dussan.org Git - redmine.git/commitdiff
Adds favorites and recently used projects lists to project jump box (#31355).
authorGo MAEDA <maeda@farend.jp>
Mon, 20 May 2019 22:26:30 +0000 (22:26 +0000)
committerGo MAEDA <maeda@farend.jp>
Mon, 20 May 2019 22:26:30 +0000 (22:26 +0000)
Patch by Jens Krämer.

git-svn-id: http://svn.redmine.org/redmine/trunk@18181 e93f8b46-1217-0410-a6f0-8f06a7374b81

17 files changed:
app/controllers/application_controller.rb
app/controllers/projects_controller.rb
app/helpers/application_helper.rb
app/helpers/projects_helper.rb
app/models/user_preference.rb
app/views/projects/bookmark.js.erb [new file with mode: 0644]
app/views/projects/show.html.erb
app/views/users/_preferences.html.erb
config/locales/en.yml
config/routes.rb
lib/redmine.rb
lib/redmine/project_jump_box.rb [new file with mode: 0644]
public/images/tag_blue_add.png [new file with mode: 0644]
public/images/tag_blue_delete.png [new file with mode: 0644]
public/stylesheets/application.css
test/functional/projects_controller_test.rb
test/unit/lib/redmine/project_jump_box_test.rb [new file with mode: 0644]

index 082bab93923baeef062e999c0938ef2713310fef..0aaf54b66767ee78c4586c9b00b5e0fadf6155a3 100644 (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']
index 1f0aba53fa8ffa3b12c62a69cc4d51e92b3289f9..85949216abd3270a66eb74149b9a7ac0b55f48b3 100644 (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)
index 7ae2c2d88a6d0011332d4c0252b3fc0c323a3545..07e1c9fb7c27b464a5c7bb63d2e46f98d79561b2 100644 (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
index 5e91026e6135dcca2155da784eed0bdbfa89fb0f..814eb0525aa084cf18d8f9992d98903af29c053a 100644 (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
index d55b8ac58773a8591a94e6fdea17eae10566bf3d..7cb9b0ca68a9965d44315a3ac885f44aab2ad958 100644 (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
diff --git a/app/views/projects/bookmark.js.erb b/app/views/projects/bookmark.js.erb
new file mode 100644 (file)
index 0000000..559585c
--- /dev/null
@@ -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 %>');
index 16645b759776e7b39faddf2f8d5f150d553441f6..06eca997012f9e7ad5623b29640c385358bfbbeb 100644 (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' %>
index f8769125ed8b7657b1e8cb45d5be234506ecdac3..6c4e5337aad5e18c8f7a3e36caa0701d06b41d82 100644 (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 %>
index 813c00b939d20ba515f9e02844482fa820bd9876..5ade5a43cd28f148bccc22ccfd17c232fa225eb6 100644 (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
 
index 3c1fd025614c3689a204b572c2f1dcedd60a3efb..74257d3eab9c162d39f54348b8c2c2ef4fff2d94 100644 (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
index 2de351c07197c8552ff3762f595c149b3edc5ea5..805418d3d908213c0e5ab41b0bc92210521ab837 100644 (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
diff --git a/lib/redmine/project_jump_box.rb b/lib/redmine/project_jump_box.rb
new file mode 100644 (file)
index 0000000..5978dc1
--- /dev/null
@@ -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 (file)
index 0000000..f135248
Binary files /dev/null and b/public/images/tag_blue_add.png differ
diff --git a/public/images/tag_blue_delete.png b/public/images/tag_blue_delete.png
new file mode 100644 (file)
index 0000000..9fbae67
Binary files /dev/null and b/public/images/tag_blue_delete.png differ
index 3950c3e056d980c7fcb39cdfefe34559fccc45b6..03fd81441bca6b95394c5a4e68c750d850f98008 100644 (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); }
index e1a17059bc8b9b5be0cd6b5dae09d177f61367d5..508efa2028ae78840cd49f3d8a3639d64e15f28c 100644 (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'
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 (file)
index 0000000..3753755
--- /dev/null
@@ -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