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
068c1539
Commit
068c1539
authored
Aug 03, 2020
by
Kushal Pandya
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Use GlFilteredSearch in Roadmap
Add async filtering support within Roadmap by using GlFilteredSearch.
parent
7f9af793
Changes
22
Hide whitespace changes
Inline
Side-by-side
Showing
22 changed files
with
768 additions
and
52 deletions
+768
-52
app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue
...ed_search/components/recent_searches_dropdown_content.vue
+13
-5
app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
...mponents/filtered_search_bar/filtered_search_bar_root.vue
+5
-2
app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
...red/components/filtered_search_bar/tokens/label_token.vue
+7
-7
ee/app/assets/javascripts/roadmap/components/roadmap_app.vue
ee/app/assets/javascripts/roadmap/components/roadmap_app.vue
+42
-27
ee/app/assets/javascripts/roadmap/components/roadmap_filters.vue
...assets/javascripts/roadmap/components/roadmap_filters.vue
+265
-0
ee/app/assets/javascripts/roadmap/constants.js
ee/app/assets/javascripts/roadmap/constants.js
+6
-0
ee/app/assets/javascripts/roadmap/roadmap_bundle.js
ee/app/assets/javascripts/roadmap/roadmap_bundle.js
+3
-3
ee/app/assets/javascripts/roadmap/store/actions.js
ee/app/assets/javascripts/roadmap/store/actions.js
+10
-0
ee/app/assets/javascripts/roadmap/store/mutation_types.js
ee/app/assets/javascripts/roadmap/store/mutation_types.js
+4
-0
ee/app/assets/javascripts/roadmap/store/mutations.js
ee/app/assets/javascripts/roadmap/store/mutations.js
+22
-0
ee/app/assets/javascripts/roadmap/store/state.js
ee/app/assets/javascripts/roadmap/store/state.js
+2
-0
ee/app/assets/stylesheets/pages/roadmap.scss
ee/app/assets/stylesheets/pages/roadmap.scss
+13
-0
ee/app/controllers/groups/roadmap_controller.rb
ee/app/controllers/groups/roadmap_controller.rb
+1
-0
ee/app/views/groups/roadmap/show.html.haml
ee/app/views/groups/roadmap/show.html.haml
+13
-2
ee/spec/features/epics/epics_list_spec.rb
ee/spec/features/epics/epics_list_spec.rb
+1
-0
ee/spec/features/groups/group_roadmap_spec.rb
ee/spec/features/groups/group_roadmap_spec.rb
+1
-0
ee/spec/frontend/roadmap/components/roadmap_app_spec.js
ee/spec/frontend/roadmap/components/roadmap_app_spec.js
+10
-3
ee/spec/frontend/roadmap/components/roadmap_filters_spec.js
ee/spec/frontend/roadmap/components/roadmap_filters_spec.js
+260
-0
ee/spec/frontend/roadmap/store/mutations_spec.js
ee/spec/frontend/roadmap/store/mutations_spec.js
+62
-1
locale/gitlab.pot
locale/gitlab.pot
+9
-0
spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js
...earch/components/recent_searches_dropdown_content_spec.js
+6
-2
spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
...ents/filtered_search_bar/filtered_search_bar_root_spec.js
+13
-0
No files found.
app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue
View file @
068c1539
...
...
@@ -20,8 +20,18 @@ export default {
},
},
computed
:
{
/**
* Both Epic and Roadmap pages share same recents store
* and with https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36421
* Roadmap started using `GlFilteredSearch` which is not compatible
* with string tokens stored in recents, so this is a temporary
* fix by ignoring non-string recents while in Epic page.
*/
compatibleItems
()
{
return
this
.
items
.
filter
(
item
=>
typeof
item
===
'
string
'
);
},
processedItems
()
{
return
this
.
i
tems
.
map
(
item
=>
{
return
this
.
compatibleI
tems
.
map
(
item
=>
{
const
{
tokens
,
searchToken
}
=
FilteredSearchTokenizer
.
processTokens
(
item
,
this
.
allowedKeys
,
...
...
@@ -41,7 +51,7 @@ export default {
});
},
hasItems
()
{
return
this
.
i
tems
.
length
>
0
;
return
this
.
compatibleI
tems
.
length
>
0
;
},
},
methods
:
{
...
...
@@ -84,9 +94,7 @@ export default {
<span
class=
"value"
>
{{
token
.
suffix
}}
</span>
</span>
</span>
<span
class=
"filtered-search-history-dropdown-search-token"
>
{{
item
.
searchToken
}}
</span>
<span
class=
"filtered-search-history-dropdown-search-token"
>
{{
item
.
searchToken
}}
</span>
</button>
</li>
<li
class=
"divider"
></li>
...
...
app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
View file @
068c1539
...
...
@@ -83,7 +83,7 @@ export default {
return
{
initialRender
:
true
,
recentSearchesPromise
:
null
,
recentSearches
:
null
,
recentSearches
:
[]
,
filterValue
:
this
.
initialFilterValue
,
selectedSortOption
,
selectedSortDirection
,
...
...
@@ -118,6 +118,9 @@ export default {
?
__
(
'
Sort direction: Ascending
'
)
:
__
(
'
Sort direction: Descending
'
);
},
filteredRecentSearches
()
{
return
this
.
recentSearches
.
filter
(
item
=>
typeof
item
!==
'
string
'
);
},
},
watch
:
{
/**
...
...
@@ -246,7 +249,7 @@ export default {
v-model=
"filterValue"
:placeholder=
"searchInputPlaceholder"
:available-tokens=
"tokens"
:history-items=
"
r
ecentSearches"
:history-items=
"
filteredR
ecentSearches"
class=
"flex-grow-1"
@
history-item-selected=
"handleHistoryItemSelected"
@
clear-history=
"handleClearHistory"
...
...
app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
View file @
068c1539
...
...
@@ -3,7 +3,7 @@ import {
GlToken
,
GlFilteredSearchToken
,
GlFilteredSearchSuggestion
,
GlDropdownDivider
,
Gl
NewDropdownDivider
as
Gl
DropdownDivider
,
GlLoadingIcon
,
}
from
'
@gitlab/ui
'
;
import
{
debounce
}
from
'
lodash
'
;
...
...
@@ -102,14 +102,14 @@ export default {
@input="searchLabels"
>
<template
#view-token
="
{ inputValue, cssClasses, listeners }">
<gl-token
variant=
"search-value"
:class=
"cssClasses"
:style=
"containerStyle"
v-on=
"listeners"
>
~
{{
activeLabel
?
activeLabel
.
title
:
inputValue
}}
</gl-token
>
<gl-token
variant=
"search-value"
:class=
"cssClasses"
:style=
"containerStyle"
v-on=
"listeners"
>
~
{{
activeLabel
?
activeLabel
.
title
:
inputValue
}}
</gl-token
>
</
template
>
<
template
#suggestions
>
<gl-filtered-search-suggestion
:value=
"$options.noLabel"
>
{{
__
(
'
No label
'
)
}}
</gl-filtered-search-suggestion>
<gl-filtered-search-suggestion
:value=
"$options.noLabel"
>
{{
__
(
'
No label
'
)
}}
</gl-filtered-search-suggestion>
<gl-dropdown-divider
/>
<gl-loading-icon
v-if=
"loading"
/>
<template
v-else
>
...
...
ee/app/assets/javascripts/roadmap/components/roadmap_app.vue
View file @
068c1539
<
script
>
import
{
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
{
mapState
,
mapActions
}
from
'
vuex
'
;
import
glFeatureFlagsMixin
from
'
~/vue_shared/mixins/gl_feature_flags_mixin
'
;
import
EpicsListEmpty
from
'
./epics_list_empty.vue
'
;
import
RoadmapFilters
from
'
./roadmap_filters.vue
'
;
import
RoadmapShell
from
'
./roadmap_shell.vue
'
;
import
eventHub
from
'
../event_hub
'
;
import
{
EXTEND_AS
}
from
'
../constants
'
;
...
...
@@ -10,17 +14,15 @@ export default {
components
:
{
EpicsListEmpty
,
GlLoadingIcon
,
RoadmapFilters
,
RoadmapShell
,
},
mixins
:
[
glFeatureFlagsMixin
()],
props
:
{
presetType
:
{
type
:
String
,
required
:
true
,
},
hasFiltersApplied
:
{
type
:
Boolean
,
required
:
true
,
},
newEpicEndpoint
:
{
type
:
String
,
required
:
true
,
...
...
@@ -42,8 +44,18 @@ export default {
'
epicsFetchResultEmpty
'
,
'
epicsFetchFailure
'
,
'
isChildEpics
'
,
'
hasFiltersApplied
'
,
'
milestonesFetchFailure
'
,
]),
showFilteredSearchbar
()
{
if
(
this
.
glFeatures
.
asyncFiltering
)
{
if
(
this
.
epicsFetchResultEmpty
)
{
return
this
.
hasFiltersApplied
;
}
return
true
;
}
return
false
;
},
timeframeStart
()
{
return
this
.
timeframe
[
0
];
},
...
...
@@ -118,28 +130,31 @@ export default {
</
script
>
<
template
>
<div
:class=
"
{ 'overflow-reset': epicsFetchResultEmpty }" class="roadmap-container">
<gl-loading-icon
v-if=
"epicsFetchInProgress"
class=
"mt-4"
size=
"md"
/>
<epics-list-empty
v-else-if=
"epicsFetchResultEmpty"
:preset-type=
"presetType"
:timeframe-start=
"timeframeStart"
:timeframe-end=
"timeframeEnd"
:has-filters-applied=
"hasFiltersApplied"
:new-epic-endpoint=
"newEpicEndpoint"
:empty-state-illustration-path=
"emptyStateIllustrationPath"
:is-child-epics=
"isChildEpics"
/>
<roadmap-shell
v-else-if=
"!epicsFetchFailure"
:preset-type=
"presetType"
:epics=
"epics"
:milestones=
"milestones"
:timeframe=
"timeframe"
:current-group-id=
"currentGroupId"
:has-filters-applied=
"hasFiltersApplied"
@
onScrollToStart=
"handleScrollToExtend"
@
onScrollToEnd=
"handleScrollToExtend"
/>
<div
class=
"roadmap-app-container gl-h-full"
>
<roadmap-filters
v-if=
"showFilteredSearchbar"
/>
<div
:class=
"
{ 'overflow-reset': epicsFetchResultEmpty }" class="roadmap-container">
<gl-loading-icon
v-if=
"epicsFetchInProgress"
class=
"gl-mt-5"
size=
"md"
/>
<epics-list-empty
v-else-if=
"epicsFetchResultEmpty"
:preset-type=
"presetType"
:timeframe-start=
"timeframeStart"
:timeframe-end=
"timeframeEnd"
:has-filters-applied=
"hasFiltersApplied"
:new-epic-endpoint=
"newEpicEndpoint"
:empty-state-illustration-path=
"emptyStateIllustrationPath"
:is-child-epics=
"isChildEpics"
/>
<roadmap-shell
v-else-if=
"!epicsFetchFailure"
:preset-type=
"presetType"
:epics=
"epics"
:milestones=
"milestones"
:timeframe=
"timeframe"
:current-group-id=
"currentGroupId"
:has-filters-applied=
"hasFiltersApplied"
@
onScrollToStart=
"handleScrollToExtend"
@
onScrollToEnd=
"handleScrollToExtend"
/>
</div>
</div>
</
template
>
ee/app/assets/javascripts/roadmap/components/roadmap_filters.vue
0 → 100644
View file @
068c1539
<
script
>
import
{
mapState
,
mapActions
}
from
'
vuex
'
;
import
{
GlFormGroup
,
GlSegmentedControl
,
GlNewDropdown
as
GlDropdown
,
GlNewDropdownItem
as
GlDropdownItem
,
GlNewDropdownDivider
as
GlDropdownDivider
,
}
from
'
@gitlab/ui
'
;
import
{
__
}
from
'
~/locale
'
;
import
Api
from
'
~/api
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
{
urlParamsToObject
}
from
'
~/lib/utils/common_utils
'
;
import
{
visitUrl
,
mergeUrlParams
,
updateHistory
,
setUrlParams
}
from
'
~/lib/utils/url_utility
'
;
import
FilteredSearchBar
from
'
~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
'
;
import
AuthorToken
from
'
~/vue_shared/components/filtered_search_bar/tokens/author_token.vue
'
;
import
LabelToken
from
'
~/vue_shared/components/filtered_search_bar/tokens/label_token.vue
'
;
import
{
EPICS_STATES
,
PRESET_TYPES
}
from
'
../constants
'
;
export
default
{
epicStates
:
EPICS_STATES
,
availablePresets
:
[
{
text
:
__
(
'
Quarters
'
),
value
:
PRESET_TYPES
.
QUARTERS
},
{
text
:
__
(
'
Months
'
),
value
:
PRESET_TYPES
.
MONTHS
},
{
text
:
__
(
'
Weeks
'
),
value
:
PRESET_TYPES
.
WEEKS
},
],
availableSortOptions
:
[
{
id
:
1
,
title
:
__
(
'
Start date
'
),
sortDirection
:
{
descending
:
'
start_date_desc
'
,
ascending
:
'
start_date_asc
'
,
},
},
{
id
:
2
,
title
:
__
(
'
Due date
'
),
sortDirection
:
{
descending
:
'
end_date_desc
'
,
ascending
:
'
end_date_asc
'
,
},
},
],
components
:
{
GlFormGroup
,
GlSegmentedControl
,
GlDropdown
,
GlDropdownItem
,
GlDropdownDivider
,
FilteredSearchBar
,
},
computed
:
{
...
mapState
([
'
presetType
'
,
'
epicsState
'
,
'
sortedBy
'
,
'
fullPath
'
,
'
groupLabelsEndpoint
'
,
'
filterParams
'
,
]),
selectedEpicStateTitle
()
{
if
(
this
.
epicsState
===
EPICS_STATES
.
ALL
)
{
return
__
(
'
All epics
'
);
}
else
if
(
this
.
epicsState
===
EPICS_STATES
.
OPENED
)
{
return
__
(
'
Open epics
'
);
}
return
__
(
'
Closed epics
'
);
},
},
methods
:
{
...
mapActions
([
'
setEpicsState
'
,
'
setFilterParams
'
,
'
setSortedBy
'
,
'
fetchEpics
'
]),
getFilteredSearchTokens
()
{
return
[
{
type
:
'
author_username
'
,
icon
:
'
user
'
,
title
:
__
(
'
Author
'
),
unique
:
true
,
symbol
:
'
@
'
,
token
:
AuthorToken
,
operators
:
[{
value
:
'
=
'
,
description
:
__
(
'
is
'
),
default
:
'
true
'
}],
fetchAuthors
:
Api
.
users
.
bind
(
Api
),
},
{
type
:
'
label_name
'
,
icon
:
'
labels
'
,
title
:
__
(
'
Label
'
),
unique
:
false
,
symbol
:
'
~
'
,
token
:
LabelToken
,
operators
:
[{
value
:
'
=
'
,
description
:
__
(
'
is
'
),
default
:
'
true
'
}],
fetchLabels
:
(
search
=
''
)
=>
{
const
params
=
{
only_group_labels
:
true
,
include_ancestor_groups
:
true
,
include_descendant_groups
:
true
,
};
if
(
search
)
{
params
.
search
=
search
;
}
return
axios
.
get
(
this
.
groupLabelsEndpoint
,
{
params
,
});
},
},
];
},
getFilteredSearchValue
()
{
const
{
authorUsername
,
labelName
,
search
}
=
this
.
filterParams
||
{};
const
filteredSearchValue
=
[];
if
(
authorUsername
)
{
filteredSearchValue
.
push
({
type
:
'
author_username
'
,
value
:
{
data
:
authorUsername
},
});
}
if
(
labelName
?.
length
)
{
filteredSearchValue
.
push
(
...
labelName
.
map
(
label
=>
({
type
:
'
label_name
'
,
value
:
{
data
:
label
},
})),
);
}
if
(
search
)
{
filteredSearchValue
.
push
(
search
);
}
return
filteredSearchValue
;
},
updateUrl
()
{
const
queryParams
=
urlParamsToObject
(
window
.
location
.
search
);
const
{
authorUsername
,
labelName
,
search
}
=
this
.
filterParams
||
{};
queryParams
.
state
=
this
.
epicsState
;
queryParams
.
sort
=
this
.
sortedBy
;
if
(
authorUsername
)
{
queryParams
.
author_username
=
authorUsername
;
}
else
{
delete
queryParams
.
author_username
;
}
delete
queryParams
.
label_name
;
if
(
labelName
?.
length
)
{
queryParams
[
'
label_name[]
'
]
=
labelName
;
}
if
(
search
)
{
queryParams
.
search
=
search
;
}
else
{
delete
queryParams
.
search
;
}
// We want to replace the history state so that back button
// correctly reloads the page with previous URL.
updateHistory
({
url
:
setUrlParams
(
queryParams
,
window
.
location
.
href
,
true
),
title
:
document
.
title
,
replace
:
true
,
});
},
handleRoadmapLayoutChange
(
presetType
)
{
visitUrl
(
mergeUrlParams
({
layout
:
presetType
},
window
.
location
.
href
));
},
handleEpicStateChange
(
epicsState
)
{
this
.
setEpicsState
(
epicsState
);
this
.
fetchEpics
();
this
.
updateUrl
();
},
handleFilterEpics
(
filters
)
{
const
filterParams
=
filters
.
length
?
{}
:
null
;
const
labels
=
[];
filters
.
forEach
(
filter
=>
{
if
(
typeof
filter
===
'
object
'
)
{
if
(
filter
.
type
===
'
author_username
'
)
{
filterParams
.
authorUsername
=
filter
.
value
.
data
;
}
else
if
(
filter
.
type
===
'
label_name
'
)
{
labels
.
push
(
filter
.
value
.
data
);
}
}
else
{
filterParams
.
search
=
filter
;
}
});
if
(
labels
.
length
)
{
filterParams
.
labelName
=
labels
;
}
this
.
setFilterParams
(
filterParams
);
this
.
fetchEpics
();
this
.
updateUrl
();
},
handleSortEpics
(
sortedBy
)
{
this
.
setSortedBy
(
sortedBy
);
this
.
fetchEpics
();
this
.
updateUrl
();
},
},
};
</
script
>
<
template
>
<div
class=
"epics-filters epics-roadmap-filters epics-roadmap-filters-gl-ui"
>
<div
class=
"epics-details-filters filtered-search-block gl-display-flex gl-flex-direction-column flex-xl-row row-content-block second-block"
>
<gl-form-group
class=
"mb-0"
>
<gl-segmented-control
:checked=
"presetType"
:options=
"$options.availablePresets"
class=
"gl-display-flex d-xl-block"
buttons
@
input=
"handleRoadmapLayoutChange"
/>
</gl-form-group>
<gl-dropdown
:text=
"selectedEpicStateTitle"
class=
"gl-my-2 my-xl-0 mx-xl-2"
toggle-class=
"gl-rounded-small"
>
<gl-dropdown-item
:is-check-item=
"true"
:is-checked=
"epicsState === $options.epicStates.ALL"
@
click=
"handleEpicStateChange('all')"
>
{{
__
(
'
All epics
'
)
}}
</gl-dropdown-item
>
<gl-dropdown-divider
/>
<gl-dropdown-item
:is-check-item=
"true"
:is-checked=
"epicsState === $options.epicStates.OPENED"
@
click=
"handleEpicStateChange('opened')"
>
{{
__
(
'
Open epics
'
)
}}
</gl-dropdown-item
>
<gl-dropdown-item
:is-check-item=
"true"
:is-checked=
"epicsState === $options.epicStates.CLOSED"
@
click=
"handleEpicStateChange('closed')"
>
{{
__
(
'
Closed epics
'
)
}}
</gl-dropdown-item
>
</gl-dropdown>
<filtered-search-bar
:namespace=
"fullPath"
:search-input-placeholder=
"__('Search or filter results...')"
:tokens=
"getFilteredSearchTokens()"
:sort-options=
"$options.availableSortOptions"
:initial-filter-value=
"getFilteredSearchValue()"
:initial-sort-by=
"sortedBy"
recent-searches-storage-key=
"epics"
class=
"gl-flex-grow-1"
@
onFilter=
"handleFilterEpics"
@
onSort=
"handleSortEpics"
/>
</div>
</div>
</
template
>
ee/app/assets/javascripts/roadmap/constants.js
View file @
068c1539
...
...
@@ -22,6 +22,12 @@ export const PRESET_TYPES = {
WEEKS
:
'
WEEKS
'
,
};
export
const
EPICS_STATES
=
{
ALL
:
'
all
'
,
OPENED
:
'
opened
'
,
CLOSED
:
'
closed
'
,
};
export
const
EXTEND_AS
=
{
PREPEND
:
'
prepend
'
,
APPEND
:
'
append
'
,
...
...
ee/app/assets/javascripts/roadmap/roadmap_bundle.js
View file @
068c1539
...
...
@@ -86,6 +86,7 @@ export default () => {
fullPath
:
dataset
.
fullPath
,
epicIid
:
dataset
.
iid
,
newEpicEndpoint
:
dataset
.
newEpicEndpoint
,
groupLabelsEndpoint
:
dataset
.
groupLabelsEndpoint
,
epicsState
:
dataset
.
epicsState
,
sortedBy
:
dataset
.
sortedBy
,
filterQueryString
,
...
...
@@ -108,8 +109,10 @@ export default () => {
filterQueryString
:
this
.
filterQueryString
,
filterParams
:
this
.
filterParams
,
initialEpicsPath
:
this
.
initialEpicsPath
,
groupLabelsEndpoint
:
this
.
groupLabelsEndpoint
,
defaultInnerHeight
:
this
.
defaultInnerHeight
,
isChildEpics
:
this
.
isChildEpics
,
hasFiltersApplied
:
this
.
hasFiltersApplied
,
allowSubEpics
:
this
.
allowSubEpics
,
});
},
...
...
@@ -119,10 +122,7 @@ export default () => {
render
(
createElement
)
{
return
createElement
(
'
roadmap-app
'
,
{
props
:
{
store
:
this
.
store
,
presetType
:
this
.
presetType
,
hasFiltersApplied
:
this
.
hasFiltersApplied
,
epicsState
:
this
.
epicsState
,
newEpicEndpoint
:
this
.
newEpicEndpoint
,
emptyStateIllustrationPath
:
this
.
emptyStateIllustrationPath
,
},
...
...
ee/app/assets/javascripts/roadmap/store/actions.js
View file @
068c1539
...
...
@@ -345,3 +345,13 @@ export const refreshMilestoneDates = ({ commit, state, getters }) => {
};
export
const
setBufferSize
=
({
commit
},
bufferSize
)
=>
commit
(
types
.
SET_BUFFER_SIZE
,
bufferSize
);
export
const
setEpicsState
=
({
commit
},
epicsState
)
=>
commit
(
types
.
SET_EPICS_STATE
,
epicsState
);
export
const
setFilterParams
=
({
commit
},
filterParams
)
=>
commit
(
types
.
SET_FILTER_PARAMS
,
filterParams
);
export
const
setSortedBy
=
({
commit
},
sortedBy
)
=>
commit
(
types
.
SET_SORTED_BY
,
sortedBy
);
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export
default
()
=>
{};
ee/app/assets/javascripts/roadmap/store/mutation_types.js
View file @
068c1539
...
...
@@ -28,3 +28,7 @@ export const RECEIVE_MILESTONES_SUCCESS = 'RECEIVE_MILESTONES_SUCCESS';
export
const
RECEIVE_MILESTONES_FAILURE
=
'
RECEIVE_MILESTONES_FAILURE
'
;
export
const
SET_BUFFER_SIZE
=
'
SET_BUFFER_SIZE
'
;
export
const
SET_EPICS_STATE
=
'
SET_EPICS_STATE
'
;
export
const
SET_FILTER_PARAMS
=
'
SET_FILTER_PARAMS
'
;
export
const
SET_SORTED_BY
=
'
SET_SORTED_BY
'
;
ee/app/assets/javascripts/roadmap/store/mutations.js
View file @
068c1539
...
...
@@ -2,6 +2,12 @@ import Vue from 'vue';
import
*
as
types
from
'
./mutation_types
'
;
const
resetEpics
=
state
=>
{
state
.
epics
=
[];
state
.
childrenFlags
=
{};
state
.
epicIds
=
[];
};
export
default
{
[
types
.
SET_INITIAL_DATA
](
state
,
data
)
{
Object
.
assign
(
state
,
{
...
data
});
...
...
@@ -103,4 +109,20 @@ export default {
[
types
.
SET_BUFFER_SIZE
](
state
,
bufferSize
)
{
state
.
bufferSize
=
bufferSize
;
},
[
types
.
SET_FILTER_PARAMS
](
state
,
filterParams
)
{
state
.
filterParams
=
filterParams
;
state
.
hasFiltersApplied
=
Boolean
(
filterParams
);
resetEpics
(
state
);
},
[
types
.
SET_EPICS_STATE
](
state
,
epicsState
)
{
state
.
epicsState
=
epicsState
;
resetEpics
(
state
);
},
[
types
.
SET_SORTED_BY
](
state
,
sortedBy
)
{
state
.
sortedBy
=
sortedBy
;
resetEpics
(
state
);
},
};
ee/app/assets/javascripts/roadmap/store/state.js
View file @
068c1539
...
...
@@ -5,6 +5,7 @@ export default () => ({
filterQueryString
:
''
,
initialEpicsPath
:
''
,
filterParams
:
null
,
groupLabelsEndpoint
:
''
,
// Data
epicIid
:
''
,
...
...
@@ -26,6 +27,7 @@ export default () => ({
// UI Flags
defaultInnerHeight
:
0
,
isChildEpics
:
false
,
hasFiltersApplied
:
false
,
epicsFetchInProgress
:
false
,
epicsFetchForTimeframeInProgress
:
false
,
epicsFetchFailure
:
false
,
...
...
ee/app/assets/stylesheets/pages/roadmap.scss
View file @
068c1539
...
...
@@ -560,3 +560,16 @@ html.group-epics-roadmap-html {
color
:
$gray-700
;
padding-top
:
$gl-spacing-scale-1
;
}
// There are several styling issues happening while using
// `GlFilteredSearch` in roadmap due to some of our global
// styles which we need to override until those are fixed
// at framework level.
// See https://gitlab.com/gitlab-org/gitlab-ui/-/issues/908
.epics-roadmap-filters-gl-ui
{
.gl-search-box-by-click
{
.gl-filtered-search-scrollable
{
border-radius
:
0
;
}
}
}
ee/app/controllers/groups/roadmap_controller.rb
View file @
068c1539
...
...
@@ -12,6 +12,7 @@ module Groups
before_action
do
push_frontend_feature_flag
(
:roadmap_buffered_rendering
,
@group
)
push_frontend_feature_flag
(
:confidential_epics
,
@group
,
default_enabled:
true
)
push_frontend_feature_flag
(
:async_filtering
,
@group
)
end
# show roadmap for a group
...
...
ee/app/views/groups/roadmap/show.html.haml
View file @
068c1539
...
...
@@ -13,7 +13,8 @@
-
has_filters_applied
=
params
[
:label_name
].
present?
||
params
[
:author_username
].
present?
||
params
[
:search
].
present?
-
if
@epics_count
!=
0
=
render
'shared/epic/search_bar'
,
type: :epics
,
show_roadmap_presets:
true
,
hide_extra_sort_options:
true
-
if
!
Feature
.
enabled?
(
:async_filtering
,
@group
)
=
render
'shared/epic/search_bar'
,
type: :epics
,
show_roadmap_presets:
true
,
hide_extra_sort_options:
true
-
if
@epics_count
>
Groups
::
RoadmapController
::
EPICS_ROADMAP_LIMIT
&&
show_callout?
(
epics_limit_feature
)
.warning_message.mb-0.js-epics-limit-callout
{
role:
'alert'
,
data:
{
uid:
epics_limit_feature
}
}
...
...
@@ -24,7 +25,17 @@
%a
.btn.btn-outline-warning
#js-learn-more
{
"href"
=>
"https://docs.gitlab.com/ee/user/group/roadmap/"
}
=
_
(
"Learn more"
)
#js-roadmap
{
data:
{
epics_path:
group_epics_path
(
@group
,
format: :json
),
group_id:
@group
.
id
,
full_path:
@group
.
full_path
,
empty_state_illustration:
image_path
(
'illustrations/epics/roadmap.svg'
),
has_filters_applied:
"#{has_filters_applied}"
,
new_epic_endpoint:
group_epics_path
(
@group
),
preset_type:
roadmap_layout
,
epics_state:
@epics_state
,
sorted_by:
@sort
,
allow_sub_epics:
allow_sub_epics
}
}
#js-roadmap
{
data:
{
epics_path:
group_epics_path
(
@group
,
format: :json
),
group_id:
@group
.
id
,
full_path:
@group
.
full_path
,
empty_state_illustration:
image_path
(
'illustrations/epics/roadmap.svg'
),
has_filters_applied:
"#{has_filters_applied}"
,
new_epic_endpoint:
group_epics_path
(
@group
),
group_labels_endpoint:
group_labels_path
(
@group
,
format: :json
),
preset_type:
roadmap_layout
,
epics_state:
@epics_state
,
sorted_by:
@sort
,
allow_sub_epics:
allow_sub_epics
}
}
-
else
=
render
'shared/empty_states/roadmap'
ee/spec/features/epics/epics_list_spec.rb
View file @
068c1539
...
...
@@ -9,6 +9,7 @@ RSpec.describe 'epics list', :js do
before
do
stub_licensed_features
(
epics:
true
)
stub_feature_flags
(
unfiltered_epic_aggregates:
false
)
stub_feature_flags
(
async_filtering:
false
)
sign_in
(
user
)
end
...
...
ee/spec/features/groups/group_roadmap_spec.rb
View file @
068c1539
...
...
@@ -25,6 +25,7 @@ RSpec.describe 'group epic roadmap', :js do
before
do
stub_licensed_features
(
epics:
true
)
stub_feature_flags
(
unfiltered_epic_aggregates:
false
)
stub_feature_flags
(
async_filtering:
false
)
sign_in
(
user
)
end
...
...
ee/spec/frontend/roadmap/components/roadmap_app_spec.js
View file @
068c1539
import
{
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
{
mount
,
shallowMount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
import
Vue
from
'
vue
'
;
import
Vuex
from
'
vuex
'
;
import
EpicsListEmpty
from
'
ee/roadmap/components/epics_list_empty.vue
'
;
import
RoadmapApp
from
'
ee/roadmap/components/roadmap_app.vue
'
;
import
RoadmapFilters
from
'
ee/roadmap/components/roadmap_filters.vue
'
;
import
RoadmapShell
from
'
ee/roadmap/components/roadmap_shell.vue
'
;
import
{
PRESET_TYPES
,
EXTEND_AS
}
from
'
ee/roadmap/constants
'
;
import
eventHub
from
'
ee/roadmap/event_hub
'
;
...
...
@@ -42,10 +42,12 @@ describe('RoadmapApp', () => {
localVue
,
propsData
:
{
emptyStateIllustrationPath
,
hasFiltersApplied
,
newEpicEndpoint
,
presetType
,
},
provide
:
{
glFeatures
:
{
asyncFiltering
:
true
},
},
store
,
});
};
...
...
@@ -57,6 +59,7 @@ describe('RoadmapApp', () => {
sortedBy
:
mockSortedBy
,
presetType
,
timeframe
,
hasFiltersApplied
,
filterQueryString
:
''
,
initialEpicsPath
:
epicsPath
,
basePath
,
...
...
@@ -142,6 +145,10 @@ describe('RoadmapApp', () => {
store
.
commit
(
types
.
RECEIVE_EPICS_SUCCESS
,
epics
);
});
it
(
'
contains roadmap filters UI
'
,
()
=>
{
expect
(
wrapper
.
contains
(
RoadmapFilters
)).
toBe
(
true
);
});
it
(
'
contains the current group id
'
,
()
=>
{
expect
(
wrapper
.
find
(
RoadmapShell
).
props
(
'
currentGroupId
'
)).
toBe
(
currentGroupId
);
});
...
...
@@ -223,7 +230,7 @@ describe('RoadmapApp', () => {
wrapper
.
vm
.
handleScrollToExtend
(
roadmapTimelineEl
,
extendType
);
return
Vue
.
nextTick
(()
=>
{
return
wrapper
.
vm
.
$
nextTick
(()
=>
{
expect
(
wrapper
.
vm
.
fetchEpicsForTimeframe
).
toHaveBeenCalledWith
(
expect
.
objectContaining
({
timeframe
:
wrapper
.
vm
.
extendedTimeframe
,
...
...
ee/spec/frontend/roadmap/components/roadmap_filters_spec.js
0 → 100644
View file @
068c1539
import
Vuex
from
'
vuex
'
;
import
{
shallowMount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
import
{
GlSegmentedControl
,
GlNewDropdown
as
GlDropdown
,
GlNewDropdownItem
as
GlDropdownItem
,
}
from
'
@gitlab/ui
'
;
import
FilteredSearchBar
from
'
~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
'
;
import
AuthorToken
from
'
~/vue_shared/components/filtered_search_bar/tokens/author_token.vue
'
;
import
LabelToken
from
'
~/vue_shared/components/filtered_search_bar/tokens/label_token.vue
'
;
import
{
visitUrl
,
mergeUrlParams
,
updateHistory
}
from
'
~/lib/utils/url_utility
'
;
import
RoadmapFilters
from
'
ee/roadmap/components/roadmap_filters.vue
'
;
import
createStore
from
'
ee/roadmap/store
'
;
import
{
getTimeframeForMonthsView
}
from
'
ee/roadmap/utils/roadmap_utils
'
;
import
{
PRESET_TYPES
,
EPICS_STATES
}
from
'
ee/roadmap/constants
'
;
import
{
mockSortedBy
,
mockTimeframeInitialDate
}
from
'
ee_jest/roadmap/mock_data
'
;
import
{
TEST_HOST
}
from
'
helpers/test_constants
'
;
jest
.
mock
(
'
~/lib/utils/url_utility
'
,
()
=>
({
mergeUrlParams
:
jest
.
fn
(),
visitUrl
:
jest
.
fn
(),
setUrlParams
:
jest
.
requireActual
(
'
~/lib/utils/url_utility
'
).
setUrlParams
,
updateHistory
:
jest
.
requireActual
(
'
~/lib/utils/url_utility
'
).
updateHistory
,
}));
const
createComponent
=
({
presetType
=
PRESET_TYPES
.
MONTHS
,
epicsState
=
EPICS_STATES
.
ALL
,
sortedBy
=
mockSortedBy
,
fullPath
=
'
gitlab-org
'
,
groupLabelsEndpoint
=
'
/groups/gitlab-org/-/labels.json
'
,
timeframe
=
getTimeframeForMonthsView
(
mockTimeframeInitialDate
),
filterParams
=
{},
}
=
{})
=>
{
const
localVue
=
createLocalVue
();
const
store
=
createStore
();
localVue
.
use
(
Vuex
);
store
.
dispatch
(
'
setInitialData
'
,
{
presetType
,
epicsState
,
sortedBy
,
fullPath
,
groupLabelsEndpoint
,
filterParams
,
timeframe
,
});
return
shallowMount
(
RoadmapFilters
,
{
localVue
,
store
,
});
};
describe
(
'
RoadmapFilters
'
,
()
=>
{
let
wrapper
;
beforeEach
(()
=>
{
wrapper
=
createComponent
();
});
afterEach
(()
=>
{
wrapper
.
destroy
();
});
describe
(
'
computed
'
,
()
=>
{
describe
(
'
selectedEpicStateTitle
'
,
()
=>
{
it
.
each
`
returnValue | epicsState
${
'
All epics
'
}
|
${
EPICS_STATES
.
ALL
}
${
'
Open epics
'
}
|
${
EPICS_STATES
.
OPENED
}
${
'
Closed epics
'
}
|
${
EPICS_STATES
.
CLOSED
}
`
(
'
returns string "$returnValue" when epicsState represents `$epicsState`
'
,
({
returnValue
,
epicsState
})
=>
{
wrapper
.
vm
.
$store
.
dispatch
(
'
setEpicsState
'
,
epicsState
);
expect
(
wrapper
.
vm
.
selectedEpicStateTitle
).
toBe
(
returnValue
);
},
);
});
});
describe
(
'
methods
'
,
()
=>
{
describe
(
'
updateUrl
'
,
()
=>
{
it
(
'
updates window URL based on presence of props for state, filtered search and sort criteria
'
,
async
()
=>
{
wrapper
.
vm
.
$store
.
dispatch
(
'
setEpicsState
'
,
EPICS_STATES
.
CLOSED
);
wrapper
.
vm
.
$store
.
dispatch
(
'
setFilterParams
'
,
{
authorUsername
:
'
root
'
,
labelName
:
[
'
Bug
'
],
});
wrapper
.
vm
.
$store
.
dispatch
(
'
setSortedBy
'
,
'
end_date_asc
'
);
await
wrapper
.
vm
.
$nextTick
();
wrapper
.
vm
.
updateUrl
();
expect
(
global
.
window
.
location
.
href
).
toBe
(
`
${
TEST_HOST
}
/?state=
${
EPICS_STATES
.
CLOSED
}
&sort=end_date_asc&author_username=root&label_name%5B%5D=Bug`
,
);
});
});
});
describe
(
'
template
'
,
()
=>
{
beforeEach
(()
=>
{
updateHistory
({
url
:
TEST_HOST
,
title
:
document
.
title
,
replace
:
true
});
});
it
(
'
renders roadmap layout switching buttons
'
,
()
=>
{
const
layoutSwitches
=
wrapper
.
find
(
GlSegmentedControl
);
expect
(
layoutSwitches
.
exists
()).
toBe
(
true
);
expect
(
layoutSwitches
.
props
(
'
checked
'
)).
toBe
(
PRESET_TYPES
.
MONTHS
);
expect
(
layoutSwitches
.
props
(
'
options
'
)).
toEqual
([
{
text
:
'
Quarters
'
,
value
:
PRESET_TYPES
.
QUARTERS
},
{
text
:
'
Months
'
,
value
:
PRESET_TYPES
.
MONTHS
},
{
text
:
'
Weeks
'
,
value
:
PRESET_TYPES
.
WEEKS
},
]);
});
it
(
'
switching layout using roadmap layout switching buttons causes page to reload with selected layout
'
,
()
=>
{
wrapper
.
find
(
GlSegmentedControl
).
vm
.
$emit
(
'
input
'
,
PRESET_TYPES
.
OPENED
);
expect
(
mergeUrlParams
).
toHaveBeenCalledWith
(
expect
.
objectContaining
({
layout
:
PRESET_TYPES
.
OPENED
}),
`
${
TEST_HOST
}
/`
,
);
expect
(
visitUrl
).
toHaveBeenCalled
();
});
it
(
'
renders epics state toggling dropdown
'
,
()
=>
{
const
epicsStateDropdown
=
wrapper
.
find
(
GlDropdown
);
expect
(
epicsStateDropdown
.
exists
()).
toBe
(
true
);
expect
(
epicsStateDropdown
.
findAll
(
GlDropdownItem
)).
toHaveLength
(
3
);
});
describe
(
'
FilteredSearchBar
'
,
()
=>
{
const
mockInitialFilterValue
=
[
{
type
:
'
author_username
'
,
value
:
{
data
:
'
root
'
},
},
{
type
:
'
label_name
'
,
value
:
{
data
:
'
Bug
'
},
},
];
let
filteredSearchBar
;
beforeEach
(()
=>
{
filteredSearchBar
=
wrapper
.
find
(
FilteredSearchBar
);
});
it
(
'
component is rendered with correct namespace & recent search key
'
,
()
=>
{
expect
(
filteredSearchBar
.
exists
()).
toBe
(
true
);
expect
(
filteredSearchBar
.
props
(
'
namespace
'
)).
toBe
(
'
gitlab-org
'
);
expect
(
filteredSearchBar
.
props
(
'
recentSearchesStorageKey
'
)).
toBe
(
'
epics
'
);
});
it
(
'
includes `Author` and `Label` tokens
'
,
()
=>
{
expect
(
filteredSearchBar
.
props
(
'
tokens
'
)).
toEqual
([
{
type
:
'
author_username
'
,
icon
:
'
user
'
,
title
:
'
Author
'
,
unique
:
true
,
symbol
:
'
@
'
,
token
:
AuthorToken
,
operators
:
[{
value
:
'
=
'
,
description
:
'
is
'
,
default
:
'
true
'
}],
fetchAuthors
:
expect
.
any
(
Function
),
},
{
type
:
'
label_name
'
,
icon
:
'
labels
'
,
title
:
'
Label
'
,
unique
:
false
,
symbol
:
'
~
'
,
token
:
LabelToken
,
operators
:
[{
value
:
'
=
'
,
description
:
'
is
'
,
default
:
'
true
'
}],
fetchLabels
:
expect
.
any
(
Function
),
},
]);
});
it
(
'
includes "Start date" and "Due date" sort options
'
,
()
=>
{
expect
(
filteredSearchBar
.
props
(
'
sortOptions
'
)).
toEqual
([
{
id
:
1
,
title
:
'
Start date
'
,
sortDirection
:
{
descending
:
'
start_date_desc
'
,
ascending
:
'
start_date_asc
'
,
},
},
{
id
:
2
,
title
:
'
Due date
'
,
sortDirection
:
{
descending
:
'
end_date_desc
'
,
ascending
:
'
end_date_asc
'
,
},
},
]);
});
it
(
'
has initialFilterValue prop set to array of formatted values based on `filterParams`
'
,
async
()
=>
{
wrapper
.
vm
.
$store
.
dispatch
(
'
setFilterParams
'
,
{
authorUsername
:
'
root
'
,
labelName
:
[
'
Bug
'
],
});
await
wrapper
.
vm
.
$nextTick
();
expect
(
filteredSearchBar
.
props
(
'
initialFilterValue
'
)).
toEqual
(
mockInitialFilterValue
);
});
it
(
'
fetches filtered epics when `onFilter` event is emitted
'
,
async
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
,
'
setFilterParams
'
);
jest
.
spyOn
(
wrapper
.
vm
,
'
fetchEpics
'
);
jest
.
spyOn
(
wrapper
.
vm
,
'
updateUrl
'
);
await
wrapper
.
vm
.
$nextTick
();
filteredSearchBar
.
vm
.
$emit
(
'
onFilter
'
,
mockInitialFilterValue
);
await
wrapper
.
vm
.
$nextTick
();
expect
(
wrapper
.
vm
.
setFilterParams
).
toHaveBeenCalledWith
({
authorUsername
:
'
root
'
,
labelName
:
[
'
Bug
'
],
});
expect
(
wrapper
.
vm
.
fetchEpics
).
toHaveBeenCalled
();
expect
(
wrapper
.
vm
.
updateUrl
).
toHaveBeenCalled
();
});
it
(
'
fetches epics with updated sort order when `onSort` event is emitted
'
,
async
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
,
'
setSortedBy
'
);
jest
.
spyOn
(
wrapper
.
vm
,
'
fetchEpics
'
);
jest
.
spyOn
(
wrapper
.
vm
,
'
updateUrl
'
);
await
wrapper
.
vm
.
$nextTick
();
filteredSearchBar
.
vm
.
$emit
(
'
onSort
'
,
'
end_date_asc
'
);
await
wrapper
.
vm
.
$nextTick
();
expect
(
wrapper
.
vm
.
setSortedBy
).
toHaveBeenCalledWith
(
'
end_date_asc
'
);
expect
(
wrapper
.
vm
.
fetchEpics
).
toHaveBeenCalled
();
expect
(
wrapper
.
vm
.
updateUrl
).
toHaveBeenCalled
();
});
});
});
});
ee/spec/frontend/roadmap/store/mutations_spec.js
View file @
068c1539
...
...
@@ -3,7 +3,19 @@ import * as types from 'ee/roadmap/store/mutation_types';
import
defaultState
from
'
ee/roadmap/store/state
'
;
import
{
mockGroupId
,
basePath
,
epicsPath
,
mockSortedBy
}
from
'
ee_jest/roadmap/mock_data
'
;
import
{
mockGroupId
,
basePath
,
epicsPath
,
mockSortedBy
,
mockEpic
,
}
from
'
ee_jest/roadmap/mock_data
'
;
const
setEpicMockData
=
state
=>
{
state
.
epics
=
[
mockEpic
];
state
.
childrenFlags
=
{
'
gid://gitlab/Epic/1
'
:
{}
};
state
.
epicIds
=
[
'
gid://gitlab/Epic/1
'
];
};
describe
(
'
Roadmap Store Mutations
'
,
()
=>
{
let
state
;
...
...
@@ -257,4 +269,53 @@ describe('Roadmap Store Mutations', () => {
expect
(
state
.
bufferSize
).
toBe
(
bufferSize
);
});
});
describe
(
'
SET_FILTER_PARAMS
'
,
()
=>
{
it
(
'
Should set `filterParams` and `hasFiltersApplied` to the state and reset existing epics
'
,
()
=>
{
const
filterParams
=
[{
foo
:
'
bar
'
},
{
bar
:
'
baz
'
}];
setEpicMockData
(
state
);
mutations
[
types
.
SET_FILTER_PARAMS
](
state
,
filterParams
);
expect
(
state
).
toMatchObject
({
filterParams
,
hasFiltersApplied
:
true
,
epics
:
[],
childrenFlags
:
{},
epicIds
:
[],
});
});
});
describe
(
'
SET_EPICS_STATE
'
,
()
=>
{
it
(
'
Should set `epicsState` to the state and reset existing epics
'
,
()
=>
{
const
epicsState
=
'
all
'
;
setEpicMockData
(
state
);
mutations
[
types
.
SET_EPICS_STATE
](
state
,
epicsState
);
expect
(
state
).
toMatchObject
({
epicsState
,
epics
:
[],
childrenFlags
:
{},
epicIds
:
[],
});
});
});
describe
(
'
SET_SORTED_BY
'
,
()
=>
{
it
(
'
Should set `sortedBy` to the state and reset existing epics
'
,
()
=>
{
const
sortedBy
=
'
start_date_asc
'
;
setEpicMockData
(
state
);
mutations
[
types
.
SET_SORTED_BY
](
state
,
sortedBy
);
expect
(
state
).
toMatchObject
({
sortedBy
,
epics
:
[],
childrenFlags
:
{},
epicIds
:
[],
});
});
});
});
locale/gitlab.pot
View file @
068c1539
...
...
@@ -2327,6 +2327,9 @@ msgstr ""
msgid "All environments"
msgstr ""
msgid "All epics"
msgstr ""
msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
msgstr ""
...
...
@@ -5021,6 +5024,9 @@ msgstr ""
msgid "Closed %{epicTimeagoDate}"
msgstr ""
msgid "Closed epics"
msgstr ""
msgid "Closed issues"
msgstr ""
...
...
@@ -16834,6 +16840,9 @@ msgstr ""
msgid "Open comment type dropdown"
msgstr ""
msgid "Open epics"
msgstr ""
msgid "Open errors"
msgstr ""
...
...
spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js
View file @
068c1539
...
...
@@ -57,7 +57,11 @@ describe('Recent Searches Dropdown Content', () => {
beforeEach
(()
=>
{
createComponent
({
items
:
[
'
foo
'
,
'
author:@root label:~foo bar
'
],
items
:
[
'
foo
'
,
'
author:@root label:~foo bar
'
,
[{
type
:
'
author_username
'
,
value
:
{
data
:
'
toby
'
,
operator
:
'
=
'
}
}],
],
isLocalStorageAvailable
:
true
,
});
});
...
...
@@ -76,7 +80,7 @@ describe('Recent Searches Dropdown Content', () => {
});
it
(
'
renders a correct amount of dropdown items
'
,
()
=>
{
expect
(
findDropdownItems
()).
toHaveLength
(
2
);
expect
(
findDropdownItems
()).
toHaveLength
(
2
);
// Ignore non-string recent item
});
it
(
'
expect second dropdown to have 2 tokens
'
,
()
=>
{
...
...
spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
View file @
068c1539
...
...
@@ -103,6 +103,19 @@ describe('FilteredSearchBarRoot', () => {
expect
(
wrapper
.
vm
.
sortDirectionTooltip
).
toBe
(
'
Sort direction: Descending
'
);
});
});
describe
(
'
filteredRecentSearches
'
,
()
=>
{
it
(
'
returns array of recent searches filtering out any string type (unsupported) items
'
,
async
()
=>
{
wrapper
.
setData
({
recentSearches
:
[{
foo
:
'
bar
'
},
'
foo
'
],
});
await
wrapper
.
vm
.
$nextTick
();
expect
(
wrapper
.
vm
.
filteredRecentSearches
).
toHaveLength
(
1
);
expect
(
wrapper
.
vm
.
filteredRecentSearches
[
0
]).
toEqual
({
foo
:
'
bar
'
});
});
});
});
describe
(
'
watchers
'
,
()
=>
{
...
...
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