Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
6e953772
Commit
6e953772
authored
May 17, 2021
by
Paul Slaughter
Committed by
Jose Ivan Vargas
May 17, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Frontend for top nav menu redesign
parent
d02bf43a
Changes
27
Hide whitespace changes
Inline
Side-by-side
Showing
27 changed files
with
939 additions
and
72 deletions
+939
-72
app/assets/javascripts/frequent_items/constants.js
app/assets/javascripts/frequent_items/constants.js
+13
-12
app/assets/javascripts/frequent_items/store/index.js
app/assets/javascripts/frequent_items/store/index.js
+11
-9
app/assets/javascripts/main.js
app/assets/javascripts/main.js
+2
-0
app/assets/javascripts/nav/components/top_nav_app.vue
app/assets/javascripts/nav/components/top_nav_app.vue
+59
-0
app/assets/javascripts/nav/components/top_nav_container_view.vue
...ets/javascripts/nav/components/top_nav_container_view.vue
+74
-0
app/assets/javascripts/nav/components/top_nav_dropdown_menu.vue
...sets/javascripts/nav/components/top_nav_dropdown_menu.vue
+144
-0
app/assets/javascripts/nav/components/top_nav_menu_item.vue
app/assets/javascripts/nav/components/top_nav_menu_item.vue
+31
-0
app/assets/javascripts/nav/index.js
app/assets/javascripts/nav/index.js
+12
-0
app/assets/javascripts/nav/mount.js
app/assets/javascripts/nav/mount.js
+23
-0
app/assets/javascripts/nav/stores/index.js
app/assets/javascripts/nav/stores/index.js
+4
-0
app/assets/stylesheets/framework/dropdowns.scss
app/assets/stylesheets/framework/dropdowns.scss
+47
-41
app/assets/stylesheets/framework/header.scss
app/assets/stylesheets/framework/header.scss
+36
-0
app/assets/stylesheets/framework/variables.scss
app/assets/stylesheets/framework/variables.scss
+2
-0
app/assets/stylesheets/themes/_dark.scss
app/assets/stylesheets/themes/_dark.scss
+2
-0
app/assets/stylesheets/utilities.scss
app/assets/stylesheets/utilities.scss
+18
-0
app/views/layouts/header/_default.html.haml
app/views/layouts/header/_default.html.haml
+1
-1
app/views/layouts/nav/_combined_menu.html.haml
app/views/layouts/nav/_combined_menu.html.haml
+0
-3
app/views/layouts/nav/_top_nav.html.haml
app/views/layouts/nav/_top_nav.html.haml
+7
-0
app/views/layouts/nav/groups_dropdown/_show.html.haml
app/views/layouts/nav/groups_dropdown/_show.html.haml
+1
-1
app/views/layouts/nav/projects_dropdown/_show.html.haml
app/views/layouts/nav/projects_dropdown/_show.html.haml
+1
-1
locale/gitlab.pot
locale/gitlab.pot
+3
-0
spec/features/admin/admin_mode_spec.rb
spec/features/admin/admin_mode_spec.rb
+0
-4
spec/frontend/nav/components/top_nav_app_spec.js
spec/frontend/nav/components/top_nav_app_spec.js
+68
-0
spec/frontend/nav/components/top_nav_container_view_spec.js
spec/frontend/nav/components/top_nav_container_view_spec.js
+114
-0
spec/frontend/nav/components/top_nav_dropdown_menu_spec.js
spec/frontend/nav/components/top_nav_dropdown_menu_spec.js
+157
-0
spec/frontend/nav/components/top_nav_menu_item_spec.js
spec/frontend/nav/components/top_nav_menu_item_spec.js
+74
-0
spec/frontend/nav/mock_data.js
spec/frontend/nav/mock_data.js
+35
-0
No files found.
app/assets/javascripts/frequent_items/constants.js
View file @
6e953772
...
...
@@ -37,15 +37,16 @@ export const TRANSLATION_KEYS = {
},
};
export
const
FREQUENT_ITEMS_DROPDOWNS
=
[
{
namespace
:
'
projects
'
,
key
:
'
project
'
,
vuexModule
:
'
frequentProjects
'
,
},
{
namespace
:
'
groups
'
,
key
:
'
group
'
,
vuexModule
:
'
frequentGroups
'
,
},
];
export
const
FREQUENT_ITEMS_PROJECTS
=
{
namespace
:
'
projects
'
,
key
:
'
project
'
,
vuexModule
:
'
frequentProjects
'
,
};
export
const
FREQUENT_ITEMS_GROUPS
=
{
namespace
:
'
groups
'
,
key
:
'
group
'
,
vuexModule
:
'
frequentGroups
'
,
};
export
const
FREQUENT_ITEMS_DROPDOWNS
=
[
FREQUENT_ITEMS_PROJECTS
,
FREQUENT_ITEMS_GROUPS
];
app/assets/javascripts/frequent_items/store/index.js
View file @
6e953772
...
...
@@ -13,14 +13,16 @@ export const createFrequentItemsModule = (initState = {}) => ({
state
:
state
(
initState
),
});
export
const
createStoreOptions
=
()
=>
({
modules
:
FREQUENT_ITEMS_DROPDOWNS
.
reduce
(
(
acc
,
{
namespace
,
vuexModule
})
=>
Object
.
assign
(
acc
,
{
[
vuexModule
]:
createFrequentItemsModule
({
dropdownType
:
namespace
}),
}),
{},
),
});
export
const
createStore
=
()
=>
{
return
new
Vuex
.
Store
({
modules
:
FREQUENT_ITEMS_DROPDOWNS
.
reduce
(
(
acc
,
{
namespace
,
vuexModule
})
=>
Object
.
assign
(
acc
,
{
[
vuexModule
]:
createFrequentItemsModule
({
dropdownType
:
namespace
}),
}),
{},
),
});
return
new
Vuex
.
Store
(
createStoreOptions
());
};
app/assets/javascripts/main.js
View file @
6e953772
...
...
@@ -35,6 +35,7 @@ import initUsagePingConsent from './usage_ping_consent';
import
GlFieldErrors
from
'
./gl_field_errors
'
;
import
initUserPopovers
from
'
./user_popovers
'
;
import
initBroadcastNotifications
from
'
./broadcast_notification
'
;
import
{
initTopNav
}
from
'
./nav
'
;
import
'
ee_else_ce/main_ee
'
;
...
...
@@ -80,6 +81,7 @@ initRails();
function
deferredInitialisation
()
{
const
$body
=
$
(
'
body
'
);
initTopNav
();
initBreadcrumbs
();
initTodoToggle
();
initLogoAnimation
();
...
...
app/assets/javascripts/nav/components/top_nav_app.vue
0 → 100644
View file @
6e953772
<
script
>
import
{
GlNav
,
GlNavItemDropdown
,
GlDropdownForm
,
GlTooltip
}
from
'
@gitlab/ui
'
;
import
{
s__
}
from
'
~/locale
'
;
import
TopNavDropdownMenu
from
'
./top_nav_dropdown_menu.vue
'
;
const
TOOLTIP
=
s__
(
'
TopNav|Switch to...
'
);
export
default
{
components
:
{
GlNav
,
GlNavItemDropdown
,
GlDropdownForm
,
GlTooltip
,
TopNavDropdownMenu
,
},
props
:
{
navData
:
{
type
:
Object
,
required
:
true
,
},
},
methods
:
{
findTooltipTarget
()
{
// ### Why use a target function instead of `v-gl-tooltip`?
// To get the tooltip to align correctly, we need it to target the actual
// toggle button which we don't directly render.
return
this
.
$el
.
querySelector
(
'
.js-top-nav-dropdown-toggle
'
);
},
},
TOOLTIP
,
};
</
script
>
<
template
>
<gl-nav
class=
"navbar-sub-nav"
>
<gl-nav-item-dropdown
:text=
"navData.activeTitle"
icon=
"dot-grid"
menu-class=
"gl-mt-3! gl-max-w-none! gl-max-h-none! gl-sm-w-auto!"
toggle-class=
"top-nav-toggle js-top-nav-dropdown-toggle gl-px-3!"
no-flip
>
<gl-dropdown-form>
<top-nav-dropdown-menu
:primary=
"navData.primary"
:secondary=
"navData.secondary"
:views=
"navData.views"
/>
</gl-dropdown-form>
</gl-nav-item-dropdown>
<gl-tooltip
boundary=
"window"
:boundary-padding=
"0"
:target=
"findTooltipTarget"
placement=
"right"
:title=
"$options.TOOLTIP"
/>
</gl-nav>
</
template
>
app/assets/javascripts/nav/components/top_nav_container_view.vue
0 → 100644
View file @
6e953772
<
script
>
import
FrequentItemsApp
from
'
~/frequent_items/components/app.vue
'
;
import
eventHub
from
'
~/frequent_items/event_hub
'
;
import
VuexModuleProvider
from
'
~/vue_shared/components/vuex_module_provider.vue
'
;
import
TopNavMenuItem
from
'
./top_nav_menu_item.vue
'
;
export
default
{
components
:
{
FrequentItemsApp
,
TopNavMenuItem
,
VuexModuleProvider
,
},
props
:
{
frequentItemsVuexModule
:
{
type
:
String
,
required
:
true
,
},
frequentItemsDropdownType
:
{
type
:
String
,
required
:
true
,
},
linksPrimary
:
{
type
:
Array
,
required
:
false
,
default
:
()
=>
[],
},
linksSecondary
:
{
type
:
Array
,
required
:
false
,
default
:
()
=>
[],
},
},
computed
:
{
linkGroups
()
{
return
[
{
key
:
'
primary
'
,
links
:
this
.
linksPrimary
},
{
key
:
'
secondary
'
,
links
:
this
.
linksSecondary
},
].
filter
((
x
)
=>
x
.
links
?.
length
);
},
},
mounted
()
{
// For historic reasons, the frequent-items-app component requires this too start up.
this
.
$nextTick
(()
=>
{
eventHub
.
$emit
(
`
${
this
.
frequentItemsDropdownType
}
-dropdownOpen`
);
});
},
};
</
script
>
<
template
>
<div
class=
"top-nav-container-view gl-display-flex gl-flex-direction-column"
>
<div
class=
"frequent-items-dropdown-container gl-w-auto"
>
<div
class=
"frequent-items-dropdown-content gl-w-full! gl-pt-0!"
>
<vuex-module-provider
:vuex-module=
"frequentItemsVuexModule"
>
<frequent-items-app
v-bind=
"$attrs"
/>
</vuex-module-provider>
</div>
</div>
<div
v-for=
"(
{ key, links }, groupIndex) in linkGroups"
:key="key"
:class="{ 'gl-mt-3': groupIndex !== 0 }"
class="gl-mt-auto gl-pt-3 gl-border-1 gl-border-t-solid gl-border-gray-100"
data-testid="menu-item-group"
>
<top-nav-menu-item
v-for=
"(link, linkIndex) in links"
:key=
"link.title"
:menu-item=
"link"
:class=
"
{ 'gl-mt-1': linkIndex !== 0 }"
/>
</div>
</div>
</
template
>
app/assets/javascripts/nav/components/top_nav_dropdown_menu.vue
0 → 100644
View file @
6e953772
<
script
>
import
{
FREQUENT_ITEMS_PROJECTS
,
FREQUENT_ITEMS_GROUPS
}
from
'
~/frequent_items/constants
'
;
import
KeepAliveSlots
from
'
~/vue_shared/components/keep_alive_slots.vue
'
;
import
TopNavContainerView
from
'
./top_nav_container_view.vue
'
;
import
TopNavMenuItem
from
'
./top_nav_menu_item.vue
'
;
const
ACTIVE_CLASS
=
'
gl-shadow-none! gl-font-weight-bold! active
'
;
const
SECONDARY_GROUP_CLASS
=
'
gl-pt-3 gl-mt-3 gl-border-1 gl-border-t-solid gl-border-gray-100
'
;
export
default
{
components
:
{
KeepAliveSlots
,
TopNavContainerView
,
TopNavMenuItem
,
},
props
:
{
primary
:
{
type
:
Array
,
required
:
false
,
default
:
()
=>
[],
},
secondary
:
{
type
:
Array
,
required
:
false
,
default
:
()
=>
[],
},
views
:
{
type
:
Object
,
required
:
false
,
default
:
()
=>
({}),
},
},
data
()
{
return
{
activeId
:
''
,
};
},
computed
:
{
menuItemGroups
()
{
return
[
{
key
:
'
primary
'
,
items
:
this
.
primary
,
classes
:
''
},
{
key
:
'
secondary
'
,
items
:
this
.
secondary
,
classes
:
SECONDARY_GROUP_CLASS
,
},
].
filter
((
x
)
=>
x
.
items
?.
length
);
},
allMenuItems
()
{
return
this
.
menuItemGroups
.
flatMap
((
x
)
=>
x
.
items
);
},
activeMenuItem
()
{
return
this
.
allMenuItems
.
find
((
x
)
=>
x
.
id
===
this
.
activeId
);
},
activeView
()
{
return
this
.
activeMenuItem
?.
view
;
},
menuClass
()
{
if
(
!
this
.
activeView
)
{
return
'
gl-w-full
'
;
}
return
''
;
},
},
created
()
{
// Initialize activeId based on initialization prop
this
.
activeId
=
this
.
allMenuItems
.
find
((
x
)
=>
x
.
active
)?.
id
;
},
methods
:
{
onClick
({
id
,
href
})
{
// If we're a link, let's just do the default behavior so the view won't change
if
(
href
)
{
return
;
}
this
.
activeId
=
id
;
},
menuItemClasses
(
menuItem
)
{
if
(
menuItem
.
id
===
this
.
activeId
)
{
return
ACTIVE_CLASS
;
}
return
''
;
},
},
FREQUENT_ITEMS_PROJECTS
,
FREQUENT_ITEMS_GROUPS
,
// expose for unit tests
ACTIVE_CLASS
,
SECONDARY_GROUP_CLASS
,
};
</
script
>
<
template
>
<div
class=
"gl-display-flex gl-align-items-stretch"
>
<div
class=
"gl-w-grid-size-30 gl-flex-shrink-0 gl-bg-gray-10"
:class=
"menuClass"
data-testid=
"menu-sidebar"
>
<div
class=
"gl-py-3 gl-px-5 gl-h-full gl-display-flex gl-align-items-stretch gl-flex-direction-column"
>
<div
v-for=
"group in menuItemGroups"
:key=
"group.key"
:class=
"group.classes"
data-testid=
"menu-item-group"
>
<top-nav-menu-item
v-for=
"(menu, index) in group.items"
:key=
"menu.id"
data-testid=
"menu-item"
:class=
"[
{ 'gl-mt-1': index !== 0 }, menuItemClasses(menu)]"
:menu-item="menu"
@click="onClick(menu)"
/>
</div>
</div>
</div>
<keep-alive-slots
v-show=
"activeView"
:slot-key=
"activeView"
class=
"gl-w-grid-size-40 gl-overflow-hidden gl-py-3 gl-px-5"
data-testid=
"menu-subview"
>
<template
#projects
>
<top-nav-container-view
:frequent-items-dropdown-type=
"$options.FREQUENT_ITEMS_PROJECTS.namespace"
:frequent-items-vuex-module=
"$options.FREQUENT_ITEMS_PROJECTS.vuexModule"
v-bind=
"views.projects"
/>
</
template
>
<
template
#groups
>
<top-nav-container-view
:frequent-items-dropdown-type=
"$options.FREQUENT_ITEMS_GROUPS.namespace"
:frequent-items-vuex-module=
"$options.FREQUENT_ITEMS_GROUPS.vuexModule"
v-bind=
"views.groups"
/>
</
template
>
</keep-alive-slots>
</div>
</template>
app/assets/javascripts/nav/components/top_nav_menu_item.vue
0 → 100644
View file @
6e953772
<
script
>
import
{
GlButton
,
GlIcon
}
from
'
@gitlab/ui
'
;
export
default
{
components
:
{
GlButton
,
GlIcon
,
},
props
:
{
menuItem
:
{
type
:
Object
,
required
:
true
,
},
},
};
</
script
>
<
template
>
<gl-button
category=
"tertiary"
:href=
"menuItem.href"
class=
"top-nav-menu-item gl-display-block"
v-on=
"$listeners"
>
<span
class=
"gl-display-flex"
>
<gl-icon
v-if=
"menuItem.icon"
:name=
"menuItem.icon"
class=
"gl-mr-2!"
/>
{{
menuItem
.
title
}}
<gl-icon
v-if=
"menuItem.view"
name=
"chevron-right"
class=
"gl-ml-auto"
/>
</span>
</gl-button>
</
template
>
app/assets/javascripts/nav/index.js
0 → 100644
View file @
6e953772
export
const
initTopNav
=
async
()
=>
{
const
el
=
document
.
getElementById
(
'
js-top-nav
'
);
if
(
!
el
)
{
return
;
}
// With combined_menu feature flag, there's a benefit to splitting up the import
const
{
mountTopNav
}
=
await
import
(
/* webpackChunkName: 'top_nav' */
'
./mount
'
);
mountTopNav
(
el
);
};
app/assets/javascripts/nav/mount.js
0 → 100644
View file @
6e953772
import
Vue
from
'
vue
'
;
import
Vuex
from
'
vuex
'
;
import
App
from
'
./components/top_nav_app.vue
'
;
import
{
createStore
}
from
'
./stores
'
;
Vue
.
use
(
Vuex
);
export
const
mountTopNav
=
(
el
)
=>
{
const
viewModel
=
JSON
.
parse
(
el
.
dataset
.
viewModel
);
const
store
=
createStore
();
return
new
Vue
({
el
,
store
,
render
(
h
)
{
return
h
(
App
,
{
props
:
{
navData
:
viewModel
,
},
});
},
});
};
app/assets/javascripts/nav/stores/index.js
0 → 100644
View file @
6e953772
import
Vuex
from
'
vuex
'
;
import
{
createStoreOptions
}
from
'
~/frequent_items/store
'
;
export
const
createStore
=
()
=>
new
Vuex
.
Store
(
createStoreOptions
());
app/assets/stylesheets/framework/dropdowns.scss
View file @
6e953772
...
...
@@ -839,8 +839,52 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
.frequent-items-dropdown-container
{
display
:
flex
;
flex-direction
:
row
;
width
:
500px
;
height
:
354px
;
height
:
$grid-size
*
40
;
&
.with-deprecated-styles
{
width
:
500px
;
height
:
354px
;
.section-header
,
.frequent-items-list-container
li
.section-empty
{
padding
:
0
$gl-padding
;
}
.search-input-container
{
position
:
relative
;
padding
:
4px
$gl-padding
;
.search-icon
{
position
:
absolute
;
top
:
13px
;
right
:
25px
;
color
:
$gray-300
;
}
}
@include
media-breakpoint-down
(
xs
)
{
flex-direction
:
column
;
width
:
100%
;
height
:
auto
;
flex
:
1
;
.frequent-items-dropdown-sidebar
,
.frequent-items-dropdown-content
{
width
:
100%
;
}
.frequent-items-dropdown-sidebar
{
border-bottom
:
1px
solid
$border-color
;
border-right
:
0
;
}
}
.frequent-items-list-container
{
width
:
auto
;
height
:
auto
;
padding-bottom
:
0
;
}
}
.frequent-items-dropdown-sidebar
,
.frequent-items-dropdown-content
{
...
...
@@ -861,26 +905,8 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
width
:
70%
;
}
@include
media-breakpoint-down
(
xs
)
{
flex-direction
:
column
;
width
:
100%
;
height
:
auto
;
flex
:
1
;
.frequent-items-dropdown-sidebar
,
.frequent-items-dropdown-content
{
width
:
100%
;
}
.frequent-items-dropdown-sidebar
{
border-bottom
:
1px
solid
$border-color
;
border-right
:
0
;
}
}
.section-header
,
.frequent-items-list-container
li
.section-empty
{
padding
:
0
$gl-padding
;
color
:
$gl-text-color-secondary
;
font-size
:
$gl-font-size
;
}
...
...
@@ -898,36 +924,16 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
}
}
.search-input-container
{
position
:
relative
;
padding
:
4px
$gl-padding
;
.search-icon
{
position
:
absolute
;
top
:
13px
;
right
:
25px
;
color
:
$gray-300
;
}
}
.section-header
{
font-weight
:
700
;
margin-top
:
8px
;
}
@include
media-breakpoint-down
(
xs
)
{
.frequent-items-list-container
{
width
:
auto
;
height
:
auto
;
padding-bottom
:
0
;
}
}
}
.frequent-items-list-item-container
{
.frequent-items-item-avatar-container
,
.frequent-items-item-metadata-container
{
fl
oat
:
left
;
fl
ex-shrink
:
0
;
}
.frequent-items-item-metadata-container
{
...
...
app/assets/stylesheets/framework/header.scss
View file @
6e953772
$top-nav-hover-bg
:
var
(
--
indigo-900-alpha-008
,
$indigo-900-alpha-008
)
!
important
;
.navbar-gitlab
{
padding
:
0
16px
;
z-index
:
$header-zindex
;
...
...
@@ -254,6 +256,7 @@
}
}
.top-nav-toggle
,
>
button
{
background
:
transparent
;
border
:
0
;
...
...
@@ -629,3 +632,36 @@
}
}
}
.top-nav-container-view
{
.gl-new-dropdown
&
.gl-search-box-by-type
{
@include
gl-m-0
;
}
.frequent-items-list-item-container
>
a
:hover
{
background-color
:
$top-nav-hover-bg
;
}
}
.top-nav-toggle
{
.dropdown-icon
{
@include
gl-mr-3
;
}
.dropdown-chevron
{
top
:
0
;
}
}
.top-nav-menu-item
{
color
:
var
(
--
indigo-900
,
$theme-indigo-900
)
!
important
;
&
.active
,
&
:hover
{
background-color
:
$top-nav-hover-bg
;
}
.gl-icon
{
color
:
inherit
!
important
;
}
}
app/assets/stylesheets/framework/variables.scss
View file @
6e953772
...
...
@@ -283,6 +283,8 @@ $indigo-700: #4b4ba3;
$indigo-800
:
#393982
;
$indigo-900
:
#292961
;
$indigo-950
:
#1a1a40
;
// To do this variant right for darkmode, we need to create a variable for it.
$indigo-900-alpha-008
:
rgba
(
$indigo-900
,
0
.08
);
$theme-blue-50
:
#f4f8fc
;
$theme-blue-100
:
#e6edf5
;
...
...
app/assets/stylesheets/themes/_dark.scss
View file @
6e953772
...
...
@@ -70,6 +70,7 @@ $indigo-700: #a6a6de;
$indigo-800
:
#d1d1f0
;
$indigo-900
:
#ebebfa
;
$indigo-950
:
#f7f7ff
;
$indigo-900-alpha-008
:
rgba
(
$indigo-900
,
0
.08
);
$gray-lightest
:
#222
;
$gray-light
:
$gray-50
;
...
...
@@ -160,6 +161,7 @@ body.gl-dark {
--indigo-800
:
#{
$indigo-800
}
;
--indigo-900
:
#{
$indigo-900
}
;
--indigo-950
:
#{
$indigo-950
}
;
--indigo-900-alpha-008
:
#{
$indigo-900-alpha-008
}
;
--gl-text-color
:
#{
$gray-900
}
;
--border-color
:
#{
$border-color
}
;
...
...
app/assets/stylesheets/utilities.scss
View file @
6e953772
...
...
@@ -189,3 +189,21 @@ $gl-line-height-42: px-to-rem(42px);
.gl-line-height-42
{
line-height
:
$gl-line-height-42
;
}
.gl-w-grid-size-30
{
width
:
$grid-size
*
30
;
}
.gl-w-grid-size-40
{
width
:
$grid-size
*
40
;
}
// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/2209
.gl-max-w-none
\
!
{
max-width
:
none
!
important
;
}
// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/2209
.gl-max-h-none
\
!
{
max-height
:
none
!
important
;
}
app/views/layouts/header/_default.html.haml
View file @
6e953772
...
...
@@ -20,7 +20,7 @@
=
_
(
'Next'
)
-
if
Feature
.
enabled?
(
:combined_menu
,
current_user
,
default_enabled: :yaml
)
=
render
"layouts/nav/
combined_menu
"
=
render
"layouts/nav/
top_nav
"
-
else
-
if
current_user
=
render
"layouts/nav/dashboard"
...
...
app/views/layouts/nav/_combined_menu.html.haml
deleted
100644 → 0
View file @
d02bf43a
%button
{
type:
'button'
,
data:
{
toggle:
"dropdown"
}
}
=
sprite_icon
(
'ellipsis_v'
)
=
_
(
'Projects'
)
app/views/layouts/nav/_top_nav.html.haml
0 → 100644
View file @
6e953772
-
view_model
=
top_nav_view_model
(
project:
@project
,
group:
@group
)
%ul
.list-unstyled.navbar-sub-nav
#js-top-nav
{
data:
{
view_model:
view_model
.
to_json
}
}
%li
%a
.top-nav-toggle
{
href:
'#'
,
type:
'button'
,
data:
{
toggle:
"dropdown"
}
}
=
sprite_icon
(
'dot-grid'
,
css_class:
"dropdown-icon"
)
=
view_model
[
:activeTitle
]
=
sprite_icon
(
'chevron-down'
)
app/views/layouts/nav/groups_dropdown/_show.html.haml
View file @
6e953772
...
...
@@ -3,7 +3,7 @@
-# Please see [this MR][1] for more context.
-# [1]: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56587
-
group_meta
=
{
id:
@group
.
id
,
name:
@group
.
name
,
namespace:
@group
.
full_name
,
web_url:
group_path
(
@group
),
avatar_url:
@group
.
avatar_url
}
if
@group
&
.
persisted?
.frequent-items-dropdown-container
.frequent-items-dropdown-container
.with-deprecated-styles
.frequent-items-dropdown-sidebar.qa-groups-dropdown-sidebar
%ul
=
nav_link
(
path:
'dashboard/groups#index'
)
do
...
...
app/views/layouts/nav/projects_dropdown/_show.html.haml
View file @
6e953772
...
...
@@ -3,7 +3,7 @@
-# Please see [this MR][1] for more context.
-# [1]: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56587
-
project_meta
=
{
id:
@project
.
id
,
name:
@project
.
name
,
namespace:
@project
.
full_name
,
web_url:
project_path
(
@project
),
avatar_url:
@project
.
avatar_url
}
if
@project
&
.
persisted?
.frequent-items-dropdown-container
.frequent-items-dropdown-container
.with-deprecated-styles
.frequent-items-dropdown-sidebar.qa-projects-dropdown-sidebar
%ul
=
nav_link
(
path:
'dashboard/projects#index'
)
do
...
...
locale/gitlab.pot
View file @
6e953772
...
...
@@ -34070,6 +34070,9 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
msgid "TopNav|Switch to..."
msgstr ""
msgid "Topics (optional)"
msgstr ""
...
...
spec/features/admin/admin_mode_spec.rb
View file @
6e953772
...
...
@@ -20,8 +20,6 @@ RSpec.describe 'Admin mode' do
context
'when not in admin mode'
do
it
'has no leave admin mode button'
do
pending_on_combined_menu_flag
visit
new_admin_session_path
page
.
within
(
'.navbar-sub-nav'
)
do
...
...
@@ -180,8 +178,6 @@ RSpec.describe 'Admin mode' do
end
it
'shows no admin mode buttons in navbar'
do
pending_on_combined_menu_flag
visit
admin_root_path
page
.
within
(
'.navbar-sub-nav'
)
do
...
...
spec/frontend/nav/components/top_nav_app_spec.js
0 → 100644
View file @
6e953772
import
{
GlNavItemDropdown
,
GlTooltip
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
,
mount
}
from
'
@vue/test-utils
'
;
import
TopNavApp
from
'
~/nav/components/top_nav_app.vue
'
;
import
TopNavDropdownMenu
from
'
~/nav/components/top_nav_dropdown_menu.vue
'
;
import
{
TEST_NAV_DATA
}
from
'
../mock_data
'
;
describe
(
'
~/nav/components/top_nav_app.vue
'
,
()
=>
{
let
wrapper
;
const
createComponent
=
(
mountFn
=
shallowMount
)
=>
{
wrapper
=
mountFn
(
TopNavApp
,
{
propsData
:
{
navData
:
TEST_NAV_DATA
,
},
});
};
const
findNavItemDropdown
=
()
=>
wrapper
.
findComponent
(
GlNavItemDropdown
);
const
findMenu
=
()
=>
wrapper
.
findComponent
(
TopNavDropdownMenu
);
const
findTooltip
=
()
=>
wrapper
.
findComponent
(
GlTooltip
);
afterEach
(()
=>
{
wrapper
.
destroy
();
});
describe
(
'
default
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
();
});
it
(
'
renders nav item dropdown
'
,
()
=>
{
expect
(
findNavItemDropdown
().
attributes
(
'
href
'
)).
toBeUndefined
();
expect
(
findNavItemDropdown
().
attributes
()).
toMatchObject
({
icon
:
'
dot-grid
'
,
text
:
TEST_NAV_DATA
.
activeTitle
,
'
no-flip
'
:
''
,
});
});
it
(
'
renders top nav dropdown menu
'
,
()
=>
{
expect
(
findMenu
().
props
()).
toStrictEqual
({
primary
:
TEST_NAV_DATA
.
primary
,
secondary
:
TEST_NAV_DATA
.
secondary
,
views
:
TEST_NAV_DATA
.
views
,
});
});
it
(
'
renders tooltip
'
,
()
=>
{
expect
(
findTooltip
().
attributes
()).
toMatchObject
({
'
boundary-padding
'
:
'
0
'
,
placement
:
'
right
'
,
title
:
TopNavApp
.
TOOLTIP
,
});
});
});
describe
(
'
when full mounted
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
(
mount
);
});
it
(
'
has dropdown toggle as tooltip target
'
,
()
=>
{
const
targetFn
=
findTooltip
().
props
(
'
target
'
);
expect
(
targetFn
()).
toBe
(
wrapper
.
find
(
'
.js-top-nav-dropdown-toggle
'
).
element
);
});
});
});
spec/frontend/nav/components/top_nav_container_view_spec.js
0 → 100644
View file @
6e953772
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
nextTick
}
from
'
vue
'
;
import
FrequentItemsApp
from
'
~/frequent_items/components/app.vue
'
;
import
{
FREQUENT_ITEMS_PROJECTS
}
from
'
~/frequent_items/constants
'
;
import
eventHub
from
'
~/frequent_items/event_hub
'
;
import
TopNavContainerView
from
'
~/nav/components/top_nav_container_view.vue
'
;
import
TopNavMenuItem
from
'
~/nav/components/top_nav_menu_item.vue
'
;
import
VuexModuleProvider
from
'
~/vue_shared/components/vuex_module_provider.vue
'
;
import
{
TEST_NAV_DATA
}
from
'
../mock_data
'
;
const
DEFAULT_PROPS
=
{
frequentItemsDropdownType
:
FREQUENT_ITEMS_PROJECTS
.
namespace
,
frequentItemsVuexModule
:
FREQUENT_ITEMS_PROJECTS
.
vuexModule
,
linksPrimary
:
TEST_NAV_DATA
.
primary
,
linksSecondary
:
TEST_NAV_DATA
.
secondary
,
};
const
TEST_OTHER_PROPS
=
{
namespace
:
'
projects
'
,
currentUserName
:
''
,
currentItem
:
{},
};
describe
(
'
~/nav/components/top_nav_container_view.vue
'
,
()
=>
{
let
wrapper
;
const
createComponent
=
(
props
=
{})
=>
{
wrapper
=
shallowMount
(
TopNavContainerView
,
{
propsData
:
{
...
DEFAULT_PROPS
,
...
TEST_OTHER_PROPS
,
...
props
,
},
});
};
const
findMenuItems
=
(
parent
=
wrapper
)
=>
parent
.
findAll
(
TopNavMenuItem
);
const
findMenuItemsModel
=
(
parent
=
wrapper
)
=>
findMenuItems
(
parent
).
wrappers
.
map
((
x
)
=>
x
.
props
());
const
findMenuItemGroups
=
()
=>
wrapper
.
findAll
(
'
[data-testid="menu-item-group"]
'
);
const
findMenuItemGroupsModel
=
()
=>
findMenuItemGroups
().
wrappers
.
map
(
findMenuItemsModel
);
const
findFrequentItemsApp
=
()
=>
{
const
parent
=
wrapper
.
findComponent
(
VuexModuleProvider
);
return
{
vuexModule
:
parent
.
props
(
'
vuexModule
'
),
props
:
parent
.
findComponent
(
FrequentItemsApp
).
props
(),
};
};
afterEach
(()
=>
{
wrapper
.
destroy
();
});
it
.
each
([
'
projects
'
,
'
groups
'
])(
'
emits frequent items event to event hub (%s)
'
,
async
(
frequentItemsDropdownType
)
=>
{
const
listener
=
jest
.
fn
();
eventHub
.
$on
(
`
${
frequentItemsDropdownType
}
-dropdownOpen`
,
listener
);
createComponent
({
frequentItemsDropdownType
});
expect
(
listener
).
not
.
toHaveBeenCalled
();
await
nextTick
();
expect
(
listener
).
toHaveBeenCalled
();
},
);
describe
(
'
default
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
();
});
it
(
'
renders frequent items app
'
,
()
=>
{
expect
(
findFrequentItemsApp
()).
toEqual
({
vuexModule
:
DEFAULT_PROPS
.
frequentItemsVuexModule
,
props
:
TEST_OTHER_PROPS
,
});
});
it
(
'
renders menu item groups
'
,
()
=>
{
expect
(
findMenuItemGroupsModel
()).
toEqual
([
TEST_NAV_DATA
.
primary
.
map
((
menuItem
)
=>
({
menuItem
})),
TEST_NAV_DATA
.
secondary
.
map
((
menuItem
)
=>
({
menuItem
})),
]);
});
it
(
'
only the first group does not have margin top
'
,
()
=>
{
expect
(
findMenuItemGroups
().
wrappers
.
map
((
x
)
=>
x
.
classes
(
'
gl-mt-3
'
))).
toEqual
([
false
,
true
]);
});
it
(
'
only the first menu item does not have margin top
'
,
()
=>
{
const
actual
=
findMenuItems
(
findMenuItemGroups
().
at
(
1
)).
wrappers
.
map
((
x
)
=>
x
.
classes
(
'
gl-mt-1
'
),
);
expect
(
actual
).
toEqual
([
false
,
...
TEST_NAV_DATA
.
secondary
.
slice
(
1
).
fill
(
true
)]);
});
});
describe
(
'
without secondary links
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
({
linksSecondary
:
[],
});
});
it
(
'
renders one menu item group
'
,
()
=>
{
expect
(
findMenuItemGroupsModel
()).
toEqual
([
TEST_NAV_DATA
.
primary
.
map
((
menuItem
)
=>
({
menuItem
})),
]);
});
});
});
spec/frontend/nav/components/top_nav_dropdown_menu_spec.js
0 → 100644
View file @
6e953772
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
nextTick
}
from
'
vue
'
;
import
TopNavDropdownMenu
from
'
~/nav/components/top_nav_dropdown_menu.vue
'
;
import
KeepAliveSlots
from
'
~/vue_shared/components/keep_alive_slots.vue
'
;
import
{
TEST_NAV_DATA
}
from
'
../mock_data
'
;
const
SECONDARY_GROUP_CLASSES
=
TopNavDropdownMenu
.
SECONDARY_GROUP_CLASS
.
split
(
'
'
);
describe
(
'
~/nav/components/top_nav_dropdown_menu.vue
'
,
()
=>
{
let
wrapper
;
const
createComponent
=
(
props
=
{})
=>
{
wrapper
=
shallowMount
(
TopNavDropdownMenu
,
{
propsData
:
{
primary
:
TEST_NAV_DATA
.
primary
,
secondary
:
TEST_NAV_DATA
.
secondary
,
views
:
TEST_NAV_DATA
.
views
,
...
props
,
},
});
};
const
findMenuItems
=
(
parent
=
wrapper
)
=>
parent
.
findAll
(
'
[data-testid="menu-item"]
'
);
const
findMenuItemsModel
=
(
parent
=
wrapper
)
=>
findMenuItems
(
parent
).
wrappers
.
map
((
x
)
=>
({
menuItem
:
x
.
props
(
'
menuItem
'
),
isActive
:
x
.
classes
(
'
active
'
),
}));
const
findMenuItemGroups
=
()
=>
wrapper
.
findAll
(
'
[data-testid="menu-item-group"]
'
);
const
findMenuItemGroupsModel
=
()
=>
findMenuItemGroups
().
wrappers
.
map
((
x
)
=>
({
classes
:
x
.
classes
(),
items
:
findMenuItemsModel
(
x
),
}));
const
findMenuSidebar
=
()
=>
wrapper
.
find
(
'
[data-testid="menu-sidebar"]
'
);
const
findMenuSubview
=
()
=>
wrapper
.
findComponent
(
KeepAliveSlots
);
const
hasFullWidthMenuSidebar
=
()
=>
findMenuSidebar
().
classes
(
'
gl-w-full
'
);
const
createItemsGroupModelExpectation
=
({
primary
=
TEST_NAV_DATA
.
primary
,
secondary
=
TEST_NAV_DATA
.
secondary
,
activeIndex
=
-
1
,
}
=
{})
=>
[
{
classes
:
[],
items
:
primary
.
map
((
menuItem
,
index
)
=>
({
isActive
:
index
===
activeIndex
,
menuItem
})),
},
{
classes
:
SECONDARY_GROUP_CLASSES
,
items
:
secondary
.
map
((
menuItem
)
=>
({
isActive
:
false
,
menuItem
})),
},
];
afterEach
(()
=>
{
wrapper
.
destroy
();
});
describe
(
'
default
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
();
});
it
(
'
renders menu item groups
'
,
()
=>
{
expect
(
findMenuItemGroupsModel
()).
toEqual
(
createItemsGroupModelExpectation
());
});
it
(
'
has full width menu sidebar
'
,
()
=>
{
expect
(
hasFullWidthMenuSidebar
()).
toBe
(
true
);
});
it
(
'
renders hidden subview with no slot key
'
,
()
=>
{
const
subview
=
findMenuSubview
();
expect
(
subview
.
isVisible
()).
toBe
(
false
);
expect
(
subview
.
props
()).
toEqual
({
slotKey
:
''
});
});
it
(
'
the first menu item in a group does not render margin top
'
,
()
=>
{
const
actual
=
findMenuItems
(
findMenuItemGroups
().
at
(
0
)).
wrappers
.
map
((
x
)
=>
x
.
classes
(
'
gl-mt-1
'
),
);
expect
(
actual
).
toEqual
([
false
,
...
TEST_NAV_DATA
.
primary
.
slice
(
1
).
fill
(
true
)]);
});
});
describe
(
'
with pre-initialized active view
'
,
()
=>
{
const
primaryWithActive
=
[
TEST_NAV_DATA
.
primary
[
0
],
{
...
TEST_NAV_DATA
.
primary
[
1
],
active
:
true
,
},
...
TEST_NAV_DATA
.
primary
.
slice
(
2
),
];
beforeEach
(()
=>
{
createComponent
({
primary
:
primaryWithActive
,
});
});
it
(
'
renders menu item groups
'
,
()
=>
{
expect
(
findMenuItemGroupsModel
()).
toEqual
(
createItemsGroupModelExpectation
({
primary
:
primaryWithActive
,
activeIndex
:
1
}),
);
});
it
(
'
does not have full width menu sidebar
'
,
()
=>
{
expect
(
hasFullWidthMenuSidebar
()).
toBe
(
false
);
});
it
(
'
renders visible subview with slot key
'
,
()
=>
{
const
subview
=
findMenuSubview
();
expect
(
subview
.
isVisible
()).
toBe
(
true
);
expect
(
subview
.
props
(
'
slotKey
'
)).
toBe
(
primaryWithActive
[
1
].
view
);
});
it
(
'
does not change view if non-view menu item is clicked
'
,
async
()
=>
{
const
secondaryLink
=
findMenuItems
().
at
(
primaryWithActive
.
length
);
// Ensure this doesn't have a view
expect
(
secondaryLink
.
props
(
'
menuItem
'
).
view
).
toBeUndefined
();
secondaryLink
.
vm
.
$emit
(
'
click
'
);
await
nextTick
();
expect
(
findMenuSubview
().
props
(
'
slotKey
'
)).
toBe
(
primaryWithActive
[
1
].
view
);
});
describe
(
'
when other view menu item is clicked
'
,
()
=>
{
let
primaryLink
;
beforeEach
(
async
()
=>
{
primaryLink
=
findMenuItems
().
at
(
0
);
primaryLink
.
vm
.
$emit
(
'
click
'
);
await
nextTick
();
});
it
(
'
clicked on link with view
'
,
()
=>
{
expect
(
primaryLink
.
props
(
'
menuItem
'
).
view
).
toBeTruthy
();
});
it
(
'
changes active view
'
,
()
=>
{
expect
(
findMenuSubview
().
props
(
'
slotKey
'
)).
toBe
(
primaryWithActive
[
0
].
view
);
});
it
(
'
changes active status on menu item
'
,
()
=>
{
expect
(
findMenuItemGroupsModel
()).
toStrictEqual
(
createItemsGroupModelExpectation
({
primary
:
primaryWithActive
,
activeIndex
:
0
}),
);
});
});
});
});
spec/frontend/nav/components/top_nav_menu_item_spec.js
0 → 100644
View file @
6e953772
import
{
GlButton
,
GlIcon
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
TopNavMenuItem
from
'
~/nav/components/top_nav_menu_item.vue
'
;
const
TEST_MENU_ITEM
=
{
title
:
'
Cheeseburger
'
,
icon
:
'
search
'
,
href
:
'
/pretty/good/burger
'
,
view
:
'
burger-view
'
,
};
describe
(
'
~/nav/components/top_nav_menu_item.vue
'
,
()
=>
{
let
listener
;
let
wrapper
;
const
createComponent
=
(
props
=
{})
=>
{
wrapper
=
shallowMount
(
TopNavMenuItem
,
{
propsData
:
{
menuItem
:
TEST_MENU_ITEM
,
...
props
,
},
listeners
:
{
click
:
listener
,
},
});
};
const
findButton
=
()
=>
wrapper
.
find
(
GlButton
);
const
findButtonIcons
=
()
=>
findButton
()
.
findAllComponents
(
GlIcon
)
.
wrappers
.
map
((
x
)
=>
x
.
props
(
'
name
'
));
beforeEach
(()
=>
{
listener
=
jest
.
fn
();
});
describe
(
'
default
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
();
});
it
(
'
renders button href and text
'
,
()
=>
{
const
button
=
findButton
();
expect
(
button
.
attributes
(
'
href
'
)).
toBe
(
TEST_MENU_ITEM
.
href
);
expect
(
button
.
text
()).
toBe
(
TEST_MENU_ITEM
.
title
);
});
it
(
'
passes listeners to button
'
,
()
=>
{
expect
(
listener
).
not
.
toHaveBeenCalled
();
findButton
().
vm
.
$emit
(
'
click
'
,
'
TEST
'
);
expect
(
listener
).
toHaveBeenCalledWith
(
'
TEST
'
);
});
});
describe
.
each
`
desc | menuItem | expectedIcons
${
'
default
'
}
|
${
TEST_MENU_ITEM
}
|
${[
TEST_MENU_ITEM
.
icon
,
'
chevron-right
'
]}
${
'
with no icon
'
}
|
${{
...
TEST_MENU_ITEM
,
icon
:
null
}
} |
${[
'
chevron-right
'
]}
${
'
with no view
'
}
|
${{
...
TEST_MENU_ITEM
,
view
:
null
}
} |
${[
TEST_MENU_ITEM
.
icon
]}
${
'
with no icon or view
'
}
|
${{
...
TEST_MENU_ITEM
,
view
:
null
,
icon
:
null
}
} |
${[]}
`
(
'
$desc
'
,
({
menuItem
,
expectedIcons
})
=>
{
beforeEach
(()
=>
{
createComponent
({
menuItem
});
});
it
(
`renders expected icons
${
JSON
.
stringify
(
expectedIcons
)}
`
,
()
=>
{
expect
(
findButtonIcons
()).
toEqual
(
expectedIcons
);
});
});
});
spec/frontend/nav/mock_data.js
0 → 100644
View file @
6e953772
import
{
range
}
from
'
lodash
'
;
export
const
TEST_NAV_DATA
=
{
activeTitle
:
'
Test Active Title
'
,
primary
:
[
...[
'
projects
'
,
'
groups
'
].
map
((
view
)
=>
({
id
:
view
,
href
:
null
,
title
:
view
,
view
,
})),
...
range
(
0
,
2
).
map
((
idx
)
=>
({
id
:
`primary-link-
${
idx
}
`
,
href
:
`/path/to/primary/
${
idx
}
`
,
title
:
`Title
${
idx
}
`
,
})),
],
secondary
:
range
(
0
,
2
).
map
((
idx
)
=>
({
id
:
`secondary-link-
${
idx
}
`
,
href
:
`/path/to/secondary/
${
idx
}
`
,
title
:
`SecTitle
${
idx
}
`
,
})),
views
:
{
projects
:
{
namespace
:
'
projects
'
,
currentUserName
:
''
,
currentItem
:
{},
},
groups
:
{
namespace
:
'
groups
'
,
currentUserName
:
''
,
currentItem
:
{},
},
},
};
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment