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
0687d0d8
Commit
0687d0d8
authored
Jul 10, 2020
by
Kushal Pandya
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add search history support for Requirements
Adds support for saving recent searches in Requirements
parent
3a980767
Changes
11
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
136 additions
and
49 deletions
+136
-49
app/assets/javascripts/filtered_search/constants.js
app/assets/javascripts/filtered_search/constants.js
+2
-0
app/assets/javascripts/filtered_search/stores/recent_searches_store.js
...vascripts/filtered_search/stores/recent_searches_store.js
+9
-3
app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
...mponents/filtered_search_bar/filtered_search_bar_root.vue
+34
-20
app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
...ed/components/filtered_search_bar/tokens/author_token.vue
+13
-3
ee/app/assets/javascripts/requirements/components/requirements_root.vue
...javascripts/requirements/components/requirements_root.vue
+3
-0
ee/changelogs/unreleased/kp-requirements-add-search-history-support.yml
...unreleased/kp-requirements-add-search-history-support.yml
+5
-0
ee/spec/frontend/requirements/components/requirements_root_spec.js
...rontend/requirements/components/requirements_root_spec.js
+3
-0
spec/frontend/filtered_search/stores/recent_searches_store_spec.js
...tend/filtered_search/stores/recent_searches_store_spec.js
+9
-0
spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
...ents/filtered_search_bar/filtered_search_bar_root_spec.js
+26
-9
spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
...nd/vue_shared/components/filtered_search_bar/mock_data.js
+23
-0
spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js
...omponents/filtered_search_bar/tokens/author_token_spec.js
+9
-14
No files found.
app/assets/javascripts/filtered_search/constants.js
View file @
0687d0d8
...
...
@@ -9,3 +9,5 @@ export const FILTER_TYPE = {
none
:
'
none
'
,
any
:
'
any
'
,
};
export
const
MAX_HISTORY_SIZE
=
5
;
app/assets/javascripts/filtered_search/stores/recent_searches_store.js
View file @
0687d0d8
import
{
uniq
}
from
'
lodash
'
;
import
{
uniqWith
,
isEqual
}
from
'
lodash
'
;
import
{
MAX_HISTORY_SIZE
}
from
'
../constants
'
;
class
RecentSearchesStore
{
constructor
(
initialState
=
{},
allowedKeys
)
{
...
...
@@ -17,8 +19,12 @@ class RecentSearchesStore {
}
setRecentSearches
(
searches
=
[])
{
const
trimmedSearches
=
searches
.
map
(
search
=>
search
.
trim
());
this
.
state
.
recentSearches
=
uniq
(
trimmedSearches
).
slice
(
0
,
5
);
const
trimmedSearches
=
searches
.
map
(
search
=>
typeof
search
===
'
string
'
?
search
.
trim
()
:
search
,
);
// Do object equality check to remove duplicates.
this
.
state
.
recentSearches
=
uniqWith
(
trimmedSearches
,
isEqual
).
slice
(
0
,
MAX_HISTORY_SIZE
);
return
this
.
state
.
recentSearches
;
}
}
...
...
app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
View file @
0687d0d8
...
...
@@ -98,6 +98,15 @@ export default {
{},
);
},
tokenTitles
()
{
return
this
.
tokens
.
reduce
(
(
tokenSymbols
,
token
)
=>
({
...
tokenSymbols
,
[
token
.
type
]:
token
.
title
,
}),
{},
);
},
sortDirectionIcon
()
{
return
this
.
selectedSortDirection
===
SortDirection
.
ascending
?
'
sort-lowest
'
...
...
@@ -112,11 +121,10 @@ export default {
watch
:
{
/**
* GlFilteredSearch currently doesn't emit any event when
* search field is cleared, but we still want our parent
* component to know that filters were cleared and do
* necessary data refetch, so this watcher is basically
* a dirty hack/workaround to identify if filter input
* was cleared. :(
* tokens are manually removed from search field so we'd
* never know when user actually clears all the tokens.
* This watcher listens for updates to `filterValue` on
* such instances. :(
*/
filterValue
(
value
)
{
const
[
firstVal
]
=
value
;
...
...
@@ -188,25 +196,16 @@ export default {
:
SortDirection
.
ascending
;
this
.
$emit
(
'
onSort
'
,
this
.
selectedSortOption
.
sortDirection
[
this
.
selectedSortDirection
]);
},
handleClearHistory
()
{
const
resultantSearches
=
this
.
recentSearchesStore
.
setRecentSearches
([]);
this
.
recentSearchesService
.
save
(
resultantSearches
);
},
handleFilterSubmit
(
filters
)
{
if
(
this
.
recentSearchesStorageKey
)
{
this
.
recentSearchesPromise
.
then
(()
=>
{
if
(
filters
.
length
)
{
const
searchTokens
=
filters
.
map
(
filter
=>
{
// check filter was plain text search
if
(
typeof
filter
===
'
string
'
)
{
return
filter
;
}
// filter was a token.
return
`
${
filter
.
type
}
:
${
filter
.
value
.
operator
}${
this
.
tokenSymbols
[
filter
.
type
]}${
filter
.
value
.
data
}
`
;
});
const
resultantSearches
=
this
.
recentSearchesStore
.
addRecentSearch
(
searchTokens
.
join
(
'
'
),
);
const
resultantSearches
=
this
.
recentSearchesStore
.
addRecentSearch
(
filters
);
this
.
recentSearchesService
.
save
(
resultantSearches
);
}
})
...
...
@@ -228,8 +227,23 @@ export default {
:available-tokens=
"tokens"
:history-items=
"getRecentSearches()"
class=
"flex-grow-1"
@
history-item-selected=
"$emit('onFilter', filters)"
@
clear-history=
"handleClearHistory"
@
submit=
"handleFilterSubmit"
/>
@
clear=
"$emit('onFilter', [])"
>
<template
#history-item
="
{ historyItem }">
<template
v-for=
"token in historyItem"
>
<span
v-if=
"typeof token === 'string'"
:key=
"token"
class=
"gl-px-1"
>
"
{{
token
}}
"
</span>
<span
v-else
:key=
"`$
{token.type}-${token.value.data}`" class="gl-px-1">
<span
v-if=
"tokenTitles[token.type]"
>
{{
tokenTitles
[
token
.
type
]
}}
:
{{
token
.
value
.
operator
}}
</span
>
<strong>
{{
tokenSymbols
[
token
.
type
]
}}{{
token
.
value
.
data
}}
</strong>
</span>
</
template
>
</template>
</gl-filtered-search>
<gl-button-group
class=
"sort-dropdown-container d-flex"
>
<gl-dropdown
:text=
"selectedSortOption.title"
:right=
"true"
class=
"w-100"
>
<gl-dropdown-item
...
...
app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
View file @
0687d0d8
...
...
@@ -46,6 +46,16 @@ export default {
return
this
.
authors
.
find
(
author
=>
author
.
username
.
toLowerCase
()
===
this
.
currentValue
);
},
},
watch
:
{
active
:
{
immediate
:
true
,
handler
(
newValue
)
{
if
(
!
newValue
&&
!
this
.
authors
.
length
)
{
this
.
fetchAuthorBySearchTerm
(
this
.
value
.
data
);
}
},
},
},
methods
:
{
fetchAuthorBySearchTerm
(
searchTerm
)
{
const
fetchPromise
=
this
.
config
.
fetchPath
...
...
@@ -89,9 +99,9 @@ export default {
<span>
{{
activeAuthor
?
activeAuthor
.
name
:
inputValue
}}
</span>
</
template
>
<
template
#suggestions
>
<gl-filtered-search-suggestion
:value=
"$options.anyAuthor"
>
{{
__
(
'
Any
'
)
}}
</gl-filtered-search-suggestion>
<gl-filtered-search-suggestion
:value=
"$options.anyAuthor"
>
{{
__
(
'
Any
'
)
}}
</gl-filtered-search-suggestion>
<gl-dropdown-divider
/>
<gl-loading-icon
v-if=
"loading"
/>
<template
v-else
>
...
...
ee/app/assets/javascripts/requirements/components/requirements_root.vue
View file @
0687d0d8
...
...
@@ -10,6 +10,7 @@ import { 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
{
ANY_AUTHOR
}
from
'
~/vue_shared/components/filtered_search_bar/constants
'
;
import
RecentSearchesStorageKeys
from
'
ee/filtered_search/recent_searches_storage_keys
'
;
import
RequirementsTabs
from
'
./requirements_tabs.vue
'
;
import
RequirementsLoading
from
'
./requirements_loading.vue
'
;
...
...
@@ -27,6 +28,7 @@ import { FilterState, AvailableSortOptions, DEFAULT_PAGE_SIZE } from '../constan
export
default
{
DEFAULT_PAGE_SIZE
,
AvailableSortOptions
,
requirementsRecentSearchesKey
:
RecentSearchesStorageKeys
.
requirements
,
components
:
{
GlPagination
,
FilteredSearchBar
,
...
...
@@ -524,6 +526,7 @@ export default {
:sort-options=
"$options.AvailableSortOptions"
:initial-filter-value=
"getFilteredSearchValue()"
:initial-sort-by=
"sortBy"
:recent-searches-storage-key=
"$options.requirementsRecentSearchesKey"
class=
"row-content-block"
@
onFilter=
"handleFilterRequirements"
@
onSort=
"handleSortRequirements"
...
...
ee/changelogs/unreleased/kp-requirements-add-search-history-support.yml
0 → 100644
View file @
0687d0d8
---
title
:
Add search history support for Requirements
merge_request
:
36554
author
:
type
:
added
ee/spec/frontend/requirements/components/requirements_root_spec.js
View file @
0687d0d8
...
...
@@ -765,6 +765,9 @@ describe('RequirementsRoot', () => {
fetchAuthors
:
expect
.
any
(
Function
),
},
]);
expect
(
wrapper
.
find
(
FilteredSearchBarRoot
).
props
(
'
recentSearchesStorageKey
'
)).
toBe
(
'
requirements-recent-searches
'
,
);
});
it
(
'
renders empty state when query results are empty
'
,
()
=>
{
...
...
spec/frontend/filtered_search/stores/recent_searches_store_spec.js
View file @
0687d0d8
...
...
@@ -44,6 +44,15 @@ describe('RecentSearchesStore', () => {
expect
(
store
.
state
.
recentSearches
).
toEqual
([
'
baz
'
,
'
qux
'
]);
});
it
(
'
handles non-string values
'
,
()
=>
{
store
.
setRecentSearches
([
'
foo
'
,
{
foo
:
'
bar
'
},
{
foo
:
'
bar
'
},
[
'
foobar
'
]]);
// 1. String values will be trimmed of leading/trailing spaces
// 2. Comparison will account for objects to remove duplicates
// 3. Old behaviour of handling string values stays as it is.
expect
(
store
.
state
.
recentSearches
).
toEqual
([
'
foo
'
,
{
foo
:
'
bar
'
},
[
'
foobar
'
]]);
});
it
(
'
only keeps track of 5 items
'
,
()
=>
{
store
.
setRecentSearches
([
'
1
'
,
'
2
'
,
'
3
'
,
'
4
'
,
'
5
'
,
'
6
'
,
'
7
'
]);
...
...
spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
View file @
0687d0d8
...
...
@@ -13,7 +13,7 @@ import { SortDirection } from '~/vue_shared/components/filtered_search_bar/const
import
RecentSearchesStore
from
'
~/filtered_search/stores/recent_searches_store
'
;
import
RecentSearchesService
from
'
~/filtered_search/services/recent_searches_service
'
;
import
{
mockAvailableTokens
,
mockSortOptions
}
from
'
./mock_data
'
;
import
{
mockAvailableTokens
,
mockSortOptions
,
mockHistoryItems
}
from
'
./mock_data
'
;
const
createComponent
=
({
namespace
=
'
gitlab-org/gitlab-test
'
,
...
...
@@ -53,11 +53,17 @@ describe('FilteredSearchBarRoot', () => {
describe
(
'
computed
'
,
()
=>
{
describe
(
'
tokenSymbols
'
,
()
=>
{
it
(
'
returns a
rray of
map containing type and symbols from `tokens` prop
'
,
()
=>
{
it
(
'
returns a map containing type and symbols from `tokens` prop
'
,
()
=>
{
expect
(
wrapper
.
vm
.
tokenSymbols
).
toEqual
({
author_username
:
'
@
'
});
});
});
describe
(
'
tokenTitles
'
,
()
=>
{
it
(
'
returns a map containing type and title from `tokens` prop
'
,
()
=>
{
expect
(
wrapper
.
vm
.
tokenTitles
).
toEqual
({
author_username
:
'
Author
'
});
});
});
describe
(
'
sortDirectionIcon
'
,
()
=>
{
it
(
'
returns string "sort-lowest" when `selectedSortDirection` is "ascending"
'
,
()
=>
{
wrapper
.
setData
({
...
...
@@ -172,6 +178,19 @@ describe('FilteredSearchBarRoot', () => {
});
});
describe
(
'
handleClearHistory
'
,
()
=>
{
it
(
'
clears search history from recent searches store
'
,
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
.
recentSearchesStore
,
'
setRecentSearches
'
).
mockReturnValue
([]);
jest
.
spyOn
(
wrapper
.
vm
.
recentSearchesService
,
'
save
'
);
wrapper
.
vm
.
handleClearHistory
();
expect
(
wrapper
.
vm
.
recentSearchesStore
.
setRecentSearches
).
toHaveBeenCalledWith
([]);
expect
(
wrapper
.
vm
.
recentSearchesService
.
save
).
toHaveBeenCalledWith
([]);
expect
(
wrapper
.
vm
.
getRecentSearches
()).
toEqual
([]);
});
});
describe
(
'
handleFilterSubmit
'
,
()
=>
{
const
mockFilters
=
[
{
...
...
@@ -186,14 +205,11 @@ describe('FilteredSearchBarRoot', () => {
it
(
'
calls `recentSearchesStore.addRecentSearch` with serialized value of provided `filters` param
'
,
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
.
recentSearchesStore
,
'
addRecentSearch
'
);
// jest.spyOn(wrapper.vm.recentSearchesService, 'save');
wrapper
.
vm
.
handleFilterSubmit
(
mockFilters
);
return
wrapper
.
vm
.
recentSearchesPromise
.
then
(()
=>
{
expect
(
wrapper
.
vm
.
recentSearchesStore
.
addRecentSearch
).
toHaveBeenCalledWith
(
'
author_username:=@root foo
'
,
);
expect
(
wrapper
.
vm
.
recentSearchesStore
.
addRecentSearch
).
toHaveBeenCalledWith
(
mockFilters
);
});
});
...
...
@@ -203,9 +219,7 @@ describe('FilteredSearchBarRoot', () => {
wrapper
.
vm
.
handleFilterSubmit
(
mockFilters
);
return
wrapper
.
vm
.
recentSearchesPromise
.
then
(()
=>
{
expect
(
wrapper
.
vm
.
recentSearchesService
.
save
).
toHaveBeenCalledWith
([
'
author_username:=@root foo
'
,
]);
expect
(
wrapper
.
vm
.
recentSearchesService
.
save
).
toHaveBeenCalledWith
([
mockFilters
]);
});
});
...
...
@@ -224,6 +238,8 @@ describe('FilteredSearchBarRoot', () => {
selectedSortDirection
:
SortDirection
.
descending
,
});
wrapper
.
vm
.
recentSearchesStore
.
setRecentSearches
(
mockHistoryItems
);
return
wrapper
.
vm
.
$nextTick
();
});
...
...
@@ -232,6 +248,7 @@ describe('FilteredSearchBarRoot', () => {
expect
(
glFilteredSearchEl
.
props
(
'
placeholder
'
)).
toBe
(
'
Filter requirements
'
);
expect
(
glFilteredSearchEl
.
props
(
'
availableTokens
'
)).
toEqual
(
mockAvailableTokens
);
expect
(
glFilteredSearchEl
.
props
(
'
historyItems
'
)).
toEqual
(
mockHistoryItems
);
});
it
(
'
renders sort dropdown component
'
,
()
=>
{
...
...
spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
View file @
0687d0d8
...
...
@@ -44,6 +44,29 @@ export const mockAuthorToken = {
export
const
mockAvailableTokens
=
[
mockAuthorToken
];
export
const
mockHistoryItems
=
[
[
{
type
:
'
author_username
'
,
value
:
{
data
:
'
toby
'
,
operator
:
'
=
'
,
},
},
'
duo
'
,
],
[
{
type
:
'
author_username
'
,
value
:
{
data
:
'
root
'
,
operator
:
'
=
'
,
},
},
'
si
'
,
],
];
export
const
mockSortOptions
=
[
{
id
:
1
,
...
...
spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js
View file @
0687d0d8
...
...
@@ -11,11 +11,12 @@ import { mockAuthorToken, mockAuthors } from '../mock_data';
jest
.
mock
(
'
~/flash
'
);
const
createComponent
=
({
config
=
mockAuthorToken
,
value
=
{
data
:
''
}
}
=
{})
=>
const
createComponent
=
({
config
=
mockAuthorToken
,
value
=
{
data
:
''
}
,
active
=
false
}
=
{})
=>
mount
(
AuthorToken
,
{
propsData
:
{
config
,
value
,
active
,
},
provide
:
{
portalName
:
'
fake target
'
,
...
...
@@ -51,29 +52,23 @@ describe('AuthorToken', () => {
describe
(
'
computed
'
,
()
=>
{
describe
(
'
currentValue
'
,
()
=>
{
it
(
'
returns lowercase string for `value.data`
'
,
()
=>
{
wrapper
.
setProps
({
value
:
{
data
:
'
FOO
'
},
});
wrapper
=
createComponent
({
value
:
{
data
:
'
FOO
'
}
});
return
wrapper
.
vm
.
$nextTick
(()
=>
{
expect
(
wrapper
.
vm
.
currentValue
).
toBe
(
'
foo
'
);
});
expect
(
wrapper
.
vm
.
currentValue
).
toBe
(
'
foo
'
);
});
});
describe
(
'
activeAuthor
'
,
()
=>
{
it
(
'
returns object for currently present `value.data`
'
,
()
=>
{
it
(
'
returns object for currently present `value.data`
'
,
async
()
=>
{
wrapper
=
createComponent
({
value
:
{
data
:
mockAuthors
[
0
].
username
}
});
wrapper
.
setData
({
authors
:
mockAuthors
,
});
wrapper
.
setProps
({
value
:
{
data
:
mockAuthors
[
0
].
username
},
});
await
wrapper
.
vm
.
$nextTick
();
return
wrapper
.
vm
.
$nextTick
(()
=>
{
expect
(
wrapper
.
vm
.
activeAuthor
).
toEqual
(
mockAuthors
[
0
]);
});
expect
(
wrapper
.
vm
.
activeAuthor
).
toEqual
(
mockAuthors
[
0
]);
});
});
});
...
...
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