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
>
<
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
'
;
import
eventHub
,
{
EVENT_RESPONSIVE_TOGGLE
}
from
'
../event_hub
'
;
import
{
resetMenuItemsActive
}
from
'
../utils/reset_menu_items_active
'
;
const
TEMPORARY_PLACEHOLDER
=
'
Placeholder for responsive top nav
'
;
import
ResponsiveHeader
from
'
./responsive_header.vue
'
;
import
ResponsiveHome
from
'
./responsive_home.vue
'
;
import
TopNavContainerView
from
'
./top_nav_container_view.vue
'
;
export
default
{
export
default
{
components
:
{
KeepAliveSlots
,
ResponsiveHeader
,
ResponsiveHome
,
TopNavContainerView
,
},
props
:
{
props
:
{
navData
:
{
navData
:
{
type
:
Object
,
type
:
Object
,
required
:
true
,
required
:
true
,
},
},
},
},
data
()
{
return
{
activeView
:
'
home
'
,
hasMobileOverlay
:
false
,
};
},
computed
:
{
nav
()
{
return
resetMenuItemsActive
(
this
.
navData
);
},
},
created
()
{
created
()
{
eventHub
.
$on
(
EVENT_RESPONSIVE_TOGGLE
,
this
.
onToggle
);
eventHub
.
$on
(
EVENT_RESPONSIVE_TOGGLE
,
this
.
onToggle
);
this
.
$root
.
$on
(
BV_DROPDOWN_SHOW
,
this
.
showMobileOverlay
);
this
.
$root
.
$on
(
BV_DROPDOWN_HIDE
,
this
.
hideMobileOverlay
);
},
},
beforeDestroy
()
{
beforeDestroy
()
{
eventHub
.
$off
(
EVENT_RESPONSIVE_TOGGLE
,
this
.
onToggle
);
eventHub
.
$off
(
EVENT_RESPONSIVE_TOGGLE
,
this
.
onToggle
);
this
.
$root
.
$off
(
BV_DROPDOWN_SHOW
,
this
.
showMobileOverlay
);
this
.
$root
.
$off
(
BV_DROPDOWN_HIDE
,
this
.
hideMobileOverlay
);
},
},
methods
:
{
methods
:
{
onToggle
()
{
onToggle
()
{
document
.
body
.
classList
.
toggle
(
'
top-nav-responsive-open
'
);
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
>
</
script
>
<
template
>
<
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>
</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 {
...
@@ -20,6 +20,11 @@ export default {
type
:
String
,
type
:
String
,
required
:
true
,
required
:
true
,
},
},
containerClass
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
linksPrimary
:
{
linksPrimary
:
{
type
:
Array
,
type
:
Array
,
required
:
false
,
required
:
false
,
...
@@ -50,7 +55,11 @@ export default {
...
@@ -50,7 +55,11 @@ export default {
<
template
>
<
template
>
<div
class=
"top-nav-container-view gl-display-flex gl-flex-direction-column"
>
<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!"
>
<div
class=
"frequent-items-dropdown-content gl-w-full! gl-pt-0!"
>
<vuex-module-provider
:vuex-module=
"frequentItemsVuexModule"
>
<vuex-module-provider
:vuex-module=
"frequentItemsVuexModule"
>
<frequent-items-app
v-bind=
"$attrs"
/>
<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 {
...
@@ -16,6 +16,11 @@ export default {
type
:
Object
,
type
:
Object
,
required
:
true
,
required
:
true
,
},
},
iconOnly
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
},
},
computed
:
{
computed
:
{
dataAttrs
()
{
dataAttrs
()
{
...
@@ -32,13 +37,16 @@ export default {
...
@@ -32,13 +37,16 @@ export default {
:href=
"menuItem.href"
:href=
"menuItem.href"
class=
"top-nav-menu-item gl-display-block"
class=
"top-nav-menu-item gl-display-block"
:class=
"[menuItem.css_class,
{ [$options.ACTIVE_CLASS]: menuItem.active }]"
:class=
"[menuItem.css_class,
{ [$options.ACTIVE_CLASS]: menuItem.active }]"
:aria-label="menuItem.title"
v-bind="dataAttrs"
v-bind="dataAttrs"
v-on="$listeners"
v-on="$listeners"
>
>
<span
class=
"gl-display-flex"
>
<span
class=
"gl-display-flex"
>
<gl-icon
v-if=
"menuItem.icon"
:name=
"menuItem.icon"
class=
"gl-mr-2!"
/>
<gl-icon
v-if=
"menuItem.icon"
:name=
"menuItem.icon"
:class=
"
{ 'gl-mr-2!': !iconOnly }" />
{{
menuItem
.
title
}}
<template
v-if=
"!iconOnly"
>
<gl-icon
v-if=
"menuItem.view"
name=
"chevron-right"
class=
"gl-ml-auto"
/>
{{
menuItem
.
title
}}
<gl-icon
v-if=
"menuItem.view"
name=
"chevron-right"
class=
"gl-ml-auto"
/>
</
template
>
</span>
</span>
</gl-button>
</gl-button>
</template>
</template>
app/assets/javascripts/nav/components/top_nav_menu_sections.vue
View file @
78373a26
...
@@ -54,6 +54,7 @@ export default {
...
@@ -54,6 +54,7 @@ export default {
:key=
"menuItem.id"
:key=
"menuItem.id"
:menu-item=
"menuItem"
:menu-item=
"menuItem"
data-testid=
"menu-item"
data-testid=
"menu-item"
class=
"gl-w-full"
:class=
"
{ 'gl-mt-1': menuItemIndex > 0 }"
:class=
"
{ 'gl-mt-1': menuItemIndex > 0 }"
@click="onClick(menuItem)"
@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 {
...
@@ -497,6 +497,9 @@ body {
color
:
#dbdbdb
;
color
:
#dbdbdb
;
vertical-align
:
baseline
;
vertical-align
:
baseline
;
}
}
.gl-font-sm
{
font-size
:
12px
;
}
.dropdown
{
.dropdown
{
position
:
relative
;
position
:
relative
;
}
}
...
@@ -2179,6 +2182,12 @@ body.gl-dark {
...
@@ -2179,6 +2182,12 @@ body.gl-dark {
margin-left
:
0
!
important
;
margin-left
:
0
!
important
;
margin-right
:
0
!
important
;
margin-right
:
0
!
important
;
}
}
.gl-font-sm
{
font-size
:
0
.75rem
;
}
.gl-font-weight-bold
{
font-weight
:
600
;
}
@import
"startup/cloaking"
;
@import
"startup/cloaking"
;
@include
cloak-startup-scss
(
none
);
@include
cloak-startup-scss
(
none
);
app/assets/stylesheets/startup/startup-general.scss
View file @
78373a26
...
@@ -482,6 +482,9 @@ body {
...
@@ -482,6 +482,9 @@ body {
color
:
#525252
;
color
:
#525252
;
vertical-align
:
baseline
;
vertical-align
:
baseline
;
}
}
.gl-font-sm
{
font-size
:
12px
;
}
.dropdown
{
.dropdown
{
position
:
relative
;
position
:
relative
;
}
}
...
@@ -1962,6 +1965,12 @@ body.sidebar-refactoring
...
@@ -1962,6 +1965,12 @@ body.sidebar-refactoring
margin-left
:
0
!
important
;
margin-left
:
0
!
important
;
margin-right
:
0
!
important
;
margin-right
:
0
!
important
;
}
}
.gl-font-sm
{
font-size
:
0
.75rem
;
}
.gl-font-weight-bold
{
font-weight
:
600
;
}
@import
"startup/cloaking"
;
@import
"startup/cloaking"
;
@include
cloak-startup-scss
(
none
);
@include
cloak-startup-scss
(
none
);
app/helpers/nav/top_nav_helper.rb
View file @
78373a26
...
@@ -4,21 +4,58 @@ module Nav
...
@@ -4,21 +4,58 @@ module Nav
module
TopNavHelper
module
TopNavHelper
PROJECTS_VIEW
=
:projects
PROJECTS_VIEW
=
:projects
GROUPS_VIEW
=
:groups
GROUPS_VIEW
=
:groups
NEW_VIEW
=
:new
SEARCH_VIEW
=
:search
def
top_nav_view_model
(
project
:,
group
:)
def
top_nav_view_model
(
project
:,
group
:)
builder
=
::
Gitlab
::
Nav
::
TopNavViewModelBuilder
.
new
builder
=
::
Gitlab
::
Nav
::
TopNavViewModelBuilder
.
new
if
current_user
build_base_view_model
(
builder:
builder
,
project:
project
,
group:
group
)
build_view_model
(
builder:
builder
,
project:
project
,
group:
group
)
else
builder
.
build
build_anonymous_view_model
(
builder:
builder
)
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
end
builder
.
build
builder
.
build
end
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
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
:)
def
build_anonymous_view_model
(
builder
:)
# These come from `app/views/layouts/nav/_explore.html.ham`
# These come from `app/views/layouts/nav/_explore.html.ham`
if
explore_nav_link?
(
:projects
)
if
explore_nav_link?
(
:projects
)
...
...
app/views/layouts/header/_default.html.haml
View file @
78373a26
...
@@ -6,7 +6,7 @@
...
@@ -6,7 +6,7 @@
%a
.gl-sr-only.gl-accessibility
{
href:
"#content-body"
}
Skip to content
%a
.gl-sr-only.gl-accessibility
{
href:
"#content-body"
}
Skip to content
.container-fluid
.container-fluid
.header-content
.header-content
.title-container
{
class:
(
'hide-when-menu-expanded'
if
!
use_top_nav_redesign
)
}
.title-container
.hide-when-menu-expanded
%h1
.title
%h1
.title
%span
.gl-sr-only
GitLab
%span
.gl-sr-only
GitLab
=
link_to
root_path
,
title:
_
(
'Dashboard'
),
id:
'logo'
,
**
tracking_attrs
(
'main_navigation'
,
'click_gitlab_logo_link'
,
'navigation'
)
do
=
link_to
root_path
,
title:
_
(
'Dashboard'
),
id:
'logo'
,
**
tracking_attrs
(
'main_navigation'
,
'click_gitlab_logo_link'
,
'navigation'
)
do
...
@@ -33,12 +33,13 @@
...
@@ -33,12 +33,13 @@
%ul
.nav.navbar-nav
%ul
.nav.navbar-nav
-
if
current_user
-
if
current_user
=
render
'layouts/header/new_dropdown'
,
class:
(
'gl-display-none gl-sm-display-block'
if
use_top_nav_redesign
)
=
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
%li
.nav-item.d-none.d-lg-block.m-auto
=
render
'layouts/search'
unless
current_controller?
(
:search
)
=
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"
}
%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
=
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'
)
=
sprite_icon
(
search_menu_item
.
fetch
(
:icon
)
)
-
if
header_link?
(
:issues
)
-
if
header_link?
(
:issues
)
=
nav_link
(
path:
'dashboard#issues'
,
html_options:
{
class:
"user-counter"
})
do
=
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'
)
},
=
link_to
assigned_issues_dashboard_path
,
title:
_
(
'Issues'
),
class:
'dashboard-shortcuts-issues'
,
aria:
{
label:
_
(
'Issues'
)
},
...
@@ -120,9 +121,9 @@
...
@@ -120,9 +121,9 @@
%button
.navbar-toggler.d-block.d-sm-none
{
type:
'button'
,
class:
(
'gl-border-none!'
if
use_top_nav_redesign
)
}
%button
.navbar-toggler.d-block.d-sm-none
{
type:
'button'
,
class:
(
'gl-border-none!'
if
use_top_nav_redesign
)
}
%span
.sr-only
=
_
(
'Toggle navigation'
)
%span
.sr-only
=
_
(
'Toggle navigation'
)
-
if
use_top_nav_redesign
-
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'
)
%span
.gl-pr-2
=
_
(
'Menu'
)
=
sprite_icon
(
'
dot-grid
'
,
size:
16
)
=
sprite_icon
(
'
hamburger
'
,
size:
16
)
-
else
-
else
=
sprite_icon
(
'ellipsis_h'
,
size:
12
,
css_class:
'more-icon'
)
=
sprite_icon
(
'ellipsis_h'
,
size:
12
,
css_class:
'more-icon'
)
=
sprite_icon
(
'close'
,
size:
12
,
css_class:
'close-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
)
-
return
unless
Feature
.
enabled?
(
:combined_menu
,
current_user
,
default_enabled: :yaml
)
-
top_class
=
local_assigns
.
fetch
(
:class
,
nil
)
-
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
}
.top-nav-responsive
{
class:
top_class
}
#js-top-nav-responsive
{
data:
{
view_model:
view_model
.
to_json
}
}
#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 {
...
@@ -497,6 +497,9 @@ body {
color
:
#dbdbdb
;
color
:
#dbdbdb
;
vertical-align
:
baseline
;
vertical-align
:
baseline
;
}
}
.gl-font-sm
{
font-size
:
12px
;
}
.dropdown
{
.dropdown
{
position
:
relative
;
position
:
relative
;
}
}
...
@@ -2179,6 +2182,12 @@ body.gl-dark {
...
@@ -2179,6 +2182,12 @@ body.gl-dark {
margin-left
:
0
!
important
;
margin-left
:
0
!
important
;
margin-right
:
0
!
important
;
margin-right
:
0
!
important
;
}
}
.gl-font-sm
{
font-size
:
0
.75rem
;
}
.gl-font-weight-bold
{
font-weight
:
600
;
}
@import
"startup/cloaking"
;
@import
"startup/cloaking"
;
@include
cloak-startup-scss
(
none
);
@include
cloak-startup-scss
(
none
);
ee/app/assets/stylesheets/startup/startup-general.scss
View file @
78373a26
...
@@ -482,6 +482,9 @@ body {
...
@@ -482,6 +482,9 @@ body {
color
:
#525252
;
color
:
#525252
;
vertical-align
:
baseline
;
vertical-align
:
baseline
;
}
}
.gl-font-sm
{
font-size
:
12px
;
}
.dropdown
{
.dropdown
{
position
:
relative
;
position
:
relative
;
}
}
...
@@ -1962,6 +1965,12 @@ body.sidebar-refactoring
...
@@ -1962,6 +1965,12 @@ body.sidebar-refactoring
margin-left
:
0
!
important
;
margin-left
:
0
!
important
;
margin-right
:
0
!
important
;
margin-right
:
0
!
important
;
}
}
.gl-font-sm
{
font-size
:
0
.75rem
;
}
.gl-font-weight-bold
{
font-weight
:
600
;
}
@import
"startup/cloaking"
;
@import
"startup/cloaking"
;
@include
cloak-startup-scss
(
none
);
@include
cloak-startup-scss
(
none
);
locale/gitlab.pot
View file @
78373a26
...
@@ -34369,6 +34369,9 @@ msgstr ""
...
@@ -34369,6 +34369,9 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
msgstr ""
msgid "TopNav|Go back"
msgstr ""
msgid "Topics (optional)"
msgid "Topics (optional)"
msgstr ""
msgstr ""
...
...
spec/features/nav/top_nav_responsive_spec.rb
View file @
78373a26
...
@@ -6,7 +6,6 @@ RSpec.describe 'top nav responsive', :js do
...
@@ -6,7 +6,6 @@ RSpec.describe 'top nav responsive', :js do
include
MobileHelpers
include
MobileHelpers
let_it_be
(
:user
)
{
create
(
:user
)
}
let_it_be
(
:user
)
{
create
(
:user
)
}
let_it_be
(
:responsive_menu_text
)
{
'Placeholder for responsive top nav'
}
before
do
before
do
stub_feature_flags
(
combined_menu:
true
)
stub_feature_flags
(
combined_menu:
true
)
...
@@ -20,7 +19,9 @@ RSpec.describe 'top nav responsive', :js do
...
@@ -20,7 +19,9 @@ RSpec.describe 'top nav responsive', :js do
context
'before opened'
do
context
'before opened'
do
it
'has page content and hides responsive menu'
,
:aggregate_failures
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_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
end
end
...
@@ -31,8 +32,22 @@ RSpec.describe 'top nav responsive', :js do
...
@@ -31,8 +32,22 @@ RSpec.describe 'top nav responsive', :js do
it
'hides everything and shows responsive menu'
,
:aggregate_failures
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_no_css
(
'.page-title'
,
text:
'Projects'
)
expect
(
page
).
to
have_link
(
'Dashboard'
,
id:
'logo'
)
expect
(
page
).
to
have_no_link
(
'Dashboard'
,
id:
'logo'
)
expect
(
page
).
to
have_text
(
responsive_menu_text
)
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
end
end
end
spec/frontend/nav/components/responsive_app_spec.js
View file @
78373a26
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
range
}
from
'
lodash
'
;
import
{
range
}
from
'
lodash
'
;
import
ResponsiveApp
from
'
~/nav/components/responsive_app.vue
'
;
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
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
'
;
import
{
TEST_NAV_DATA
}
from
'
../mock_data
'
;
describe
(
'
~/nav/components/responsive_app.vue
'
,
()
=>
{
describe
(
'
~/nav/components/responsive_app.vue
'
,
()
=>
{
...
@@ -12,11 +17,19 @@ describe('~/nav/components/responsive_app.vue', () => {
...
@@ -12,11 +17,19 @@ describe('~/nav/components/responsive_app.vue', () => {
propsData
:
{
propsData
:
{
navData
:
TEST_NAV_DATA
,
navData
:
TEST_NAV_DATA
,
},
},
stubs
:
{
KeepAliveSlots
,
},
});
});
};
};
const
triggerResponsiveToggle
=
()
=>
eventHub
.
$emit
(
EVENT_RESPONSIVE_TOGGLE
);
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
hasBodyResponsiveOpen
=
()
=>
document
.
body
.
classList
.
contains
(
'
top-nav-responsive-open
'
);
const
hasMobileOverlayVisible
=
()
=>
findMobileOverlay
().
classes
(
'
mobile-nav-open
'
);
beforeEach
(()
=>
{
beforeEach
(()
=>
{
// Add test class to reset state + assert that we're adding classes correctly
// Add test class to reset state + assert that we're adding classes correctly
...
@@ -32,6 +45,13 @@ describe('~/nav/components/responsive_app.vue', () => {
...
@@ -32,6 +45,13 @@ describe('~/nav/components/responsive_app.vue', () => {
createComponent
();
createComponent
();
});
});
it
(
'
shows home by default
'
,
()
=>
{
expect
(
findHome
().
isVisible
()).
toBe
(
true
);
expect
(
findHome
().
props
()).
toEqual
({
navData
:
resetMenuItemsActive
(
TEST_NAV_DATA
),
});
});
it
.
each
`
it
.
each
`
times | expectation
times | expectation
${
0
}
|
${
false
}
${
0
}
|
${
false
}
...
@@ -45,6 +65,78 @@ describe('~/nav/components/responsive_app.vue', () => {
...
@@ -45,6 +65,78 @@ describe('~/nav/components/responsive_app.vue', () => {
expect
(
hasBodyResponsiveOpen
()).
toBe
(
expectation
);
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
'
,
()
=>
{
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 = {
...
@@ -13,6 +13,7 @@ const DEFAULT_PROPS = {
frequentItemsVuexModule
:
FREQUENT_ITEMS_PROJECTS
.
vuexModule
,
frequentItemsVuexModule
:
FREQUENT_ITEMS_PROJECTS
.
vuexModule
,
linksPrimary
:
TEST_NAV_DATA
.
primary
,
linksPrimary
:
TEST_NAV_DATA
.
primary
,
linksSecondary
:
TEST_NAV_DATA
.
secondary
,
linksSecondary
:
TEST_NAV_DATA
.
secondary
,
containerClass
:
'
test-frequent-items-container-class
'
,
};
};
const
TEST_OTHER_PROPS
=
{
const
TEST_OTHER_PROPS
=
{
namespace
:
'
projects
'
,
namespace
:
'
projects
'
,
...
@@ -44,6 +45,7 @@ describe('~/nav/components/top_nav_container_view.vue', () => {
...
@@ -44,6 +45,7 @@ describe('~/nav/components/top_nav_container_view.vue', () => {
attributes
:
parent
.
findComponent
(
FrequentItemsApp
).
attributes
(),
attributes
:
parent
.
findComponent
(
FrequentItemsApp
).
attributes
(),
};
};
};
};
const
findFrequentItemsContainer
=
()
=>
wrapper
.
find
(
'
[data-testid="frequent-items-container"]
'
);
afterEach
(()
=>
{
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
.
destroy
();
...
@@ -85,6 +87,10 @@ describe('~/nav/components/top_nav_container_view.vue', () => {
...
@@ -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
'
,
()
=>
{
it
(
'
renders menu sections
'
,
()
=>
{
const
sections
=
[
const
sections
=
[
{
id
:
'
primary
'
,
menuItems
:
TEST_NAV_DATA
.
primary
},
{
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', () => {
...
@@ -30,7 +30,10 @@ describe('~/nav/components/top_nav_menu_item.vue', () => {
const
findButtonIcons
=
()
=>
const
findButtonIcons
=
()
=>
findButton
()
findButton
()
.
findAllComponents
(
GlIcon
)
.
findAllComponents
(
GlIcon
)
.
wrappers
.
map
((
x
)
=>
x
.
props
(
'
name
'
));
.
wrappers
.
map
((
x
)
=>
({
name
:
x
.
props
(
'
name
'
),
classes
:
x
.
classes
(),
}));
beforeEach
(()
=>
{
beforeEach
(()
=>
{
listener
=
jest
.
fn
();
listener
=
jest
.
fn
();
...
@@ -65,11 +68,42 @@ describe('~/nav/components/top_nav_menu_item.vue', () => {
...
@@ -65,11 +68,42 @@ describe('~/nav/components/top_nav_menu_item.vue', () => {
expect
(
listener
).
toHaveBeenCalledWith
(
'
TEST
'
);
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
`
describe
.
each
`
desc | menuItem | expectedIcons
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 icon
'
}
|
${{
...
TEST_MENU_ITEM
,
icon
:
null
}
} |
${[
'
chevron-right
'
]}
${
'
with no view
'
}
|
${{
...
TEST_MENU_ITEM
,
view
:
null
}
} |
${[
TEST_MENU_ITEM
.
icon
]}
${
'
with no view
'
}
|
${{
...
TEST_MENU_ITEM
,
view
:
null
}
} |
${[
TEST_MENU_ITEM
.
icon
]}
${
'
with no icon or view
'
}
|
${{
...
TEST_MENU_ITEM
,
view
:
null
,
icon
:
null
}
} |
${[]}
${
'
with no icon or view
'
}
|
${{
...
TEST_MENU_ITEM
,
view
:
null
,
icon
:
null
}
} |
${[]}
...
@@ -79,7 +113,7 @@ describe('~/nav/components/top_nav_menu_item.vue', () => {
...
@@ -79,7 +113,7 @@ describe('~/nav/components/top_nav_menu_item.vue', () => {
});
});
it
(
`renders expected icons
${
JSON
.
stringify
(
expectedIcons
)}
`
,
()
=>
{
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', () => {
...
@@ -51,11 +51,11 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
menuItems
:
[
menuItems
:
[
{
{
menuItem
:
TEST_SECTIONS
[
0
].
menuItems
[
0
],
menuItem
:
TEST_SECTIONS
[
0
].
menuItems
[
0
],
classes
:
[],
classes
:
[
'
gl-w-full
'
],
},
},
...
TEST_SECTIONS
[
0
].
menuItems
.
slice
(
1
).
map
((
menuItem
)
=>
({
...
TEST_SECTIONS
[
0
].
menuItems
.
slice
(
1
).
map
((
menuItem
)
=>
({
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', () => {
...
@@ -64,11 +64,11 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
menuItems
:
[
menuItems
:
[
{
{
menuItem
:
TEST_SECTIONS
[
1
].
menuItems
[
0
],
menuItem
:
TEST_SECTIONS
[
1
].
menuItems
[
0
],
classes
:
[],
classes
:
[
'
gl-w-full
'
],
},
},
...
TEST_SECTIONS
[
1
].
menuItems
.
slice
(
1
).
map
((
menuItem
)
=>
({
...
TEST_SECTIONS
[
1
].
menuItems
.
slice
(
1
).
map
((
menuItem
)
=>
({
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 = {
...
@@ -25,11 +25,15 @@ export const TEST_NAV_DATA = {
namespace
:
'
projects
'
,
namespace
:
'
projects
'
,
currentUserName
:
''
,
currentUserName
:
''
,
currentItem
:
{},
currentItem
:
{},
linksPrimary
:
[{
id
:
'
project-link
'
,
href
:
'
/path/to/projects
'
,
title
:
'
Project Link
'
}],
linksSecondary
:
[],
},
},
groups
:
{
groups
:
{
namespace
:
'
groups
'
,
namespace
:
'
groups
'
,
currentUserName
:
''
,
currentUserName
:
''
,
currentItem
:
{},
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'
...
@@ -5,11 +5,16 @@ require 'spec_helper'
RSpec
.
describe
Nav
::
TopNavHelper
do
RSpec
.
describe
Nav
::
TopNavHelper
do
include
ActionView
::
Helpers
::
UrlHelper
include
ActionView
::
Helpers
::
UrlHelper
describe
'#top_nav_view_model'
do
let_it_be
(
:user
)
{
build_stubbed
(
:user
)
}
let_it_be
(
:user
)
{
build_stubbed
(
:user
)
}
let_it_be
(
:admin
)
{
build_stubbed
(
:user
,
:admin
)
}
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_project
)
{
nil
}
let
(
:current_group
)
{
nil
}
let
(
:current_group
)
{
nil
}
let
(
:with_current_settings_admin_mode
)
{
false
}
let
(
:with_current_settings_admin_mode
)
{
false
}
...
@@ -26,7 +31,6 @@ RSpec.describe Nav::TopNavHelper do
...
@@ -26,7 +31,6 @@ RSpec.describe Nav::TopNavHelper do
let
(
:active_title
)
{
'Menu'
}
let
(
:active_title
)
{
'Menu'
}
before
do
before
do
allow
(
helper
).
to
receive
(
:current_user
)
{
current_user
}
allow
(
Gitlab
::
CurrentSettings
).
to
receive
(
:admin_mode
)
{
with_current_settings_admin_mode
}
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
(
helper
).
to
receive
(
:header_link?
).
with
(
:admin_mode
)
{
with_header_link_admin_mode
}
allow
(
Gitlab
::
Sherlock
).
to
receive
(
:enabled?
)
{
with_sherlock_enabled
}
allow
(
Gitlab
::
Sherlock
).
to
receive
(
:enabled?
)
{
with_sherlock_enabled
}
...
@@ -487,4 +491,50 @@ RSpec.describe Nav::TopNavHelper do
...
@@ -487,4 +491,50 @@ RSpec.describe Nav::TopNavHelper do
end
end
end
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
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