Project

General

Profile

Feature #31355 » 0001-adds-favorites-and-recently-used-projects-lists-to-p.patch

Jens Krämer, 2019-05-13 09:32

View differences:

app/controllers/application_controller.rb
54 54
  end
55 55

  
56 56
  before_action :session_expiration, :user_setup, :check_if_login_required, :set_localization, :check_password_change
57
  after_action :record_project_usage
57 58

  
58 59
  rescue_from ::Unauthorized, :with => :deny_access
59 60
  rescue_from ::ActionView::MissingTemplate, :with => :missing_template
......
400 401
    end
401 402
  end
402 403

  
404
  def record_project_usage
405
    if @project && @project.id && User.current.logged? && User.current.allowed_to?(:view_project, @project)
406
      Redmine::ProjectJumpBox.new(User.current).project_used(@project)
407
    end
408
    true
409
  end
410

  
403 411
  def back_url
404 412
    url = params[:back_url]
405 413
    if url.nil? && referer = request.env['HTTP_REFERER']
app/controllers/projects_controller.rb
221 221
    redirect_to_referer_or admin_projects_path(:status => params[:status])
222 222
  end
223 223

  
224
  def bookmark
225
    jump_box = Redmine::ProjectJumpBox.new User.current
226
    if request.delete?
227
      jump_box.delete_project_bookmark @project
228
    elsif request.post?
229
      jump_box.bookmark_project @project
230
    end
231
    respond_to do |format|
232
      format.js
233
      format.html { redirect_to project_path(@project) }
234
    end
235
  end
236

  
224 237
  def close
225 238
    @project.close
226 239
    redirect_to project_path(@project)
app/helpers/application_helper.rb
426 426
  end
427 427

  
428 428
  def render_projects_for_jump_box(projects, selected=nil)
429
    jump_box = Redmine::ProjectJumpBox.new User.current
430
    bookmarked = jump_box.bookmarked_projects(params[:q])
431
    recents = jump_box.recently_used_projects(params[:q])
432
    projects = projects - (recents + bookmarked)
433

  
434
    projects_label = (bookmarked.any? || recents.any?) ? :label_optgroup_others : :label_project_plural
435

  
429 436
    jump = params[:jump].presence || current_menu_item
430 437
    s = (+'').html_safe
431
    project_tree(projects) do |project, level|
438

  
439
    build_project_link = ->(project, level = 0){
432 440
      padding = level * 16
433 441
      text = content_tag('span', project.name, :style => "padding-left:#{padding}px;")
434 442
      s << link_to(text, project_path(project, :jump => jump), :title => project.name, :class => (project == selected ? 'selected' : nil))
443
    }
444

  
445
    [
446
      [bookmarked, :label_optgroup_bookmarks, true],
447
      [recents,   :label_optgroup_recents,    false],
448
      [projects,  projects_label,             true]
449
    ].each do |projects, label, is_tree|
450

  
451
      next if projects.blank?
452

  
453
      s << content_tag(:strong, l(label))
454
      if is_tree
455
        project_tree(projects, &build_project_link)
456
      else
457
        # we do not want to render recently used projects as a tree, but in the
458
        # order they were used (most recent first)
459
        projects.each(&build_project_link)
460
      end
435 461
    end
436 462
    s
437 463
  end
app/helpers/projects_helper.rb
138 138
      end
139 139
    end if include_in_api_response?('enabled_modules')
140 140
  end
141

  
142
  def bookmark_link(project, user = User.current)
143
    return '' unless user && user.logged?
144
    @jump_box ||= Redmine::ProjectJumpBox.new user
145
    bookmarked = @jump_box.bookmark?(project)
146
    css = +"icon bookmark "
147

  
148
    if bookmarked
149
      css << "icon-bookmark"
150
      method = "delete"
151
      text = l(:button_project_bookmark_delete)
152
    else
153
      css << "icon-bookmark-off"
154
      method = "post"
155
      text = l(:button_project_bookmark)
156
    end
157

  
158
    url = bookmark_project_url(project)
159
    link_to text, url, remote: true, method: method, class: css
160
  end
141 161
end
app/models/user_preference.rb
32 32
    'comments_sorting',
33 33
    'warn_on_leaving_unsaved',
34 34
    'no_self_notified',
35
    'textarea_font'
35
    'textarea_font',
36
    'recently_used_projects'
36 37

  
37 38
  TEXTAREA_FONT_OPTIONS = ['monospace', 'proportional']
38 39

  
......
90 91
  def textarea_font; self[:textarea_font] end
91 92
  def textarea_font=(value); self[:textarea_font]=value; end
92 93

  
94
  def recently_used_projects; (self[:recently_used_projects] || 3).to_i; end
95
  def recently_used_projects=(value); self[:recently_used_projects] = value.to_i; end
