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
8610b290
Commit
8610b290
authored
Mar 30, 2022
by
Tomas Bulva
Committed by
Simon Knox
Mar 30, 2022
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Issue 351689 - Prevent autocomplete searches under X characters
Changelog: changed
parent
0fce8e36
Changes
11
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
199 additions
and
17 deletions
+199
-17
app/assets/javascripts/header_search/components/app.vue
app/assets/javascripts/header_search/components/app.vue
+14
-4
app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue
...er_search/components/header_search_autocomplete_items.vue
+2
-2
app/assets/javascripts/header_search/components/header_search_scoped_items.vue
...s/header_search/components/header_search_scoped_items.vue
+4
-2
app/assets/javascripts/header_search/constants.js
app/assets/javascripts/header_search/constants.js
+2
-0
app/assets/javascripts/header_search/store/actions.js
app/assets/javascripts/header_search/store/actions.js
+3
-1
app/assets/javascripts/header_search/store/getters.js
app/assets/javascripts/header_search/store/getters.js
+0
-1
app/assets/javascripts/main.js
app/assets/javascripts/main.js
+1
-0
spec/frontend/header_search/components/app_spec.js
spec/frontend/header_search/components/app_spec.js
+13
-1
spec/frontend/header_search/components/header_search_autocomplete_items_spec.js
...earch/components/header_search_autocomplete_items_spec.js
+38
-2
spec/frontend/header_search/components/header_search_scoped_items_spec.js
...ader_search/components/header_search_scoped_items_spec.js
+27
-4
spec/frontend/header_search/mock_data.js
spec/frontend/header_search/mock_data.js
+95
-0
No files found.
app/assets/javascripts/header_search/components/app.vue
View file @
8610b290
...
...
@@ -11,6 +11,7 @@ import {
SEARCH_BOX_INDEX
,
SEARCH_INPUT_DESCRIPTION
,
SEARCH_RESULTS_DESCRIPTION
,
SEARCH_SHORTCUTS_MIN_CHARACTERS
,
}
from
'
../constants
'
;
import
HeaderSearchAutocompleteItems
from
'
./header_search_autocomplete_items.vue
'
;
import
HeaderSearchDefaultItems
from
'
./header_search_default_items.vue
'
;
...
...
@@ -50,7 +51,7 @@ export default {
},
computed
:
{
...
mapState
([
'
search
'
,
'
loading
'
]),
...
mapGetters
([
'
searchQuery
'
,
'
searchOptions
'
]),
...
mapGetters
([
'
searchQuery
'
,
'
searchOptions
'
,
'
autocompleteGroupedSearchOptions
'
]),
searchText
:
{
get
()
{
return
this
.
search
;
...
...
@@ -66,14 +67,20 @@ export default {
return
this
.
currentFocusedOption
?.
html_id
;
},
isLoggedIn
()
{
return
gon
?.
current_username
;
return
Boolean
(
gon
?.
current_username
)
;
},
showSearchDropdown
()
{
return
this
.
showDropdown
&&
this
.
isLoggedIn
;
const
hasResultsUnderMinCharacters
=
this
.
searchText
?.
length
===
1
?
this
?.
autocompleteGroupedSearchOptions
?.
length
>
0
:
true
;
return
this
.
showDropdown
&&
this
.
isLoggedIn
&&
hasResultsUnderMinCharacters
;
},
showDefaultItems
()
{
return
!
this
.
searchText
;
},
showShortcuts
()
{
return
this
.
searchText
&&
this
.
searchText
?.
length
>=
SEARCH_SHORTCUTS_MIN_CHARACTERS
;
},
defaultIndex
()
{
if
(
this
.
showDefaultItems
)
{
return
SEARCH_BOX_INDEX
;
...
...
@@ -182,7 +189,10 @@ export default {
:current-focused-option=
"currentFocusedOption"
/>
<template
v-else
>
<header-search-scoped-items
:current-focused-option=
"currentFocusedOption"
/>
<header-search-scoped-items
v-if=
"showShortcuts"
:current-focused-option=
"currentFocusedOption"
/>
<header-search-autocomplete-items
:current-focused-option=
"currentFocusedOption"
/>
</
template
>
</div>
...
...
app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue
View file @
8610b290
...
...
@@ -72,8 +72,8 @@ export default {
<
template
>
<div>
<template
v-if=
"!loading"
>
<div
v-for=
"
option
in autocompleteGroupedSearchOptions"
:key=
"option.category"
>
<gl-dropdown-divider
/>
<div
v-for=
"
(option, index)
in autocompleteGroupedSearchOptions"
:key=
"option.category"
>
<gl-dropdown-divider
v-if=
"index > 0"
/>
<gl-dropdown-section-header>
{{
option
.
category
}}
</gl-dropdown-section-header>
<gl-dropdown-item
v-for=
"data in option.data"
...
...
app/assets/javascripts/header_search/components/header_search_scoped_items.vue
View file @
8610b290
<
script
>
import
{
GlDropdownItem
}
from
'
@gitlab/ui
'
;
import
{
GlDropdownItem
,
GlDropdownDivider
}
from
'
@gitlab/ui
'
;
import
{
mapState
,
mapGetters
}
from
'
vuex
'
;
import
{
__
,
sprintf
}
from
'
~/locale
'
;
...
...
@@ -7,6 +7,7 @@ export default {
name
:
'
HeaderSearchScopedItems
'
,
components
:
{
GlDropdownItem
,
GlDropdownDivider
,
},
props
:
{
currentFocusedOption
:
{
...
...
@@ -17,7 +18,7 @@ export default {
},
computed
:
{
...
mapState
([
'
search
'
]),
...
mapGetters
([
'
scopedSearchOptions
'
]),
...
mapGetters
([
'
scopedSearchOptions
'
,
'
autocompleteGroupedSearchOptions
'
]),
},
methods
:
{
isOptionFocused
(
option
)
{
...
...
@@ -53,5 +54,6 @@ export default {
<span
v-if=
"option.scope"
class=
"gl-font-style-italic"
>
{{
option
.
scope
}}
</span>
</span>
</gl-dropdown-item>
<gl-dropdown-divider
v-if=
"autocompleteGroupedSearchOptions.length > 0"
/>
</div>
</
template
>
app/assets/javascripts/header_search/constants.js
View file @
8610b290
...
...
@@ -28,6 +28,8 @@ export const FIRST_DROPDOWN_INDEX = 0;
export
const
SEARCH_BOX_INDEX
=
-
1
;
export
const
SEARCH_SHORTCUTS_MIN_CHARACTERS
=
2
;
export
const
SEARCH_INPUT_DESCRIPTION
=
'
search-input-description
'
;
export
const
SEARCH_RESULTS_DESCRIPTION
=
'
search-results-description
'
;
app/assets/javascripts/header_search/store/actions.js
View file @
8610b290
...
...
@@ -5,7 +5,9 @@ export const fetchAutocompleteOptions = ({ commit, getters }) => {
commit
(
types
.
REQUEST_AUTOCOMPLETE
);
return
axios
.
get
(
getters
.
autocompleteQuery
)
.
then
(({
data
})
=>
commit
(
types
.
RECEIVE_AUTOCOMPLETE_SUCCESS
,
data
))
.
then
(({
data
})
=>
{
commit
(
types
.
RECEIVE_AUTOCOMPLETE_SUCCESS
,
data
);
})
.
catch
(()
=>
{
commit
(
types
.
RECEIVE_AUTOCOMPLETE_ERROR
);
});
...
...
app/assets/javascripts/header_search/store/getters.js
View file @
8610b290
...
...
@@ -190,7 +190,6 @@ export const autocompleteGroupedSearchOptions = (state) => {
results
.
push
(
groupedOptions
[
option
.
category
]);
}
});
return
results
;
};
...
...
app/assets/javascripts/main.js
View file @
8610b290
...
...
@@ -127,6 +127,7 @@ function deferredInitialisation() {
// In case the user started searching before we bootstrapped, let's pass the search along.
const
initialSearchValue
=
searchInputBox
.
value
;
await
initHeaderSearchApp
(
initialSearchValue
);
// this is new #search input element. We need to re-find it.
document
.
querySelector
(
'
#search
'
).
focus
();
})
.
catch
(()
=>
{});
...
...
spec/frontend/header_search/components/app_spec.js
View file @
8610b290
...
...
@@ -16,6 +16,7 @@ import {
MOCK_USERNAME
,
MOCK_DEFAULT_SEARCH_OPTIONS
,
MOCK_SCOPED_SEARCH_OPTIONS
,
MOCK_SORTED_AUTOCOMPLETE_OPTIONS
,
}
from
'
../mock_data
'
;
Vue
.
use
(
Vuex
);
...
...
@@ -108,6 +109,11 @@ describe('HeaderSearchApp', () => {
search | showDefault | showScoped | showAutocomplete | showDropdownNavigation
${
null
}
|
${
true
}
|
${
false
}
|
${
false
}
|
${
true
}
${
''
}
|
${
true
}
|
${
false
}
|
${
false
}
|
${
true
}
${
'
1
'
}
|
${
false
}
|
${
false
}
|
${
false
}
|
${
false
}
${
'
)
'
}
|
${
false
}
|
${
false
}
|
${
false
}
|
${
false
}
${
'
t
'
}
|
${
false
}
|
${
false
}
|
${
true
}
|
${
true
}
${
'
te
'
}
|
${
false
}
|
${
true
}
|
${
true
}
|
${
true
}
${
'
tes
'
}
|
${
false
}
|
${
true
}
|
${
true
}
|
${
true
}
${
MOCK_SEARCH
}
|
${
false
}
|
${
true
}
|
${
true
}
|
${
true
}
`
(
'
Header Search Dropdown Items
'
,
...
...
@@ -115,7 +121,13 @@ describe('HeaderSearchApp', () => {
describe
(
`when search is
${
search
}
`
,
()
=>
{
beforeEach
(()
=>
{
window
.
gon
.
current_username
=
MOCK_USERNAME
;
createComponent
({
search
});
createComponent
(
{
search
},
{
autocompleteGroupedSearchOptions
:
()
=>
search
.
match
(
/^
[
A-Za-z
]
+$/g
)
?
MOCK_SORTED_AUTOCOMPLETE_OPTIONS
:
[],
},
);
findHeaderSearchInput
().
vm
.
$emit
(
'
click
'
);
});
...
...
spec/frontend/header_search/components/header_search_autocomplete_items_spec.js
View file @
8610b290
import
{
GlDropdownItem
,
GlLoadingIcon
,
GlAvatar
,
GlAlert
}
from
'
@gitlab/ui
'
;
import
{
GlDropdownItem
,
GlLoadingIcon
,
GlAvatar
,
GlAlert
,
GlDropdownDivider
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
Vue
,
{
nextTick
}
from
'
vue
'
;
import
Vuex
from
'
vuex
'
;
...
...
@@ -9,7 +9,14 @@ import {
PROJECTS_CATEGORY
,
SMALL_AVATAR_PX
,
}
from
'
~/header_search/constants
'
;
import
{
MOCK_GROUPED_AUTOCOMPLETE_OPTIONS
,
MOCK_SORTED_AUTOCOMPLETE_OPTIONS
}
from
'
../mock_data
'
;
import
{
MOCK_GROUPED_AUTOCOMPLETE_OPTIONS
,
MOCK_SORTED_AUTOCOMPLETE_OPTIONS
,
MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_SETTINGS_HELP
,
MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_HELP
,
MOCK_SEARCH
,
MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_2
,
}
from
'
../mock_data
'
;
Vue
.
use
(
Vuex
);
...
...
@@ -41,6 +48,7 @@ describe('HeaderSearchAutocompleteItems', () => {
});
const
findDropdownItems
=
()
=>
wrapper
.
findAllComponents
(
GlDropdownItem
);
const
findGlDropdownDividers
=
()
=>
wrapper
.
findAllComponents
(
GlDropdownDivider
);
const
findFirstDropdownItem
=
()
=>
findDropdownItems
().
at
(
0
);
const
findDropdownItemTitles
=
()
=>
findDropdownItems
().
wrappers
.
map
((
w
)
=>
w
.
text
());
const
findDropdownItemLinks
=
()
=>
findDropdownItems
().
wrappers
.
map
((
w
)
=>
w
.
attributes
(
'
href
'
));
...
...
@@ -140,6 +148,34 @@ describe('HeaderSearchAutocompleteItems', () => {
});
});
});
describe.each`
search | items | dividerCount
${null} | ${[]} | ${0}
${''} | ${[]} | ${0}
${'1'} | ${[]} | ${0}
${')'} | ${[]} | ${0}
${'t'} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_SETTINGS_HELP} | ${1}
${'te'} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_HELP} | ${0}
${'tes'} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_2} | ${1}
${MOCK_SEARCH} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_2} | ${1}
`('Header Search Dropdown Dividers', ({ search, items, dividerCount }) => {
describe(`when search is ${search}`, () => {
beforeEach(() => {
createComponent(
{ search },
{
autocompleteGroupedSearchOptions: () => items,
},
{},
);
});
it(`component should have ${dividerCount} dividers`, () => {
expect(findGlDropdownDividers()).toHaveLength(dividerCount);
});
});
});
});
describe('watchers', () => {
...
...
spec/frontend/header_search/components/header_search_scoped_items_spec.js
View file @
8610b290
import
{
GlDropdownItem
}
from
'
@gitlab/ui
'
;
import
{
GlDropdownItem
,
GlDropdownDivider
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
Vue
from
'
vue
'
;
import
Vuex
from
'
vuex
'
;
import
{
trimText
}
from
'
helpers/text_helper
'
;
import
HeaderSearchScopedItems
from
'
~/header_search/components/header_search_scoped_items.vue
'
;
import
{
MOCK_SEARCH
,
MOCK_SCOPED_SEARCH_OPTIONS
}
from
'
../mock_data
'
;
import
{
MOCK_SEARCH
,
MOCK_SCOPED_SEARCH_OPTIONS
,
MOCK_GROUPED_AUTOCOMPLETE_OPTIONS
,
}
from
'
../mock_data
'
;
Vue
.
use
(
Vuex
);
describe
(
'
HeaderSearchScopedItems
'
,
()
=>
{
let
wrapper
;
const
createComponent
=
(
initialState
,
props
)
=>
{
const
createComponent
=
(
initialState
,
mockGetters
,
props
)
=>
{
const
store
=
new
Vuex
.
Store
({
state
:
{
search
:
MOCK_SEARCH
,
...
...
@@ -19,6 +23,8 @@ describe('HeaderSearchScopedItems', () => {
},
getters
:
{
scopedSearchOptions
:
()
=>
MOCK_SCOPED_SEARCH_OPTIONS
,
autocompleteGroupedSearchOptions
:
()
=>
MOCK_GROUPED_AUTOCOMPLETE_OPTIONS
,
...
mockGetters
,
},
});
...
...
@@ -35,6 +41,7 @@ describe('HeaderSearchScopedItems', () => {
});
const
findDropdownItems
=
()
=>
wrapper
.
findAllComponents
(
GlDropdownItem
);
const
findGlDropdownDivider
=
()
=>
wrapper
.
findComponent
(
GlDropdownDivider
);
const
findFirstDropdownItem
=
()
=>
findDropdownItems
().
at
(
0
);
const
findDropdownItemTitles
=
()
=>
findDropdownItems
().
wrappers
.
map
((
w
)
=>
trimText
(
w
.
text
()));
const
findDropdownItemAriaLabels
=
()
=>
...
...
@@ -79,7 +86,7 @@ describe('HeaderSearchScopedItems', () => {
`
(
'
isOptionFocused
'
,
({
currentFocusedOption
,
isFocused
,
ariaSelected
})
=>
{
describe
(
`when currentFocusedOption.html_id is
${
currentFocusedOption
?.
html_id
}
`, () => {
beforeEach(() => {
createComponent({}, { currentFocusedOption });
createComponent({}, {
}, {
currentFocusedOption });
});
it(`
should$
{
isFocused
?
''
:
'
not
'
}
have
gl
-
bg
-
gray
-
50
applied
`, () => {
...
...
@@ -91,5 +98,21 @@ describe('HeaderSearchScopedItems', () => {
});
});
});
describe.each`
autosuggestResults | showDivider
${[]} | ${false}
${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${true}
`('scoped search items', ({ autosuggestResults, showDivider }) => {
describe(`when when we have ${autosuggestResults.length} auto-sugest results`, () => {
beforeEach(() => {
createComponent({}, { autocompleteGroupedSearchOptions: () => autosuggestResults }, {});
});
it(`divider should${showDivider ? '' : ' not'} be shown`, () => {
expect(findGlDropdownDivider().exists()).toBe(showDivider);
});
});
});
});
});
spec/frontend/header_search/mock_data.js
View file @
8610b290
...
...
@@ -226,3 +226,98 @@ export const MOCK_SORTED_AUTOCOMPLETE_OPTIONS = [
url
:
'
help/gitlab
'
,
},
];
export
const
MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_HELP
=
[
{
category
:
'
Help
'
,
data
:
[
{
html_id
:
'
autocomplete-Help-1
'
,
category
:
'
Help
'
,
label
:
'
Rake Tasks Help
'
,
url
:
'
/help/raketasks/index
'
,
},
{
html_id
:
'
autocomplete-Help-2
'
,
category
:
'
Help
'
,
label
:
'
System Hooks Help
'
,
url
:
'
/help/system_hooks/system_hooks
'
,
},
],
},
];
export
const
MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_SETTINGS_HELP
=
[
{
category
:
'
Settings
'
,
data
:
[
{
html_id
:
'
autocomplete-Settings-0
'
,
category
:
'
Settings
'
,
label
:
'
User settings
'
,
url
:
'
/-/profile
'
,
},
{
html_id
:
'
autocomplete-Settings-3
'
,
category
:
'
Settings
'
,
label
:
'
Admin Section
'
,
url
:
'
/admin
'
,
},
],
},
{
category
:
'
Help
'
,
data
:
[
{
html_id
:
'
autocomplete-Help-1
'
,
category
:
'
Help
'
,
label
:
'
Rake Tasks Help
'
,
url
:
'
/help/raketasks/index
'
,
},
{
html_id
:
'
autocomplete-Help-2
'
,
category
:
'
Help
'
,
label
:
'
System Hooks Help
'
,
url
:
'
/help/system_hooks/system_hooks
'
,
},
],
},
];
export
const
MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_2
=
[
{
category
:
'
Groups
'
,
data
:
[
{
html_id
:
'
autocomplete-Groups-0
'
,
category
:
'
Groups
'
,
id
:
148
,
label
:
'
Jashkenas / Test Subgroup / test-subgroup
'
,
url
:
'
/jashkenas/test-subgroup/test-subgroup
'
,
avatar_url
:
''
,
},
{
html_id
:
'
autocomplete-Groups-1
'
,
category
:
'
Groups
'
,
id
:
147
,
label
:
'
Jashkenas / Test Subgroup
'
,
url
:
'
/jashkenas/test-subgroup
'
,
avatar_url
:
''
,
},
],
},
{
category
:
'
Projects
'
,
data
:
[
{
html_id
:
'
autocomplete-Projects-2
'
,
category
:
'
Projects
'
,
id
:
1
,
value
:
'
Gitlab Test
'
,
label
:
'
Gitlab Org / Gitlab Test
'
,
url
:
'
/gitlab-org/gitlab-test
'
,
avatar_url
:
'
/uploads/-/system/project/avatar/1/icons8-gitlab-512.png
'
,
},
],
},
];
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