Project

General

Profile

Feature #31353 » 0001-Improve-top-menu-navigation-31353.patch

Marius BĂLTEANU, 2026-05-25 22:56

View differences:

app/assets/javascripts/responsive.js
55 55
        $('#main-menu > ul').detach().appendTo('.js-project-menu');
56 56
        $('#top-menu > ul').detach().appendTo('.js-general-menu');
57 57
        $('#sidebar > *').detach().appendTo('.js-sidebar');
58
        $('#account > ul').detach().appendTo('.js-profile-menu');
58
        $('#account ul').detach().appendTo('.js-profile-menu');
59 59

  
60 60
        mobileInit = true;
61 61
        desktopInit = false;
......
65 65
    var _initDesktopMenu = function() {
66 66
      if(!desktopInit) {
67 67

  
68
        $('.js-project-menu > ul').detach().appendTo('#main-menu');
69
        $('.js-general-menu > ul').detach().appendTo('#top-menu');
68
        $('.js-project-menu > ul').detach().prependTo('#main-menu');
69
        $('.js-general-menu > ul').detach().prependTo('#top-menu');
70 70
        $('.js-sidebar > *').detach().appendTo('#sidebar');
71
        $('.js-profile-menu > ul').detach().appendTo('#account');
71

  
72
        var accountMenuParent = $('#account .dropdown-content');
73
        if (accountMenuParent.length === 0) {
74
          accountMenuParent = $('#account');
75
        }
76
        $('.js-profile-menu > ul').detach().appendTo(accountMenuParent);
72 77

  
73 78
        desktopInit = true;
74 79
        mobileInit = false;
app/assets/stylesheets/application.css
76 76
  position: relative;
77 77
}
78 78

  
79
#top-menu {
79
nav.top-menu {
80 80
  background: #234761; /* no match in Open Color, using hex code */
81 81
  color: var(--oc-gray-2);
82 82
  font-size: 0.75rem;
83 83
  padding-block: 3px;
84 84
  padding-inline: 20px;
85
  display: flex;
86
  justify-content: space-between;
87
  align-items: center;
85 88
}
86
#top-menu ul {margin: 0;  padding: 0;}
87
#top-menu li {
88
  float:inline-start;
89
  list-style-type:none;
90
  margin-block: 0;
91
  margin-inline: 0 12px;
89

  
90
.general-menu, .profile-menu {
91
  display: flex;
92
  align-items: center;
93
  gap: 8px;
94
}
95

  
96
.top-menu__links ul {
97
  margin: 0;
92 98
  padding: 0;
93
  white-space:nowrap;
99
  display: flex;
100
  list-style-type: none;
94 101
}
95
#top-menu a {
102

  
103
.top-menu__links a, .top-menu__links a:link, .top-menu__links a:visited {
96 104
  color: var(--oc-gray-1);
97 105
  margin-inline-end: 0;
98 106
  text-decoration: none;
99
  transition: color 0.18s ease, opacity 0.18s ease;
107
  transition: background-color 0.18s ease, color 0.18s ease;
108
  padding: 4px;
100 109
}
101
#top-menu a:hover {
102
  color: var(--oc-white);
103
  text-decoration: underline;
104
  text-underline-offset: 0.18em;
110

  
111
.top-menu__links a:hover {
112
    color: var(--oc-white);
113
    background-color: rgba(255, 255, 255, 0.12);
114
    transform: scale(1.08);
115
    border-radius: 3px;
116
    text-decoration: none;
105 117
}
106
#top-menu #loggedas {
107
  float: inline-end;
108
  margin-inline-end: 20px;
109
  color: var(--oc-gray-5);
118

  
119
.top-menu__links svg.icon-svg {
120
  stroke: var(--oc-white);
110 121
}
111
#top-menu #loggedas a {
112
  color: var(--oc-white);
113
  font-weight: bold;
122

  
123
#account .dropdown-content .user-info {
124
  padding-block: 10px;
125
  padding-inline: 16px;
126
  color: var(--oc-gray-9);
114 127
}
115 128

  
116
#account {float:inline-end;}
117
#account li:last-of-type {margin-inline-end: 0;}
129
#account .dropdown-content .user-info .user-name {
130
    font-weight: bold;
131
}
118 132

  
133
#account .dropdown-content .logout {
134
  border-block-start: 1px solid var(--oc-gray-2);
135
}
119 136

  
120 137
#header {
121 138
  margin: 0;