96

  
93 97
  # Returns the names of groups that are displayed on user's page
94 98
  # Example:
95 99
  #   preferences.my_page_groups
app/views/projects/bookmark.js.erb
1
$('#project-jump div.drdn-items.projects').html('<%= j render_projects_for_jump_box(projects_for_jump_box(User.current), @project) %>');
2
$('.contextual a.icon.bookmark').replaceWith('<%= j bookmark_link @project %>');
app/views/projects/show.html.erb
2 2
  <% if User.current.allowed_to?(:add_subprojects, @project) %>
3 3
    <%= link_to l(:label_subproject_new), new_project_path(:parent_id => @project), :class => 'icon icon-add' %>
4 4
  <% end %>
5
  <%= bookmark_link @project %>
5 6
  <% if User.current.allowed_to?(:close_project, @project) %>
6 7
    <% if @project.active? %>
7 8
      <%= link_to l(:button_close), close_project_path(@project), :data => {:confirm => l(:text_are_you_sure)}, :method => :post, :class => 'icon icon-lock' %>
app/views/users/_preferences.html.erb
4 4
<p><%= pref_fields.select :comments_sorting, [[l(:label_chronological_order), 'asc'], [l(:label_reverse_chronological_order), 'desc']] %></p>
5 5
<p><%= pref_fields.check_box :warn_on_leaving_unsaved %></p>
6 6
<p><%= pref_fields.select :textarea_font, textarea_font_options %></p>
7
<p><%= pref_fields.text_field :recently_used_projects, :size => 2 %></p>
7 8
<% end %>
config/locales/en.yml
382 382
  field_full_width_layout: Full width layout
383 383
  field_digest: Checksum
384 384
  field_default_assigned_to: Default assignee
385
  field_recently_used_projects: Number of recently used projects in jump box
385 386

  
386 387
  setting_app_title: Application title
387 388
  setting_welcome_text: Welcome text
......
1044 1045
  label_font_default: Default font
1045 1046
  label_font_monospace: Monospaced font
1046 1047
  label_font_proportional: Proportional font
1048
  label_optgroup_bookmarks: Bookmarks
1049
  label_optgroup_others: Other projects
1050
  label_optgroup_recents: Recently used
1047 1051
  label_last_notes: Last notes
1048 1052
  label_nothing_to_preview: Nothing to preview
1049 1053
  label_inherited_from_parent_project: "Inherited from parent project"
......
1106 1110
  button_close: Close
1107 1111
  button_reopen: Reopen
1108 1112
  button_import: Import
1113
  button_project_bookmark: Add bookmark
1114
  button_project_bookmark_delete: Remove bookmark
1109 1115
  button_filter: Filter
1110 1116
  button_actions: Actions
1111 1117

  
config/routes.rb
113 113
      post 'close'
114 114
      post 'reopen'
115 115
      match 'copy', :via => [:get, :post]
116
      match 'bookmark', :via => [:delete, :post]
116 117
    end
117 118

  
118 119
    shallow do
lib/redmine.rb
76 76

  
77 77
# Permissions
78 78
Redmine::AccessControl.map do |map|
79
  map.permission :view_project, {:projects => [:show], :activities => [:index]}, :public => true, :read => true
79
  map.permission :view_project, {:projects => [:show, :bookmark], :activities => [:index]}, :public => true, :read => true
80 80
  map.permission :search_project, {:search => :index}, :public => true, :read => true
81 81
  map.permission :add_project, {:projects => [:new, :create]}, :require => :loggedin
82 82
  map.permission :edit_project, {:projects => [:settings, :edit, :update]}, :require => :member
lib/redmine/project_jump_box.rb
1
module Redmine
2
  class ProjectJumpBox
3
    def initialize(user)
4
      @user = user
5
      @pref_project_ids = {}
6
    end
7

  
8
    def recent_projects_count
9
      @user.pref.recently_used_projects
10
    end
11

  
12
    def recently_used_projects(query = nil)
13
      project_ids = recently_used_project_ids
14
      projects = Project.where(id: project_ids)
15
      if query
16
        projects = projects.like(query)
17
      end
18
      projects.
19
        index_by(&:id).
20
        values_at(*project_ids). # sort according to stored order
21
        compact
22
    end
23

  
24
    def bookmarked_projects(query = nil)
25
      projects = Project.where(id: bookmarked_project_ids).visible
26
      if query
27
        projects = projects.like(query)
28
      end
29
      projects.to_a
30
    end
31

  
32
    def project_used(project)
33
      return if project.blank? || project.id.blank?
34

  
35
      id_array = recently_used_project_ids
36
      id_array.reject!{ |i| i == project.id }
37
      # we dont want bookmarks in the recently used list:
38
      id_array.unshift(project.id) unless bookmark?(project)
39
      self.recently_used_project_ids = id_array[0, recent_projects_count]
