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
78373a26
Commit
78373a26
authored
Jun 09, 2021
by
Paul Slaughter
Committed by
David O'Regan
Jun 09, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Part 2 - Top nav responsive view
parent
3f08f01e
Changes
26
Hide whitespace changes
Inline
Side-by-side
Showing
26 changed files
with
896 additions
and
35 deletions
+896
-35
app/assets/javascripts/nav/components/responsive_app.vue
app/assets/javascripts/nav/components/responsive_app.vue
+75
-4
app/assets/javascripts/nav/components/responsive_header.vue
app/assets/javascripts/nav/components/responsive_header.vue
+37
-0
app/assets/javascripts/nav/components/responsive_home.vue
app/assets/javascripts/nav/components/responsive_home.vue
+62
-0
app/assets/javascripts/nav/components/top_nav_container_view.vue
...ets/javascripts/nav/components/top_nav_container_view.vue
+10
-1
app/assets/javascripts/nav/components/top_nav_menu_item.vue
app/assets/javascripts/nav/components/top_nav_menu_item.vue
+11
-3
app/assets/javascripts/nav/components/top_nav_menu_sections.vue
...sets/javascripts/nav/components/top_nav_menu_sections.vue
+1
-0
app/assets/javascripts/nav/components/top_nav_new_dropdown.vue
...ssets/javascripts/nav/components/top_nav_new_dropdown.vue
+55
-0
app/assets/javascripts/nav/utils/reset_menu_items_active.js
app/assets/javascripts/nav/utils/reset_menu_items_active.js
+14
-0
app/assets/stylesheets/startup/startup-dark.scss
app/assets/stylesheets/startup/startup-dark.scss
+9
-0
app/assets/stylesheets/startup/startup-general.scss
app/assets/stylesheets/startup/startup-general.scss
+9
-0
app/helpers/nav/top_nav_helper.rb
app/helpers/nav/top_nav_helper.rb
+41
-4
app/views/layouts/header/_default.html.haml
app/views/layouts/header/_default.html.haml
+7
-6
app/views/layouts/nav/_top_nav_responsive.html.haml
app/views/layouts/nav/_top_nav_responsive.html.haml
+1
-1
ee/app/assets/stylesheets/startup/startup-dark.scss
ee/app/assets/stylesheets/startup/startup-dark.scss
+9
-0
ee/app/assets/stylesheets/startup/startup-general.scss
ee/app/assets/stylesheets/startup/startup-general.scss
+9
-0
locale/gitlab.pot
locale/gitlab.pot
+3
-0
spec/features/nav/top_nav_responsive_spec.rb
spec/features/nav/top_nav_responsive_spec.rb
+19
-4
spec/frontend/nav/components/responsive_app_spec.js
spec/frontend/nav/components/responsive_app_spec.js
+92
-0
spec/frontend/nav/components/responsive_header_spec.js
spec/frontend/nav/components/responsive_header_spec.js
+67
-0
spec/frontend/nav/components/responsive_home_spec.js
spec/frontend/nav/components/responsive_home_spec.js
+137
-0
spec/frontend/nav/components/top_nav_container_view_spec.js
spec/frontend/nav/components/top_nav_container_view_spec.js
+6
-0
spec/frontend/nav/components/top_nav_menu_item_spec.js
spec/frontend/nav/components/top_nav_menu_item_spec.js
+37
-3
spec/frontend/nav/components/top_nav_menu_sections_spec.js
spec/frontend/nav/components/top_nav_menu_sections_spec.js
+4
-4
spec/frontend/nav/components/top_nav_new_dropdown_spec.js
spec/frontend/nav/components/top_nav_new_dropdown_spec.js
+122
-0
spec/frontend/nav/mock_data.js
spec/frontend/nav/mock_data.js
+4
-0
spec/helpers/nav/top_nav_helper_spec.rb
spec/helpers/nav/top_nav_helper_spec.rb
+55
-5
No files found.
app/assets/javascripts/nav/components/responsive_app.vue
View file @
78373a26
<
script
>
import
{
FREQUENT_ITEMS_PROJECTS
,
FREQUENT_ITEMS_GROUPS
}
from
'
~/frequent_items/constants
'
;
import
{
BV_DROPDOWN_SHOW
,
BV_DROPDOWN_HIDE
}
from
'
~/lib/utils/constants
'
;
import
KeepAliveSlots
from
'
~/vue_shared/components/keep_alive_slots.vue
'
;
import
eventHub
,
{
EVENT_RESPONSIVE_TOGGLE
}
from
'
../event_hub
'
;
const
TEMPORARY_PLACEHOLDER
=
'
Placeholder for responsive top nav
'
;
import
{
resetMenuItemsActive
}
from
'
../utils/reset_menu_items_active
'
;
import
ResponsiveHeader
from
'
./responsive_header.vue
'
;
import
ResponsiveHome
from
'
./responsive_home.vue
'
;
import
TopNavContainerView
from
'
./top_nav_container_view.vue
'
;
export
default
{
components
:
{
KeepAliveSlots
,
ResponsiveHeader
,
ResponsiveHome
,
TopNavContainerView
,
},
props
:
{
navData
:
{
type
:
Object
,
required
:
true
,
},
},
data
()
{
return
{
activeView
:
'
home
'
,
hasMobileOverlay
:
false
,
};
},
computed
:
{
nav
()
{
return
resetMenuItemsActive
(
this
.
navData
);
},
},
created
()
{
eventHub
.
$on
(
EVENT_RESPONSIVE_TOGGLE
,
this
.
onToggle
);
this
.
$root
.
$on
(
BV_DROPDOWN_SHOW
,
this
.
showMobileOverlay
);
this
.
$root
.
$on
(
BV_DROPDOWN_HIDE
,
this
.
hideMobileOverlay
);
},
beforeDestroy
()
{
eventHub
.
$off
(
EVENT_RESPONSIVE_TOGGLE
,
this
.
onToggle
);
this
.
$root
.
$off
(
BV_DROPDOWN_SHOW
,
this
.
showMobileOverlay
);
this
.
$root
.
$off
(
BV_DROPDOWN_HIDE
,
this
.
hideMobileOverlay
);
},
methods
:
{
onToggle
()
{
document
.
body
.
classList
.
toggle
(
'
top-nav-responsive-open
'
);
},
onMenuItemClick
({
view
})
{
if
(
view
)
{
this
.
activeView
=
view
;
}
},
showMobileOverlay
()
{
this
.
hasMobileOverlay
=
true
;
},
hideMobileOverlay
()
{
this
.
hasMobileOverlay
=
false
;
},
},
TEMPORARY_PLACEHOLDER
,
FREQUENT_ITEMS_PROJECTS
,
FREQUENT_ITEMS_GROUPS
,
};
</
script
>
<
template
>
<p>
{{
$options
.
TEMPORARY_PLACEHOLDER
}}
</p>
<div>
<div
class=
"mobile-overlay"
:class=
"
{ 'mobile-nav-open': hasMobileOverlay }"
data-testid="mobile-overlay"
>
</div>
<keep-alive-slots
:slot-key=
"activeView"
>
<template
#home
>
<responsive-home
:nav-data=
"nav"
@
menu-item-click=
"onMenuItemClick"
/>
</
template
>
<
template
#projects
>
<responsive-header
@
menu-item-click=
"onMenuItemClick"
>
{{
__
(
'
Projects
'
)
}}
</responsive-header>
<top-nav-container-view
:frequent-items-dropdown-type=
"$options.FREQUENT_ITEMS_PROJECTS.namespace"
:frequent-items-vuex-module=
"$options.FREQUENT_ITEMS_PROJECTS.vuexModule"
container-class=
"gl-px-3"
v-bind=
"nav.views.projects"
/>
</
template
>
<
template
#groups
>
<responsive-header
@
menu-item-click=
"onMenuItemClick"
>
{{
__
(
'
Groups
'
)
}}
</responsive-header>
<top-nav-container-view
:frequent-items-dropdown-type=
"$options.FREQUENT_ITEMS_GROUPS.namespace"
:frequent-items-vuex-module=
"$options.FREQUENT_ITEMS_GROUPS.vuexModule"
container-class=
"gl-px-3"
v-bind=
"nav.views.groups"
/>
</
template
>
</keep-alive-slots>
</div>
</template>
app/assets/javascripts/nav/components/responsive_header.vue
0 → 100644
View file @
78373a26
<
script
>
import
{
GlTooltipDirective
}
from
'
@gitlab/ui
'
;
import
TopNavMenuItem
from
'
./top_nav_menu_item.vue
'
;
export
default
{
components
:
{
TopNavMenuItem
,
},
directives
:
{
GlTooltip
:
GlTooltipDirective
,
},
computed
:
{
menuItem
()
{
return
{
id
:
'
home
'
,
view
:
'
home
'
,
icon
:
'
angle-left
'
,
};
},
},
};
</
script
>
<
template
>
<header
class=
"gl-py-4 gl-display-flex gl-align-items-center"
>
<top-nav-menu-item
v-gl-tooltip=
"
{ title: s__('TopNav|Go back') }"
class="gl-p-3!"
:menu-item="menuItem"
icon-only
@click="$emit('menu-item-click', menuItem)"
/>
<span
class=
"gl-font-size-h2 gl-font-weight-bold gl-ml-2"
>
<slot></slot>
</span>
</header>
</
template
>
app/assets/javascripts/nav/components/responsive_home.vue
0 → 100644
View file @
78373a26
<
script
>
import
{
GlTooltipDirective
}
from
'
@gitlab/ui
'
;
import
TopNavMenuItem
from
'
./top_nav_menu_item.vue
'
;
import
TopNavMenuSections
from
'
./top_nav_menu_sections.vue
'
;
import
TopNavNewDropdown
from
'
./top_nav_new_dropdown.vue
'
;
const
NEW_VIEW
=
'
new
'
;
const
SEARCH_VIEW
=
'
search
'
;
export
default
{
components
:
{
TopNavMenuItem
,
TopNavMenuSections
,
TopNavNewDropdown
,
},
directives
:
{
GlTooltip
:
GlTooltipDirective
,
},
props
:
{
navData
:
{
type
:
Object
,
required
:
true
,
},
},
computed
:
{
menuSections
()
{
return
[
{
id
:
'
primary
'
,
menuItems
:
this
.
navData
.
primary
},
{
id
:
'
secondary
'
,
menuItems
:
this
.
navData
.
secondary
},
].
filter
((
x
)
=>
x
.
menuItems
?.
length
);
},
newDropdownViewModel
()
{
return
this
.
navData
.
views
[
NEW_VIEW
];
},
searchMenuItem
()
{
return
this
.
navData
.
views
[
SEARCH_VIEW
];
},
},
};
</
script
>
<
template
>
<div>
<header
class=
"gl-display-flex gl-align-items-center gl-py-4 gl-pl-4"
>
<h1
class=
"gl-m-0 gl-font-size-h2 gl-reset-color gl-mr-auto"
>
{{
__
(
'
Menu
'
)
}}
</h1>
<top-nav-menu-item
v-if=
"searchMenuItem"
v-gl-tooltip=
"
{ title: searchMenuItem.title }"
class="gl-ml-3"
:menu-item="searchMenuItem"
icon-only
/>
<top-nav-new-dropdown
v-if=
"newDropdownViewModel"
v-gl-tooltip=
"
{ title: newDropdownViewModel.title }"
:view-model="newDropdownViewModel"
class="gl-ml-3"
/>
</header>
<top-nav-menu-sections
class=
"gl-h-full"
:sections=
"menuSections"
v-on=
"$listeners"
/>
</div>
</
template
>
app/assets/javascripts/nav/components/top_nav_container_view.vue
View file @
78373a26
...
...
@@ -20,6 +20,11 @@ export default {
type
:
String
,
required
:
true
,
},
containerClass
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
linksPrimary
:
{
type
:
Array
,
required
:
false
,
...
...
@@ -50,7 +55,11 @@ export default {
<
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-container gl-w-auto"
:class=
"containerClass"
data-testid=
"frequent-items-container"
>
<div
class=
"frequent-items-dropdown-content gl-w-full! gl-pt-0!"
>
<vuex-module-provider
:vuex-module=
"frequentItemsVuexModule"
>
<frequent-items-app
v-bind=
"$attrs"
/>
...
...
app/assets/javascripts/nav/components/top_nav_menu_item.vue
View file @
78373a26
...
...
@@ -16,6 +16,11 @@ export default {
type
:
Object
,
required
:
true
,
},
iconOnly
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
},
computed
:
{
dataAttrs
()
{
...
...
@@ -32,13 +37,16 @@ export default {
:href=
"menuItem.href"
class=
"top-nav-menu-item gl-display-block"
:class=
"[menuItem.css_class,
{ [$options.ACTIVE_CLASS]: menuItem.active }]"
:aria-label="menuItem.title"
v-bind="dataAttrs"
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"
/>
<gl-icon
v-if=
"menuItem.icon"
:name=
"menuItem.icon"
:class=
"
{ 'gl-mr-2!': !iconOnly }" />
<template
v-if=
"!iconOnly"
>
{{
menuItem
.
title
}}
<gl-icon
v-if=
"menuItem.view"
name=
"chevron-right"
class=
"gl-ml-auto"
/>
</
template
>
</span>
</gl-button>
</template>
app/assets/javascripts/nav/components/top_nav_menu_sections.vue
View file @
78373a26
...
...
@@ -54,6 +54,7 @@ export default {
:key=
"menuItem.id"
:menu-item=
"menuItem"
data-testid=
"menu-item"
class=
"gl-w-full"
:class=
"
{ 'gl-mt-1': menuItemIndex > 0 }"
@click="onClick(menuItem)"
/>
...
...
app/assets/javascripts/nav/components/top_nav_new_dropdown.vue
0 → 100644
View file @
78373a26
<
script
>
import
{
GlDropdown
,
GlDropdownDivider
,
GlDropdownItem
,
GlDropdownSectionHeader
}
from
'
@gitlab/ui
'
;
export
default
{
components
:
{
GlDropdown
,
GlDropdownDivider
,
GlDropdownItem
,
GlDropdownSectionHeader
,
},
props
:
{
viewModel
:
{
type
:
Object
,
required
:
true
,
},
},
computed
:
{
sections
()
{
return
this
.
viewModel
.
menu_sections
||
[];
},
showHeaders
()
{
return
this
.
sections
.
length
>
1
;
},
},
};
</
script
>
<
template
>
<gl-dropdown
toggle-class=
"top-nav-menu-item"
icon=
"plus"
:text=
"viewModel.title"
category=
"tertiary"
text-sr-only
no-caret
right
>
<template
v-for=
"(
{ title, menu_items }, index) in sections">
<gl-dropdown-divider
v-if=
"index > 0"
:key=
"`$
{index}_divider`" data-testid="divider" />
<gl-dropdown-section-header
v-if=
"showHeaders"
:key=
"`$
{index}_header`" data-testid="header">
{{
title
}}
</gl-dropdown-section-header>
<template
v-for=
"menuItem in menu_items"
>
<gl-dropdown-item
:key=
"`$
{index}_item_${menuItem.id}`"
link-class="top-nav-menu-item"
:href="menuItem.href"
data-testid="item"
>
{{
menuItem
.
title
}}
</gl-dropdown-item>
</
template
>
</template>
</gl-dropdown>
</template>
app/assets/javascripts/nav/utils/reset_menu_items_active.js
0 → 100644
View file @
78373a26
const
resetActiveInArray
=
(
arr
)
=>
arr
?.
map
((
menuItem
)
=>
({
...
menuItem
,
active
:
false
}));
/**
* This method sets `active: false` for the menu items within the given nav data.
*
* @returns navData with the menu items updated with `active: false`
*/
export
const
resetMenuItemsActive
=
({
primary
,
secondary
,
...
navData
})
=>
{
return
{
...
navData
,
primary
:
resetActiveInArray
(
primary
),
secondary
:
resetActiveInArray
(
secondary
),
};
};
app/assets/stylesheets/startup/startup-dark.scss
View file @
78373a26
...
...
@@ -497,6 +497,9 @@ body {
color
:
#dbdbdb
;
vertical-align
:
baseline
;
}
.gl-font-sm
{
font-size
:
12px
;
}
.dropdown
{
position
:
relative
;
}
...
...
@@ -2179,6 +2182,12 @@ body.gl-dark {
margin-left
:
0
!
important
;
margin-right
:
0
!
important
;
}
.gl-font-sm
{
font-size
:
0
.75rem
;
}
.gl-font-weight-bold
{
font-weight
:
600
;
}
@import
"startup/cloaking"
;
@include
cloak-startup-scss
(
none
);
app/assets/stylesheets/startup/startup-general.scss
View file @
78373a26
...
...
@@ -482,6 +482,9 @@ body {
color
:
#525252
;
vertical-align
:
baseline
;
}
.gl-font-sm
{
font-size
:
12px
;
}
.dropdown
{
position
:
relative
;
}
...
...
@@ -1962,6 +1965,12 @@ body.sidebar-refactoring
margin-left
:
0
!
important
;
margin-right
:
0
!
important
;
}
.gl-font-sm
{
font-size
:
0
.75rem
;
}
.gl-font-weight-bold
{
font-weight
:
600
;
}
@import
"startup/cloaking"
;
@include
cloak-startup-scss
(
none
);
app/helpers/nav/top_nav_helper.rb
View file @
78373a26
...
...
@@ -4,21 +4,58 @@ module Nav
module
TopNavHelper
PROJECTS_VIEW
=
:projects
GROUPS_VIEW
=
:groups
NEW_VIEW
=
:new
SEARCH_VIEW
=
:search
def
top_nav_view_model
(
project
:,
group
:)
builder
=
::
Gitlab
::
Nav
::
TopNavViewModelBuilder
.
new
if
current_user
build_view_model
(
builder:
builder
,
project:
project
,
group:
group
)
else
build_anonymous_view_model
(
builder:
builder
)
build_base_view_model
(
builder:
builder
,
project:
project
,
group:
group
)
builder
.
build
end
def
top_nav_responsive_view_model
(
project
:,
group
:)
builder
=
::
Gitlab
::
Nav
::
TopNavViewModelBuilder
.
new
build_base_view_model
(
builder:
builder
,
project:
project
,
group:
group
)
new_view_model
=
new_dropdown_view_model
(
project:
project
,
group:
group
)
if
new_view_model
builder
.
add_view
(
NEW_VIEW
,
new_view_model
)
end
if
top_nav_show_search
builder
.
add_view
(
SEARCH_VIEW
,
::
Gitlab
::
Nav
::
TopNavMenuItem
.
build
(
**
top_nav_search_menu_item_attrs
))
end
builder
.
build
end
def
top_nav_show_search
header_link?
(
:search
)
end
def
top_nav_search_menu_item_attrs
{
id:
'search'
,
title:
_
(
'Search'
),
icon:
'search'
,
href:
search_context
.
search_url
}
end
private
def
build_base_view_model
(
builder
:,
project
:,
group
:)
if
current_user
build_view_model
(
builder:
builder
,
project:
project
,
group:
group
)
else
build_anonymous_view_model
(
builder:
builder
)
end
end
def
build_anonymous_view_model
(
builder
:)
# These come from `app/views/layouts/nav/_explore.html.ham`
if
explore_nav_link?
(
:projects
)
...
...
app/views/layouts/header/_default.html.haml
View file @
78373a26
...
...
@@ -6,7 +6,7 @@
%a
.gl-sr-only.gl-accessibility
{
href:
"#content-body"
}
Skip to content
.container-fluid
.header-content
.title-container
{
class:
(
'hide-when-menu-expanded'
if
!
use_top_nav_redesign
)
}
.title-container
.hide-when-menu-expanded
%h1
.title
%span
.gl-sr-only
GitLab
=
link_to
root_path
,
title:
_
(
'Dashboard'
),
id:
'logo'
,
**
tracking_attrs
(
'main_navigation'
,
'click_gitlab_logo_link'
,
'navigation'
)
do
...
...
@@ -33,12 +33,13 @@
%ul
.nav.navbar-nav
-
if
current_user
=
render
'layouts/header/new_dropdown'
,
class:
(
'gl-display-none gl-sm-display-block'
if
use_top_nav_redesign
)
-
if
header_link?
(
:search
)
-
if
top_nav_show_search
-
search_menu_item
=
top_nav_search_menu_item_attrs
%li
.nav-item.d-none.d-lg-block.m-auto
=
render
'layouts/search'
unless
current_controller?
(
:search
)
%li
.nav-item
{
class:
use_top_nav_redesign
?
"gl-display-none gl-sm-display-inline-block gl-lg-display-none"
:
"gl-display-inline-block gl-lg-display-none"
}
=
link_to
search_
context
.
search_url
,
title:
_
(
'Search'
),
aria:
{
label:
_
(
'Search'
)
},
data:
{
toggle:
'tooltip'
,
placement:
'bottom'
,
container:
'body'
}
do
=
sprite_icon
(
'search'
)
=
link_to
search_
menu_item
.
fetch
(
:href
),
title:
search_menu_item
.
fetch
(
:title
),
aria:
{
label:
search_menu_item
.
fetch
(
:title
)
},
data:
{
toggle:
'tooltip'
,
placement:
'bottom'
,
container:
'body'
}
do
=
sprite_icon
(
search_menu_item
.
fetch
(
:icon
)
)
-
if
header_link?
(
:issues
)
=
nav_link
(
path:
'dashboard#issues'
,
html_options:
{
class:
"user-counter"
})
do
=
link_to
assigned_issues_dashboard_path
,
title:
_
(
'Issues'
),
class:
'dashboard-shortcuts-issues'
,
aria:
{
label:
_
(
'Issues'
)
},
...
...
@@ -120,9 +121,9 @@
%button
.navbar-toggler.d-block.d-sm-none
{
type:
'button'
,
class:
(
'gl-border-none!'
if
use_top_nav_redesign
)
}
%span
.sr-only
=
_
(
'Toggle navigation'
)
-
if
use_top_nav_redesign
%span
.more-icon.gl-px-3
%span
.more-icon.gl-px-3
.gl-font-sm.gl-font-weight-bold
%span
.gl-pr-2
=
_
(
'Menu'
)
=
sprite_icon
(
'
dot-grid
'
,
size:
16
)
=
sprite_icon
(
'
hamburger
'
,
size:
16
)
-
else
=
sprite_icon
(
'ellipsis_h'
,
size:
12
,
css_class:
'more-icon'
)
=
sprite_icon
(
'close'
,
size:
12
,
css_class:
'close-icon'
)
...
...
app/views/layouts/nav/_top_nav_responsive.html.haml
View file @
78373a26
-
return
unless
Feature
.
enabled?
(
:combined_menu
,
current_user
,
default_enabled: :yaml
)
-
top_class
=
local_assigns
.
fetch
(
:class
,
nil
)
-
view_model
=
top_nav_view_model
(
project:
@project
,
group:
@group
)
-
view_model
=
top_nav_
responsive_
view_model
(
project:
@project
,
group:
@group
)
.top-nav-responsive
{
class:
top_class
}
#js-top-nav-responsive
{
data:
{
view_model:
view_model
.
to_json
}
}
ee/app/assets/stylesheets/startup/startup-dark.scss
View file @
78373a26
...
...
@@ -497,6 +497,9 @@ body {
color
:
#dbdbdb
;
vertical-align
:
baseline
;
}
.gl-font-sm
{
font-size
:
12px
;
}
.dropdown
{
position
:
relative
;
}
...
...
@@ -2179,6 +2182,12 @@ body.gl-dark {
margin-left
:
0
!
important
;
margin-right
:
0
!
important
;
}
.gl-font-sm
{
font-size
:
0
.75rem
;
}
.gl-font-weight-bold
{
font-weight
:
600
;
}
@import
"startup/cloaking"
;
@include
cloak-startup-scss
(
none
);
ee/app/assets/stylesheets/startup/startup-general.scss
View file @
78373a26
...
...
@@ -482,6 +482,9 @@ body {
color
:
#525252
;
vertical-align
:
baseline
;
}
.gl-font-sm
{
font-size
:
12px
;
}
.dropdown
{
position
:
relative
;
}
...
...
@@ -1962,6 +1965,12 @@ body.sidebar-refactoring
margin-left
:
0
!
important
;
margin-right
:
0
!
important
;
}
.gl-font-sm
{
font-size
:
0
.75rem
;
}
.gl-font-weight-bold
{
font-weight
:
600
;
}
@import
"startup/cloaking"
;
@include
cloak-startup-scss
(
none
);
locale/gitlab.pot
View file @
78373a26
...
...
@@ -34369,6 +34369,9 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
msgid "TopNav|Go back"
msgstr ""
msgid "Topics (optional)"
msgstr ""
...
...
spec/features/nav/top_nav_responsive_spec.rb
View file @
78373a26
...
...
@@ -6,7 +6,6 @@ RSpec.describe 'top nav responsive', :js do
include
MobileHelpers
let_it_be
(
:user
)
{
create
(
:user
)
}
let_it_be
(
:responsive_menu_text
)
{
'Placeholder for responsive top nav'
}
before
do
stub_feature_flags
(
combined_menu:
true
)
...
...
@@ -20,7 +19,9 @@ RSpec.describe 'top nav responsive', :js do
context
'before opened'
do
it
'has page content and hides responsive menu'
,
:aggregate_failures
do
expect
(
page
).
to
have_css
(
'.page-title'
,
text:
'Projects'
)
expect
(
page
).
to
have_no_text
(
responsive_menu_text
)
expect
(
page
).
to
have_link
(
'Dashboard'
,
id:
'logo'
)
expect
(
page
).
to
have_no_css
(
'.top-nav-responsive'
)
end
end
...
...
@@ -31,8 +32,22 @@ RSpec.describe 'top nav responsive', :js do
it
'hides everything and shows responsive menu'
,
:aggregate_failures
do
expect
(
page
).
to
have_no_css
(
'.page-title'
,
text:
'Projects'
)
expect
(
page
).
to
have_link
(
'Dashboard'
,
id:
'logo'
)
expect
(
page
).
to
have_text
(
responsive_menu_text
)
expect
(
page
).
to
have_no_link
(
'Dashboard'
,
id:
'logo'
)
within
'.top-nav-responsive'
do
expect
(
page
).
to
have_link
(
nil
,
href:
search_path
)
expect
(
page
).
to
have_button
(
'Projects'
)
expect
(
page
).
to
have_button
(
'Groups'
)
expect
(
page
).
to
have_link
(
'Snippets'
,
href:
dashboard_snippets_path
)
end
end
it
'has new dropdown'
,
:aggregate_failures
do
click_button
(
'New...'
)
expect
(
page
).
to
have_link
(
'New project'
,
href:
new_project_path
)
expect
(
page
).
to
have_link
(
'New group'
,
href:
new_group_path
)
expect
(
page
).
to
have_link
(
'New snippet'
,
href:
new_snippet_path
)
end
end
end
spec/frontend/nav/components/responsive_app_spec.js
View file @
78373a26
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
range
}
from
'
lodash
'
;
import
ResponsiveApp
from
'
~/nav/components/responsive_app.vue
'
;
import
ResponsiveHeader
from
'
~/nav/components/responsive_header.vue
'
;
import
ResponsiveHome
from
'
~/nav/components/responsive_home.vue
'
;
import
TopNavContainerView
from
'
~/nav/components/top_nav_container_view.vue
'
;
import
eventHub
,
{
EVENT_RESPONSIVE_TOGGLE
}
from
'
~/nav/event_hub
'
;
import
{
resetMenuItemsActive
}
from
'
~/nav/utils/reset_menu_items_active
'
;
import
KeepAliveSlots
from
'
~/vue_shared/components/keep_alive_slots.vue
'
;
import
{
TEST_NAV_DATA
}
from
'
../mock_data
'
;
describe
(
'
~/nav/components/responsive_app.vue
'
,
()
=>
{
...
...
@@ -12,11 +17,19 @@ describe('~/nav/components/responsive_app.vue', () => {
propsData
:
{
navData
:
TEST_NAV_DATA
,
},
stubs
:
{
KeepAliveSlots
,
},
});
};
const
triggerResponsiveToggle
=
()
=>
eventHub
.
$emit
(
EVENT_RESPONSIVE_TOGGLE
);
const
findHome
=
()
=>
wrapper
.
findComponent
(
ResponsiveHome
);
const
findMobileOverlay
=
()
=>
wrapper
.
find
(
'
[data-testid="mobile-overlay"]
'
);
const
findSubviewHeader
=
()
=>
wrapper
.
findComponent
(
ResponsiveHeader
);
const
findSubviewContainer
=
()
=>
wrapper
.
findComponent
(
TopNavContainerView
);
const
hasBodyResponsiveOpen
=
()
=>
document
.
body
.
classList
.
contains
(
'
top-nav-responsive-open
'
);
const
hasMobileOverlayVisible
=
()
=>
findMobileOverlay
().
classes
(
'
mobile-nav-open
'
);
beforeEach
(()
=>
{
// Add test class to reset state + assert that we're adding classes correctly
...
...
@@ -32,6 +45,13 @@ describe('~/nav/components/responsive_app.vue', () => {
createComponent
();
});
it
(
'
shows home by default
'
,
()
=>
{
expect
(
findHome
().
isVisible
()).
toBe
(
true
);
expect
(
findHome
().
props
()).
toEqual
({
navData
:
resetMenuItemsActive
(
TEST_NAV_DATA
),
});
});
it
.
each
`
times | expectation
${
0
}
|
${
false
}
...
...
@@ -45,6 +65,78 @@ describe('~/nav/components/responsive_app.vue', () => {
expect
(
hasBodyResponsiveOpen
()).
toBe
(
expectation
);
},
);
it
.
each
`
events | expectation
${[]}
|
${
false
}
${[
'
bv::dropdown::show
'
]}
|
${
true
}
${[
'
bv::dropdown::show
'
,
'
bv::dropdown::hide
'
]}
|
${
false
}
`
(
'
with root events $events, movile overlay visible = $expectation
'
,
async
({
events
,
expectation
})
=>
{
// `await...reduce(async` is like doing an `forEach(async (...))` excpet it works
await
events
.
reduce
(
async
(
acc
,
evt
)
=>
{
await
acc
;
wrapper
.
vm
.
$root
.
$emit
(
evt
);
await
wrapper
.
vm
.
$nextTick
();
},
Promise
.
resolve
());
expect
(
hasMobileOverlayVisible
()).
toBe
(
expectation
);
},
);
});
const
projectsContainerProps
=
{
containerClass
:
'
gl-px-3
'
,
frequentItemsDropdownType
:
ResponsiveApp
.
FREQUENT_ITEMS_PROJECTS
.
namespace
,
frequentItemsVuexModule
:
ResponsiveApp
.
FREQUENT_ITEMS_PROJECTS
.
vuexModule
,
linksPrimary
:
TEST_NAV_DATA
.
views
.
projects
.
linksPrimary
,
linksSecondary
:
TEST_NAV_DATA
.
views
.
projects
.
linksSecondary
,
};
const
groupsContainerProps
=
{
containerClass
:
'
gl-px-3
'
,
frequentItemsDropdownType
:
ResponsiveApp
.
FREQUENT_ITEMS_GROUPS
.
namespace
,
frequentItemsVuexModule
:
ResponsiveApp
.
FREQUENT_ITEMS_GROUPS
.
vuexModule
,
linksPrimary
:
TEST_NAV_DATA
.
views
.
groups
.
linksPrimary
,
linksSecondary
:
TEST_NAV_DATA
.
views
.
groups
.
linksSecondary
,
};
describe
.
each
`
view | header | containerProps
${
'
projects
'
}
|
${
'
Projects
'
}
|
${
projectsContainerProps
}
${
'
groups
'
}
|
${
'
Groups
'
}
|
${
groupsContainerProps
}
`
(
'
when menu item with $view is clicked
'
,
({
view
,
header
,
containerProps
})
=>
{
beforeEach
(
async
()
=>
{
createComponent
();
findHome
().
vm
.
$emit
(
'
menu-item-click
'
,
{
view
});
await
wrapper
.
vm
.
$nextTick
();
});
it
(
'
shows header
'
,
()
=>
{
expect
(
findSubviewHeader
().
text
()).
toBe
(
header
);
});
it
(
'
shows container subview
'
,
()
=>
{
expect
(
findSubviewContainer
().
props
()).
toEqual
(
containerProps
);
});
it
(
'
hides home
'
,
()
=>
{
expect
(
findHome
().
isVisible
()).
toBe
(
false
);
});
describe
(
'
when header back button is clicked
'
,
()
=>
{
beforeEach
(()
=>
{
findSubviewHeader
().
vm
.
$emit
(
'
menu-item-click
'
,
{
view
:
'
home
'
});
});
it
(
'
shows home
'
,
()
=>
{
expect
(
findHome
().
isVisible
()).
toBe
(
true
);
});
});
});
describe
(
'
when destroyed
'
,
()
=>
{
...
...
spec/frontend/nav/components/responsive_header_spec.js
0 → 100644
View file @
78373a26
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
createMockDirective
,
getBinding
}
from
'
helpers/vue_mock_directive
'
;
import
ResponsiveHeader
from
'
~/nav/components/responsive_header.vue
'
;
import
TopNavMenuItem
from
'
~/nav/components/top_nav_menu_item.vue
'
;
const
TEST_SLOT_CONTENT
=
'
Test slot content
'
;
describe
(
'
~/nav/components/top_nav_menu_sections.vue
'
,
()
=>
{
let
wrapper
;
const
createComponent
=
()
=>
{
wrapper
=
shallowMount
(
ResponsiveHeader
,
{
slots
:
{
default
:
TEST_SLOT_CONTENT
,
},
directives
:
{
GlTooltip
:
createMockDirective
(),
},
});
};
const
findMenuItem
=
()
=>
wrapper
.
findComponent
(
TopNavMenuItem
);
beforeEach
(()
=>
{
createComponent
();
});
afterEach
(()
=>
{
wrapper
.
destroy
();
});
it
(
'
renders slot
'
,
()
=>
{
expect
(
wrapper
.
text
()).
toBe
(
TEST_SLOT_CONTENT
);
});
it
(
'
renders back button
'
,
()
=>
{
const
button
=
findMenuItem
();
const
tooltip
=
getBinding
(
button
.
element
,
'
gl-tooltip
'
).
value
.
title
;
expect
(
tooltip
).
toBe
(
'
Go back
'
);
expect
(
button
.
props
()).
toEqual
({
menuItem
:
{
id
:
'
home
'
,
view
:
'
home
'
,
icon
:
'
angle-left
'
,
},
iconOnly
:
true
,
});
});
it
(
'
emits nothing
'
,
()
=>
{
expect
(
wrapper
.
emitted
()).
toEqual
({});
});
describe
(
'
when back button is clicked
'
,
()
=>
{
beforeEach
(()
=>
{
findMenuItem
().
vm
.
$emit
(
'
click
'
);
});
it
(
'
emits menu-item-click
'
,
()
=>
{
expect
(
wrapper
.
emitted
()).
toEqual
({
'
menu-item-click
'
:
[[{
id
:
'
home
'
,
view
:
'
home
'
,
icon
:
'
angle-left
'
}]],
});
});
});
});
spec/frontend/nav/components/responsive_home_spec.js
0 → 100644
View file @
78373a26
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
createMockDirective
,
getBinding
}
from
'
helpers/vue_mock_directive
'
;
import
ResponsiveHome
from
'
~/nav/components/responsive_home.vue
'
;
import
TopNavMenuItem
from
'
~/nav/components/top_nav_menu_item.vue
'
;
import
TopNavMenuSections
from
'
~/nav/components/top_nav_menu_sections.vue
'
;
import
TopNavNewDropdown
from
'
~/nav/components/top_nav_new_dropdown.vue
'
;
import
{
TEST_NAV_DATA
}
from
'
../mock_data
'
;
const
TEST_SEARCH_MENU_ITEM
=
{
id
:
'
search
'
,
title
:
'
search
'
,
icon
:
'
search
'
,
href
:
'
/search
'
,
};
const
TEST_NEW_DROPDOWN_VIEW_MODEL
=
{
title
:
'
new
'
,
menu_sections
:
[],
};
describe
(
'
~/nav/components/responsive_home.vue
'
,
()
=>
{
let
wrapper
;
let
menuItemClickListener
;
const
createComponent
=
(
props
=
{})
=>
{
wrapper
=
shallowMount
(
ResponsiveHome
,
{
propsData
:
{
navData
:
TEST_NAV_DATA
,
...
props
,
},
directives
:
{
GlTooltip
:
createMockDirective
(),
},
listeners
:
{
'
menu-item-click
'
:
menuItemClickListener
,
},
});
};
const
findSearchMenuItem
=
()
=>
wrapper
.
findComponent
(
TopNavMenuItem
);
const
findNewDropdown
=
()
=>
wrapper
.
findComponent
(
TopNavNewDropdown
);
const
findMenuSections
=
()
=>
wrapper
.
findComponent
(
TopNavMenuSections
);
beforeEach
(()
=>
{
menuItemClickListener
=
jest
.
fn
();
});
afterEach
(()
=>
{
wrapper
.
destroy
();
});
describe
(
'
default
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
();
});
it
.
each
`
desc | fn
${
'
does not show search menu item
'
}
|
${
findSearchMenuItem
}
${
'
does not show new dropdown
'
}
|
${
findNewDropdown
}
`
(
'
$desc
'
,
({
fn
})
=>
{
expect
(
fn
().
exists
()).
toBe
(
false
);
});
it
(
'
shows menu sections
'
,
()
=>
{
expect
(
findMenuSections
().
props
(
'
sections
'
)).
toEqual
([
{
id
:
'
primary
'
,
menuItems
:
TEST_NAV_DATA
.
primary
},
{
id
:
'
secondary
'
,
menuItems
:
TEST_NAV_DATA
.
secondary
},
]);
});
it
(
'
emits when menu sections emits
'
,
()
=>
{
expect
(
menuItemClickListener
).
not
.
toHaveBeenCalled
();
findMenuSections
().
vm
.
$emit
(
'
menu-item-click
'
,
TEST_NAV_DATA
.
primary
[
0
]);
expect
(
menuItemClickListener
).
toHaveBeenCalledWith
(
TEST_NAV_DATA
.
primary
[
0
]);
});
});
describe
(
'
without secondary
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
({
navData
:
{
...
TEST_NAV_DATA
,
secondary
:
null
}
});
});
it
(
'
shows menu sections
'
,
()
=>
{
expect
(
findMenuSections
().
props
(
'
sections
'
)).
toEqual
([
{
id
:
'
primary
'
,
menuItems
:
TEST_NAV_DATA
.
primary
},
]);
});
});
describe
(
'
with search view
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
({
navData
:
{
...
TEST_NAV_DATA
,
views
:
{
search
:
TEST_SEARCH_MENU_ITEM
},
},
});
});
it
(
'
shows search menu item
'
,
()
=>
{
expect
(
findSearchMenuItem
().
props
()).
toEqual
({
menuItem
:
TEST_SEARCH_MENU_ITEM
,
iconOnly
:
true
,
});
});
it
(
'
shows tooltip for search
'
,
()
=>
{
const
tooltip
=
getBinding
(
findSearchMenuItem
().
element
,
'
gl-tooltip
'
);
expect
(
tooltip
.
value
).
toEqual
({
title
:
TEST_SEARCH_MENU_ITEM
.
title
});
});
});
describe
(
'
with new view
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
({
navData
:
{
...
TEST_NAV_DATA
,
views
:
{
new
:
TEST_NEW_DROPDOWN_VIEW_MODEL
},
},
});
});
it
(
'
shows new dropdown
'
,
()
=>
{
expect
(
findNewDropdown
().
props
()).
toEqual
({
viewModel
:
TEST_NEW_DROPDOWN_VIEW_MODEL
,
});
});
it
(
'
shows tooltip for new dropdown
'
,
()
=>
{
const
tooltip
=
getBinding
(
findNewDropdown
().
element
,
'
gl-tooltip
'
);
expect
(
tooltip
.
value
).
toEqual
({
title
:
TEST_NEW_DROPDOWN_VIEW_MODEL
.
title
});
});
});
});
spec/frontend/nav/components/top_nav_container_view_spec.js
View file @
78373a26
...
...
@@ -13,6 +13,7 @@ const DEFAULT_PROPS = {
frequentItemsVuexModule
:
FREQUENT_ITEMS_PROJECTS
.
vuexModule
,
linksPrimary
:
TEST_NAV_DATA
.
primary
,
linksSecondary
:
TEST_NAV_DATA
.
secondary
,
containerClass
:
'
test-frequent-items-container-class
'
,
};
const
TEST_OTHER_PROPS
=
{
namespace
:
'
projects
'
,
...
...
@@ -44,6 +45,7 @@ describe('~/nav/components/top_nav_container_view.vue', () => {
attributes
:
parent
.
findComponent
(
FrequentItemsApp
).
attributes
(),
};
};
const
findFrequentItemsContainer
=
()
=>
wrapper
.
find
(
'
[data-testid="frequent-items-container"]
'
);
afterEach
(()
=>
{
wrapper
.
destroy
();
...
...
@@ -85,6 +87,10 @@ describe('~/nav/components/top_nav_container_view.vue', () => {
});
});
it
(
'
renders given container class
'
,
()
=>
{
expect
(
findFrequentItemsContainer
().
classes
(
DEFAULT_PROPS
.
containerClass
)).
toBe
(
true
);
});
it
(
'
renders menu sections
'
,
()
=>
{
const
sections
=
[
{
id
:
'
primary
'
,
menuItems
:
TEST_NAV_DATA
.
primary
},
...
...
spec/frontend/nav/components/top_nav_menu_item_spec.js
View file @
78373a26
...
...
@@ -30,7 +30,10 @@ describe('~/nav/components/top_nav_menu_item.vue', () => {
const
findButtonIcons
=
()
=>
findButton
()
.
findAllComponents
(
GlIcon
)
.
wrappers
.
map
((
x
)
=>
x
.
props
(
'
name
'
));
.
wrappers
.
map
((
x
)
=>
({
name
:
x
.
props
(
'
name
'
),
classes
:
x
.
classes
(),
}));
beforeEach
(()
=>
{
listener
=
jest
.
fn
();
...
...
@@ -65,11 +68,42 @@ describe('~/nav/components/top_nav_menu_item.vue', () => {
expect
(
listener
).
toHaveBeenCalledWith
(
'
TEST
'
);
});
it
(
'
renders expected icons
'
,
()
=>
{
expect
(
findButtonIcons
()).
toEqual
([
{
name
:
TEST_MENU_ITEM
.
icon
,
classes
:
[
'
gl-mr-2!
'
],
},
{
name
:
'
chevron-right
'
,
classes
:
[
'
gl-ml-auto
'
],
},
]);
});
});
describe
(
'
with icon-only
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
({
iconOnly
:
true
});
});
it
(
'
does not render title or view icon
'
,
()
=>
{
expect
(
wrapper
.
text
()).
toBe
(
''
);
});
it
(
'
only renders menuItem icon
'
,
()
=>
{
expect
(
findButtonIcons
()).
toEqual
([
{
name
:
TEST_MENU_ITEM
.
icon
,
classes
:
[],
},
]);
});
});
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
}
} |
${[]}
...
...
@@ -79,7 +113,7 @@ describe('~/nav/components/top_nav_menu_item.vue', () => {
});
it
(
`renders expected icons
${
JSON
.
stringify
(
expectedIcons
)}
`
,
()
=>
{
expect
(
findButtonIcons
()).
toEqual
(
expectedIcons
);
expect
(
findButtonIcons
()
.
map
((
x
)
=>
x
.
name
)
).
toEqual
(
expectedIcons
);
});
});
...
...
spec/frontend/nav/components/top_nav_menu_sections_spec.js
View file @
78373a26
...
...
@@ -51,11 +51,11 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
menuItems
:
[
{
menuItem
:
TEST_SECTIONS
[
0
].
menuItems
[
0
],
classes
:
[],
classes
:
[
'
gl-w-full
'
],
},
...
TEST_SECTIONS
[
0
].
menuItems
.
slice
(
1
).
map
((
menuItem
)
=>
({
menuItem
,
classes
:
[
'
gl-mt-1
'
],
classes
:
[
'
gl-
w-full
'
,
'
gl-
mt-1
'
],
})),
],
},
...
...
@@ -64,11 +64,11 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
menuItems
:
[
{
menuItem
:
TEST_SECTIONS
[
1
].
menuItems
[
0
],
classes
:
[],
classes
:
[
'
gl-w-full
'
],
},
...
TEST_SECTIONS
[
1
].
menuItems
.
slice
(
1
).
map
((
menuItem
)
=>
({
menuItem
,
classes
:
[
'
gl-mt-1
'
],
classes
:
[
'
gl-
w-full
'
,
'
gl-
mt-1
'
],
})),
],
},
...
...
spec/frontend/nav/components/top_nav_new_dropdown_spec.js
0 → 100644
View file @
78373a26
import
{
GlDropdown
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
TopNavNewDropdown
from
'
~/nav/components/top_nav_new_dropdown.vue
'
;
const
TEST_VIEW_MODEL
=
{
title
:
'
Dropdown
'
,
menu_sections
:
[
{
title
:
'
Section 1
'
,
menu_items
:
[
{
id
:
'
foo-1
'
,
title
:
'
Foo 1
'
,
href
:
'
/foo/1
'
},
{
id
:
'
foo-2
'
,
title
:
'
Foo 2
'
,
href
:
'
/foo/2
'
},
{
id
:
'
foo-3
'
,
title
:
'
Foo 3
'
,
href
:
'
/foo/3
'
},
],
},
{
title
:
'
Section 2
'
,
menu_items
:
[
{
id
:
'
bar-1
'
,
title
:
'
Bar 1
'
,
href
:
'
/bar/1
'
},
{
id
:
'
bar-2
'
,
title
:
'
Bar 2
'
,
href
:
'
/bar/2
'
},
],
},
],
};
describe
(
'
~/nav/components/top_nav_menu_sections.vue
'
,
()
=>
{
let
wrapper
;
const
createComponent
=
(
props
=
{})
=>
{
wrapper
=
shallowMount
(
TopNavNewDropdown
,
{
propsData
:
{
viewModel
:
TEST_VIEW_MODEL
,
...
props
,
},
});
};
const
findDropdown
=
()
=>
wrapper
.
findComponent
(
GlDropdown
);
const
findDropdownContents
=
()
=>
findDropdown
()
.
findAll
(
'
[data-testid]
'
)
.
wrappers
.
map
((
child
)
=>
{
const
type
=
child
.
attributes
(
'
data-testid
'
);
if
(
type
===
'
divider
'
)
{
return
{
type
};
}
else
if
(
type
===
'
header
'
)
{
return
{
type
,
text
:
child
.
text
()
};
}
return
{
type
,
text
:
child
.
text
(),
href
:
child
.
attributes
(
'
href
'
),
};
});
afterEach
(()
=>
{
wrapper
.
destroy
();
});
describe
(
'
default
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
();
});
it
(
'
renders dropdown parent
'
,
()
=>
{
expect
(
findDropdown
().
props
()).
toMatchObject
({
text
:
TEST_VIEW_MODEL
.
title
,
textSrOnly
:
true
,
icon
:
'
plus
'
,
});
});
it
(
'
renders dropdown content
'
,
()
=>
{
expect
(
findDropdownContents
()).
toEqual
([
{
type
:
'
header
'
,
text
:
TEST_VIEW_MODEL
.
menu_sections
[
0
].
title
,
},
...
TEST_VIEW_MODEL
.
menu_sections
[
0
].
menu_items
.
map
(({
title
,
href
})
=>
({
type
:
'
item
'
,
href
,
text
:
title
,
})),
{
type
:
'
divider
'
,
},
{
type
:
'
header
'
,
text
:
TEST_VIEW_MODEL
.
menu_sections
[
1
].
title
,
},
...
TEST_VIEW_MODEL
.
menu_sections
[
1
].
menu_items
.
map
(({
title
,
href
})
=>
({
type
:
'
item
'
,
href
,
text
:
title
,
})),
]);
});
});
describe
(
'
with only 1 section
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
({
viewModel
:
{
...
TEST_VIEW_MODEL
,
menu_sections
:
TEST_VIEW_MODEL
.
menu_sections
.
slice
(
0
,
1
),
},
});
});
it
(
'
renders dropdown content without headers and dividers
'
,
()
=>
{
expect
(
findDropdownContents
()).
toEqual
(
TEST_VIEW_MODEL
.
menu_sections
[
0
].
menu_items
.
map
(({
title
,
href
})
=>
({
type
:
'
item
'
,
href
,
text
:
title
,
})),
);
});
});
});
spec/frontend/nav/mock_data.js
View file @
78373a26
...
...
@@ -25,11 +25,15 @@ export const TEST_NAV_DATA = {
namespace
:
'
projects
'
,
currentUserName
:
''
,
currentItem
:
{},
linksPrimary
:
[{
id
:
'
project-link
'
,
href
:
'
/path/to/projects
'
,
title
:
'
Project Link
'
}],
linksSecondary
:
[],
},
groups
:
{
namespace
:
'
groups
'
,
currentUserName
:
''
,
currentItem
:
{},
linksPrimary
:
[],
linksSecondary
:
[{
id
:
'
group-link
'
,
href
:
'
/path/to/groups
'
,
title
:
'
Group Link
'
}],
},
},
};
spec/helpers/nav/top_nav_helper_spec.rb
View file @
78373a26
...
...
@@ -5,11 +5,16 @@ require 'spec_helper'
RSpec
.
describe
Nav
::
TopNavHelper
do
include
ActionView
::
Helpers
::
UrlHelper
describe
'#top_nav_view_model'
do
let_it_be
(
:user
)
{
build_stubbed
(
:user
)
}
let_it_be
(
:admin
)
{
build_stubbed
(
:user
,
:admin
)
}
let_it_be
(
:user
)
{
build_stubbed
(
:user
)
}
let_it_be
(
:admin
)
{
build_stubbed
(
:user
,
:admin
)
}
let
(
:current_user
)
{
nil
}
before
do
allow
(
helper
).
to
receive
(
:current_user
)
{
current_user
}
end
let
(
:current_user
)
{
nil
}
describe
'#top_nav_view_model'
do
let
(
:current_project
)
{
nil
}
let
(
:current_group
)
{
nil
}
let
(
:with_current_settings_admin_mode
)
{
false
}
...
...
@@ -26,7 +31,6 @@ RSpec.describe Nav::TopNavHelper do
let
(
:active_title
)
{
'Menu'
}
before
do
allow
(
helper
).
to
receive
(
:current_user
)
{
current_user
}
allow
(
Gitlab
::
CurrentSettings
).
to
receive
(
:admin_mode
)
{
with_current_settings_admin_mode
}
allow
(
helper
).
to
receive
(
:header_link?
).
with
(
:admin_mode
)
{
with_header_link_admin_mode
}
allow
(
Gitlab
::
Sherlock
).
to
receive
(
:enabled?
)
{
with_sherlock_enabled
}
...
...
@@ -487,4 +491,50 @@ RSpec.describe Nav::TopNavHelper do
end
end
end
describe
'#top_nav_responsive_view_model'
do
let_it_be
(
:project
)
{
create
(
:project
)
}
let_it_be
(
:group
)
{
create
(
:group
)
}
let
(
:with_search
)
{
false
}
let
(
:with_new_view_model
)
{
nil
}
let
(
:subject
)
{
helper
.
top_nav_responsive_view_model
(
project:
project
,
group:
group
)
}
before
do
allow
(
helper
).
to
receive
(
:header_link?
).
with
(
:search
)
{
with_search
}
allow
(
helper
).
to
receive
(
:new_dropdown_view_model
).
with
(
project:
project
,
group:
group
)
{
with_new_view_model
}
end
it
'has nil new subview'
do
expect
(
subject
[
:views
][
:new
]).
to
be_nil
end
it
'has nil search subview'
do
expect
(
subject
[
:views
][
:search
]).
to
be_nil
end
context
'with search'
do
let
(
:with_search
)
{
true
}
it
'has search subview'
do
expect
(
subject
[
:views
][
:search
]).
to
eq
(
::
Gitlab
::
Nav
::
TopNavMenuItem
.
build
(
id:
'search'
,
title:
'Search'
,
icon:
'search'
,
href:
search_path
)
)
end
end
context
'with new'
do
let
(
:with_new_view_model
)
{
{
id:
'test-new-view-model'
}
}
it
'has new subview'
do
expect
(
subject
[
:views
][
:new
]).
to
eq
({
id:
'test-new-view-model'
})
end
end
end
end
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