app/assets/stylesheets/dropdown.css
1
.dropdown {
2
  position: relative;
3
  display: flex;
4
  align-items: center;
5
}
6

  
7
.dropdown-trigger {
8
  display: flex;
9
  align-items: center;
10
  cursor: pointer;
11
  padding-block: 2px;
12
  text-decoration: none;
13
}
14

  
15
.dropdown-trigger:hover {
16
  text-decoration: none;
17
}
18

  
19
.dropdown-content {
20
  position: absolute;
21
  inset-block-start: 100%;
22
  inset-inline-end: 0;
23
  background-color: var(--oc-white);
24
  border: 1px solid var(--oc-gray-4);
25
  border-radius: 4px;
26
  box-shadow: 0 4px 6px rgba(var(--oc-black-rgb), 0.1);
27
  z-index: 1000;
28
  min-inline-size: 160px;
29
  margin-block-start: 4px;
30
}
31

  
32
.dropdown-content ul {
33
  list-style: none;
34
  margin: 0;
35
  padding: 0;
36
}
37

  
38
.dropdown-content li {
39
  float: none;
40
  display: block;
41
}
42

  
43
.dropdown-content li a, .dropdown-content .dropdown-items a {
44
  display: block;
45
  padding-block: 8px;
46
  padding-inline: 16px;
47
  color: var(--oc-gray-7);
48
  font-weight: normal;
49
  margin: 0;
50
}
51

  
52
.dropdown-content li a:hover, .dropdown-content .dropdown-items a:hover {
53
  background-color: var(--oc-gray-1);
54
  color: var(--oc-blue-9);
55
  text-decoration: none;
56
}
57

  
58
.dropdown-divider {
59
  border-block-start: 1px solid var(--oc-gray-2);
60
}
app/javascript/controllers/dropdown_controller.js
1
import { Controller } from "@hotwired/stimulus"
2

  
3
export default class extends Controller {
4
  static targets = ["content"]
5

  
6
  connect() {
7
    this.closeBinding = this.close.bind(this)
8
  }
9

  
10
  toggle(event) {
11
    event.preventDefault()
12
    event.stopPropagation()
13
    this.contentTarget.classList.toggle("hidden")
14

  
15
    if (!this.contentTarget.classList.contains("hidden")) {
16
      document.addEventListener("click", this.closeBinding)
17
      document.addEventListener("keydown", this.closeBinding)
18
    } else {
19
      document.removeEventListener("click", this.closeBinding)
20
      document.removeEventListener("keydown", this.closeBinding)
21
    }
22
  }
23

  
24
  close(event) {
25
    if (event.type === "keydown" && event.key !== "Escape") {
26
      return
27
    }
28

  
29
    if (event.type === "click" && this.element.contains(event.target)) {
30
      return
31
    }
32

  
33
    this.contentTarget.classList.add("hidden")
34
    document.removeEventListener("click", this.closeBinding)
35
    document.removeEventListener("keydown", this.closeBinding)
36
  }
37

  
38
  disconnect() {
39
    document.removeEventListener("click", this.closeBinding)
40
    document.removeEventListener("keydown", this.closeBinding)
41
  }
42
}
app/views/layouts/base.html.erb
8 8
<meta name="keywords" content="issue,bug,tracker" />
9 9
<%= csrf_meta_tag %>
10 10
<%= favicon %>
11
<%= stylesheet_link_tag 'jquery/jquery-ui-1.13.2', 'tribute-5.1.3', 'application', 'responsive', :media => 'all' %>
11
<%= stylesheet_link_tag 'jquery/jquery-ui-1.13.2', 'tribute-5.1.3', 'application', 'dropdown', 'responsive', :media => 'all' %>
12 12
<%= javascript_importmap_tags %>
13 13
<%= javascript_heads %>
14 14
<%= heads_for_theme %>
......
56 56

  
57 57
</div>
58 58

  
59

  
60
<div id="top-menu">
61
    <div id="account">
59
<nav class="top-menu">
60
  <div class="general-menu">
61
    <div class="top-menu__links">
62
        <%= render_menu :top_menu if User.current.logged? || !Setting.login_required? -%>
63
    </div>
64
  </div>
65

  
66
  <div class="profile-menu">
67
    <% if User.current.admin? %>
68
      <div class="top-menu__links">
69
        <%= link_to sprite_icon('settings', l(:label_administration), icon_only: true),
70
                    {:controller => 'admin', :action => 'index'},
71
                    :class => 'administration',
72
                    :title => l(:label_administration) %>
73
      </div>
74
    <% end %>
75
    <% if User.current.logged? %>
76
    <div id="account" class="dropdown" data-controller="dropdown">
77
      <a href="#" class="dropdown-trigger" data-action="click->dropdown#toggle">
78
        <%= avatar(User.current, :size => "24") %>
79
      </a>
80
      <div class="dropdown-content hidden" data-dropdown-target="content">
81
        <div class="user-info">
82
          <span class="user-name"><%= User.current.name %></span>
83
          <span class="user-login"><%= "@#{User.current.login}" %></span>
84
        </div>
85
        <div class="dropdown-divider"></div>
62 86
        <%= render_menu :account_menu -%>
87
      </div>
63 88
    </div>
64
    <%= content_tag('div', "#{l(:label_logged_as)} #{link_to_user(User.current, :format => :username)}".html_safe, :id => 'loggedas') if User.current.logged? %>
65
    <%= render_menu :top_menu if User.current.logged? || !Setting.login_required? -%>
66
</div>
89
    <% else %>
90
    <div id="account" class="top-menu__links">
91
        <%= render_menu :account_menu -%>
92
    </div>
93
    <% end %>
94
  </div>
95
</nav>
67 96

  
68 97
<div id="header">
69 98

  
lib/redmine/preparation.rb
170 170
        menu.push :my_page, {:controller => 'my', :action => 'page'},
171 171
                  :if => Proc.new {User.current.logged?}
172 172
        menu.push :projects, {:controller => 'projects', :action => 'index'},
173
                  :caption => :label_project_plural
174
        menu.push :administration, {:controller => 'admin', :action => 'index'},
175
                  :if => Proc.new {User.current.admin?}, :last => true
173
                  :caption => :label_project_plural, :last => true
176 174
        menu.push :help, Info.help_url, :html => {:target => '_blank', :rel => 'noopener'}, :last => true
177 175
      end
178 176

  
......
180 178
        menu.push :login, :signin_path, :if => Proc.new {!User.current.logged?}
181 179
        menu.push :register, :register_path,
182 180
                  :if => Proc.new {!User.current.logged? && Setting.self_registration?}
181
        menu.push :my_profile, {:controller => 'users', :action => 'show', :id => 'current'},
182
                  :if => Proc.new {User.current.logged?},
183
                  :caption => :label_profile,
184
                  :first => true
183 185
        menu.push :my_account, {:controller => 'my', :action => 'account'},
184 186
                  :if => Proc.new {User.current.logged?}
185 187
        menu.push :logout, :signout_path, :html => {:method => 'post'},
(16-16/17)