40
    end
41

  
42
    def bookmark_project(project)
43
      self.recently_used_project_ids = recently_used_project_ids.reject{|id| id == project.id}
44
      self.bookmarked_project_ids = (bookmarked_project_ids << project.id)
45
    end
46

  
47
    def delete_project_bookmark(project)
48
      self.bookmarked_project_ids = bookmarked_project_ids.reject do |id|
49
        id == project.id
50
      end
51
    end
52

  
53
    def bookmark?(project)
54
      project && project.id && bookmarked_project_ids.include?(project.id)
55
    end
56

  
57
    private
58

  
59
    def bookmarked_project_ids
60
      pref_project_ids :bookmarked_project_ids
61
    end
62

  
63
    def bookmarked_project_ids=(new_ids)
64
      set_pref_project_ids :bookmarked_project_ids, new_ids
65
    end
66

  
67
    def recently_used_project_ids
68
      pref_project_ids(:recently_used_project_ids)[0,recent_projects_count]
69
    end
70

  
71
    def recently_used_project_ids=(new_ids)
72
      set_pref_project_ids :recently_used_project_ids, new_ids
73
    end
74

  
75
    def pref_project_ids(key)
76
      return [] unless @user.logged?
77

  
78
      @pref_project_ids[key] ||= (@user.pref[key] || '').split(',').map(&:to_i)
79
    end
80

  
81
    def set_pref_project_ids(key, new_values)
82
      return nil unless @user.logged?
83

  
84
      old_value = @user.pref[key]
85
      new_value = new_values.uniq.join(',')
86
      if old_value != new_value
87
        @user.pref[key] = new_value
88
        @user.pref.save
89
      end
90
      @pref_project_ids.delete key
91
      nil
92
    end
93
  end
94
end
public/stylesheets/application.css
1434 1434
.icon-add-bullet { background-image: url(../images/bullet_add.png); }
1435 1435
.icon-shared { background-image: url(../images/link.png); }
1436 1436
.icon-actions { background-image: url(../images/3_bullets.png); }
1437
.icon-bookmark { background-image: url(../images/tag_blue_delete.png); }
1438
.icon-bookmark-off { background-image: url(../images/tag_blue_add.png); }
1437 1439

  
1438 1440
.icon-file { background-image: url(../images/files/default.png); }
1439 1441
.icon-file.text-plain { background-image: url(../images/files/text.png); }
test/functional/projects_controller_test.rb
1000 1000
    assert_select_error /Identifier cannot be blank/
1001 1001
  end
1002 1002

  
1003
  def test_bookmark_should_create_bookmark
1004
    @request.session[:user_id] = 3
1005
    post :bookmark, params: { id: 'ecookbook' }
1006
    assert_redirected_to controller: 'projects', action: 'show', id: 'ecookbook'
1007
    jb = Redmine::ProjectJumpBox.new(User.find(3))
1008
    assert jb.bookmark?(Project.find('ecookbook'))
1009
    refute jb.bookmark?(Project.find('onlinestore'))
1010
  end
1011

  
1012
  def test_bookmark_should_delete_bookmark
1013
    @request.session[:user_id] = 3
1014
    jb = Redmine::ProjectJumpBox.new(User.find(3))
1015
    project = Project.find('ecookbook')
1016
    jb.bookmark_project project
1017
    delete :bookmark, params: { id: 'ecookbook' }
1018
    assert_redirected_to controller: 'projects', action: 'show', id: 'ecookbook'
1019

  
1020
    jb = Redmine::ProjectJumpBox.new(User.find(3))
1021
    refute jb.bookmark?(Project.find('ecookbook'))
1022
  end
1023

  
1003 1024
  def test_jump_without_project_id_should_redirect_to_active_tab
1004 1025
    get :index, :params => {
1005 1026
        :jump => 'issues'
test/unit/lib/redmine/project_jump_box_test.rb
1
require File.expand_path('../../../../test_helper', __FILE__)
2

  
3
class Redmine::ProjectJumpBoxTest < ActiveSupport::TestCase
4
  fixtures :users, :projects, :user_preferences
5

  
6
  def setup
7
    @user = User.find_by_login 'dlopper'
8
    @ecookbook = Project.find 'ecookbook'
9
    @onlinestore = Project.find 'onlinestore'
10
  end
11

  
12
  def test_should_filter_bookmarked_projects
13
    pjb = Redmine::ProjectJumpBox.new @user
14
    pjb.bookmark_project @ecookbook
15

  
16
    assert_equal 1, pjb.bookmarked_projects.size
17
    assert_equal 0, pjb.bookmarked_projects('online').size
18
    assert_equal 1, pjb.bookmarked_projects('ecook').size
19
  end
20

  
21
  def test_should_not_include_bookmark_in_recently_used_list
22
    pjb = Redmine::ProjectJumpBox.new @user
23
    pjb.project_used @ecookbook
24

  
25
    assert_equal 1, pjb.recently_used_projects.size
26

  
27
    pjb.bookmark_project @ecookbook
28
    assert_equal 0, pjb.recently_used_projects.size
29
  end
30

  
31
  def test_should_filter_recently_used_projects
32
    pjb = Redmine::ProjectJumpBox.new @user
33
    pjb.project_used @ecookbook
34

  
35
    assert_equal 1, pjb.recently_used_projects.size
36
    assert_equal 0, pjb.recently_used_projects('online').size
37
    assert_equal 1, pjb.recently_used_projects('ecook').size
38
  end
39

  
40
  def test_should_limit_recently_used_projects
41
    pjb = Redmine::ProjectJumpBox.new @user
42
    pjb.project_used @ecookbook
43
    pjb.project_used Project.find 'onlinestore'
44

  
45
    @user.pref.recently_used_projects = 1
46

  
47
    assert_equal 1, pjb.recently_used_projects.size
48
    assert_equal 1, pjb.recently_used_projects('online').size
49
    assert_equal 0, pjb.recently_used_projects('ecook').size
50
  end
51

  
52
  def test_should_record_recently_used_projects_order
53
    pjb = Redmine::ProjectJumpBox.new @user
54
    other = Project.find 'onlinestore'
55
    pjb.project_used @ecookbook
56
    pjb.project_used other
57

  
58
    pjb = Redmine::ProjectJumpBox.new @user
59
    assert_equal 2, pjb.recently_used_projects.size
60
    assert_equal [other, @ecookbook], pjb.recently_used_projects
61

  
62
    pjb.project_used other
63

  
64
    pjb = Redmine::ProjectJumpBox.new @user
65
    assert_equal 2, pjb.recently_used_projects.size
66
    assert_equal [other, @ecookbook], pjb.recently_used_projects
67

  
68
    pjb.project_used @ecookbook
69
    pjb = Redmine::ProjectJumpBox.new @user
70
    assert_equal 2, pjb.recently_used_projects.size
71
    assert_equal [@ecookbook, other], pjb.recently_used_projects
72
  end
73

  
74
  def test_should_unbookmark_project
75
    pjb = Redmine::ProjectJumpBox.new @user
76
    assert pjb.bookmarked_projects.blank?
77

  
78
    # same instance should reflect new data
79
    pjb.bookmark_project @ecookbook
80
    assert pjb.bookmark?(@ecookbook)
81
    refute pjb.bookmark?(@onlinestore)
82
    assert_equal 1, pjb.bookmarked_projects.size
83
    assert_equal @ecookbook, pjb.bookmarked_projects.first
84

  
85
    # new instance should reflect new data as well
86
    pjb = Redmine::ProjectJumpBox.new @user
87
    assert pjb.bookmark?(@ecookbook)
88
    refute pjb.bookmark?(@onlinestore)
89
    assert_equal 1, pjb.bookmarked_projects.size
90
    assert_equal @ecookbook, pjb.bookmarked_projects.first
91

  
92
    pjb.bookmark_project @ecookbook
93
    pjb = Redmine::ProjectJumpBox.new @user
94
    assert_equal 1, pjb.bookmarked_projects.size
95
    assert_equal @ecookbook, pjb.bookmarked_projects.first
96

  
97
    pjb.delete_project_bookmark @onlinestore
98
    pjb = Redmine::ProjectJumpBox.new @user
99
    assert_equal 1, pjb.bookmarked_projects.size
100
    assert_equal @ecookbook, pjb.bookmarked_projects.first
101

  
102
    pjb.delete_project_bookmark @ecookbook
103
    pjb = Redmine::ProjectJumpBox.new @user
104
    assert pjb.bookmarked_projects.blank?
105
  end
106

  
107
  def test_should_update_recents_list
108
    pjb = Redmine::ProjectJumpBox.new @user
109
    assert pjb.recently_used_projects.blank?
110

  
111
    pjb.project_used @ecookbook
112
    pjb = Redmine::ProjectJumpBox.new @user
113
    assert_equal 1, pjb.recently_used_projects.size
114
    assert_equal @ecookbook, pjb.recently_used_projects.first
115

  
116
    pjb.project_used @ecookbook
117
    pjb = Redmine::ProjectJumpBox.new @user
118
    assert_equal 1, pjb.recently_used_projects.size
119
    assert_equal @ecookbook, pjb.recently_used_projects.first
120

  
121
    pjb.project_used @onlinestore
122
    assert_equal 2, pjb.recently_used_projects.size
123
    assert_equal @onlinestore, pjb.recently_used_projects.first
124
    assert_equal @ecookbook, pjb.recently_used_projects.last
125
  end
126
end
(1-1/5)