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
0
Merge Requests
0
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
Jérome Perrin
gitlab-ce
Commits
f44fb5cf
Commit
f44fb5cf
authored
Jan 30, 2017
by
Clement Ho
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add filtered search visual tokens
parent
b5cb1115
Changes
32
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
32 changed files
with
2244 additions
and
376 deletions
+2244
-376
app/assets/javascripts/droplab/droplab_ajax.js
app/assets/javascripts/droplab/droplab_ajax.js
+5
-1
app/assets/javascripts/filtered_search/dropdown_hint.js
app/assets/javascripts/filtered_search/dropdown_hint.js
+18
-1
app/assets/javascripts/filtered_search/dropdown_user.js
app/assets/javascripts/filtered_search/dropdown_user.js
+6
-1
app/assets/javascripts/filtered_search/dropdown_utils.js
app/assets/javascripts/filtered_search/dropdown_utils.js
+50
-21
app/assets/javascripts/filtered_search/filtered_search_bundle.js
...ets/javascripts/filtered_search/filtered_search_bundle.js
+1
-0
app/assets/javascripts/filtered_search/filtered_search_dropdown.js
...s/javascripts/filtered_search/filtered_search_dropdown.js
+1
-1
app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
...ripts/filtered_search/filtered_search_dropdown_manager.js
+13
-38
app/assets/javascripts/filtered_search/filtered_search_manager.js
...ts/javascripts/filtered_search/filtered_search_manager.js
+158
-16
app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
...ascripts/filtered_search/filtered_search_visual_tokens.js
+200
-0
app/assets/stylesheets/framework/filters.scss
app/assets/stylesheets/framework/filters.scss
+99
-2
app/assets/stylesheets/framework/variables.scss
app/assets/stylesheets/framework/variables.scss
+9
-0
app/views/shared/issuable/_search_bar.html.haml
app/views/shared/issuable/_search_bar.html.haml
+7
-4
spec/features/issues/filtered_search/dropdown_assignee_spec.rb
...features/issues/filtered_search/dropdown_assignee_spec.rb
+9
-3
spec/features/issues/filtered_search/dropdown_author_spec.rb
spec/features/issues/filtered_search/dropdown_author_spec.rb
+5
-2
spec/features/issues/filtered_search/dropdown_hint_spec.rb
spec/features/issues/filtered_search/dropdown_hint_spec.rb
+55
-8
spec/features/issues/filtered_search/dropdown_label_spec.rb
spec/features/issues/filtered_search/dropdown_label_spec.rb
+20
-11
spec/features/issues/filtered_search/dropdown_milestone_spec.rb
...eatures/issues/filtered_search/dropdown_milestone_spec.rb
+19
-9
spec/features/issues/filtered_search/filter_issues_spec.rb
spec/features/issues/filtered_search/filter_issues_spec.rb
+207
-160
spec/features/issues/filtered_search/search_bar_spec.rb
spec/features/issues/filtered_search/search_bar_spec.rb
+3
-1
spec/features/issues/filtered_search/visual_tokens_spec.rb
spec/features/issues/filtered_search/visual_tokens_spec.rb
+306
-0
spec/features/merge_requests/filter_by_labels_spec.rb
spec/features/merge_requests/filter_by_labels_spec.rb
+1
-1
spec/features/merge_requests/filter_by_milestone_spec.rb
spec/features/merge_requests/filter_by_milestone_spec.rb
+3
-0
spec/features/merge_requests/filter_merge_requests_spec.rb
spec/features/merge_requests/filter_merge_requests_spec.rb
+64
-24
spec/features/merge_requests/reset_filters_spec.rb
spec/features/merge_requests/reset_filters_spec.rb
+29
-3
spec/features/search_spec.rb
spec/features/search_spec.rb
+9
-4
spec/javascripts/filtered_search/dropdown_user_spec.js
spec/javascripts/filtered_search/dropdown_user_spec.js
+2
-6
spec/javascripts/filtered_search/dropdown_utils_spec.js
spec/javascripts/filtered_search/dropdown_utils_spec.js
+9
-16
spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js
.../filtered_search/filtered_search_dropdown_manager_spec.js
+57
-15
spec/javascripts/filtered_search/filtered_search_manager_spec.js
...vascripts/filtered_search/filtered_search_manager_spec.js
+206
-26
spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
...pts/filtered_search/filtered_search_visual_tokens_spec.js
+587
-0
spec/javascripts/helpers/filtered_search_spec_helper.js
spec/javascripts/helpers/filtered_search_spec_helper.js
+52
-0
spec/support/filtered_search_helpers.rb
spec/support/filtered_search_helpers.rb
+34
-2
No files found.
app/assets/javascripts/droplab/droplab_ajax.js
View file @
f44fb5cf
...
...
@@ -37,11 +37,14 @@ require('../window')(function(w){
}
}
if
(
!
self
.
destroyed
)
{
self
.
hook
.
list
[
config
.
method
].
call
(
self
.
hook
.
list
,
data
);
}
},
init
:
function
init
(
hook
)
{
var
self
=
this
;
self
.
destroyed
=
false
;
self
.
cache
=
self
.
cache
||
{};
var
config
=
hook
.
config
.
droplabAjax
;
this
.
hook
=
hook
;
...
...
@@ -79,6 +82,7 @@ require('../window')(function(w){
destroy
:
function
()
{
var
dynamicList
=
this
.
hook
.
list
.
list
.
querySelector
(
'
[data-dynamic]
'
);
this
.
destroyed
=
true
;
if
(
this
.
listTemplate
&&
dynamicList
)
{
dynamicList
.
outerHTML
=
this
.
listTemplate
;
}
...
...
app/assets/javascripts/filtered_search/dropdown_hint.js
View file @
f44fb5cf
...
...
@@ -28,6 +28,23 @@ require('./filtered_search_dropdown');
const
tag
=
selected
.
querySelector
(
'
.js-filter-tag
'
).
innerText
.
trim
();
if
(
tag
.
length
)
{
// Get previous input values in the input field and convert them into visual tokens
const
previousInputValues
=
this
.
input
.
value
.
split
(
'
'
);
const
searchTerms
=
[];
previousInputValues
.
forEach
((
value
,
index
)
=>
{
searchTerms
.
push
(
value
);
if
(
index
===
previousInputValues
.
length
-
1
&&
token
.
indexOf
(
value
.
toLowerCase
())
!==
-
1
)
{
searchTerms
.
pop
();
}
});
if
(
searchTerms
.
length
>
0
)
{
gl
.
FilteredSearchVisualTokens
.
addSearchVisualToken
(
searchTerms
.
join
(
'
'
));
}
gl
.
FilteredSearchDropdownManager
.
addWordToInput
(
token
.
replace
(
'
:
'
,
''
));
}
this
.
dismissDropdown
();
...
...
@@ -39,7 +56,7 @@ require('./filtered_search_dropdown');
renderContent
()
{
const
dropdownData
=
[];
[].
forEach
.
call
(
this
.
input
.
parentElement
.
querySelectorAll
(
'
.dropdown-menu
'
),
(
dropdownMenu
)
=>
{
[].
forEach
.
call
(
this
.
input
.
closest
(
'
.filtered-search-input-container
'
)
.
querySelectorAll
(
'
.dropdown-menu
'
),
(
dropdownMenu
)
=>
{
const
{
icon
,
hint
,
tag
}
=
dropdownMenu
.
dataset
;
if
(
icon
&&
hint
&&
tag
)
{
dropdownData
.
push
({
...
...
app/assets/javascripts/filtered_search/dropdown_user.js
View file @
f44fb5cf
...
...
@@ -39,7 +39,12 @@ require('./filtered_search_dropdown');
getSearchInput
()
{
const
query
=
gl
.
DropdownUtils
.
getSearchInput
(
this
.
input
);
const
{
lastToken
}
=
gl
.
FilteredSearchTokenizer
.
processTokens
(
query
);
let
value
=
lastToken
.
value
||
''
;
let
value
=
lastToken
||
''
;
if
(
value
[
0
]
===
'
@
'
)
{
value
=
value
.
slice
(
1
);
}
// Removes the first character if it is a quotation so that we can search
// with multiple words
...
...
app/assets/javascripts/filtered_search/dropdown_utils.js
View file @
f44fb5cf
...
...
@@ -22,12 +22,17 @@
static
filterWithSymbol
(
filterSymbol
,
input
,
item
)
{
const
updatedItem
=
item
;
const
query
=
gl
.
DropdownUtils
.
getSearchInput
(
input
);
const
{
lastToken
,
searchToken
}
=
gl
.
FilteredSearchTokenizer
.
processTokens
(
query
);
const
searchInput
=
gl
.
DropdownUtils
.
getSearchInput
(
input
);
if
(
lastToken
!==
searchToken
)
{
const
title
=
updatedItem
.
title
.
toLowerCase
();
let
value
=
lastToken
.
value
.
toLowerCase
();
let
value
=
searchInput
.
toLowerCase
();
let
symbol
=
''
;
// Remove the symbol for filter
if
(
value
[
0
]
===
filterSymbol
)
{
symbol
=
value
[
0
];
value
=
value
.
slice
(
1
);
}
// Removes the first character if it is a quotation so that we can search
// with multiple words
...
...
@@ -36,24 +41,21 @@
}
// Eg. filterSymbol = ~ for labels
const
matchWithoutSymbol
=
lastToken
.
symbol
===
filterSymbol
&&
title
.
indexOf
(
value
)
!==
-
1
;
const
match
=
title
.
indexOf
(
`
${
lastToken
.
symbol
}${
value
}
`
)
!==
-
1
;
const
matchWithoutSymbol
=
symbol
===
filterSymbol
&&
title
.
indexOf
(
value
)
!==
-
1
;
const
match
=
title
.
indexOf
(
`
${
symbol
}${
value
}
`
)
!==
-
1
;
updatedItem
.
droplab_hidden
=
!
match
&&
!
matchWithoutSymbol
;
}
else
{
updatedItem
.
droplab_hidden
=
false
;
}
return
updatedItem
;
}
static
filterHint
(
input
,
item
)
{
const
updatedItem
=
item
;
const
query
=
gl
.
DropdownUtils
.
getSearchInput
(
input
);
let
{
lastToken
}
=
gl
.
FilteredSearchTokenizer
.
processTokens
(
query
);
const
searchInput
=
gl
.
DropdownUtils
.
getSearchInput
(
input
);
let
{
lastToken
}
=
gl
.
FilteredSearchTokenizer
.
processTokens
(
searchInput
);
lastToken
=
lastToken
.
key
||
lastToken
||
''
;
if
(
!
lastToken
||
query
.
split
(
''
).
last
()
===
'
'
)
{
if
(
!
lastToken
||
searchInput
.
split
(
''
).
last
()
===
'
'
)
{
updatedItem
.
droplab_hidden
=
false
;
}
else
if
(
lastToken
)
{
const
split
=
lastToken
.
split
(
'
:
'
);
...
...
@@ -70,13 +72,40 @@
const
dataValue
=
selected
.
getAttribute
(
'
data-value
'
);
if
(
dataValue
)
{
gl
.
FilteredSearchDropdownManager
.
addWordToInput
(
filter
,
dataValue
);
gl
.
FilteredSearchDropdownManager
.
addWordToInput
(
filter
,
dataValue
,
true
);
}
// Return boolean based on whether it was set
return
dataValue
!==
null
;
}
static
getSearchQuery
()
{
const
tokensContainer
=
document
.
querySelector
(
'
.tokens-container
'
);
const
values
=
[];
[].
forEach
.
call
(
tokensContainer
.
querySelectorAll
(
'
.js-visual-token
'
),
(
token
)
=>
{
const
name
=
token
.
querySelector
(
'
.name
'
);
const
value
=
token
.
querySelector
(
'
.value
'
);
const
symbol
=
value
&&
value
.
dataset
.
symbol
?
value
.
dataset
.
symbol
:
''
;
let
valueText
=
''
;
if
(
value
&&
value
.
innerText
)
{
valueText
=
value
.
innerText
;
}
if
(
token
.
className
.
indexOf
(
'
filtered-search-token
'
)
!==
-
1
)
{
values
.
push
(
`
${
name
.
innerText
.
toLowerCase
()}
:
${
symbol
}${
valueText
}
`
);
}
else
{
values
.
push
(
name
.
innerText
);
}
});
const
input
=
document
.
querySelector
(
'
.filtered-search
'
);
values
.
push
(
input
&&
input
.
value
);
return
values
.
join
(
'
'
);
}
static
getSearchInput
(
filteredSearchInput
)
{
const
inputValue
=
filteredSearchInput
.
value
;
const
{
right
}
=
gl
.
DropdownUtils
.
getInputSelectionPosition
(
filteredSearchInput
);
...
...
app/assets/javascripts/filtered_search/filtered_search_bundle.js
View file @
f44fb5cf
...
...
@@ -7,3 +7,4 @@ require('./filtered_search_dropdown');
require
(
'
./filtered_search_manager
'
);
require
(
'
./filtered_search_token_keys
'
);
require
(
'
./filtered_search_tokenizer
'
);
require
(
'
./filtered_search_visual_tokens
'
);
app/assets/javascripts/filtered_search/filtered_search_dropdown.js
View file @
f44fb5cf
...
...
@@ -35,7 +35,7 @@
if
(
!
dataValueSet
)
{
const
value
=
getValueFunction
(
selected
);
gl
.
FilteredSearchDropdownManager
.
addWordToInput
(
this
.
filter
,
value
);
gl
.
FilteredSearchDropdownManager
.
addWordToInput
(
this
.
filter
,
value
,
true
);
}
this
.
dismissDropdown
();
...
...
app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
View file @
f44fb5cf
...
...
@@ -58,35 +58,15 @@
};
}
static
addWordToInput
(
tokenName
,
tokenValue
=
''
)
{
static
addWordToInput
(
tokenName
,
tokenValue
=
''
,
clicked
=
false
)
{
const
input
=
document
.
querySelector
(
'
.filtered-search
'
);
const
inputValue
=
input
.
value
;
const
word
=
`
${
tokenName
}
:
${
tokenValue
}
`
;
// Get the string to replace
let
newCaretPosition
=
input
.
selectionStart
;
const
{
left
,
right
}
=
gl
.
DropdownUtils
.
getInputSelectionPosition
(
input
);
gl
.
FilteredSearchVisualTokens
.
addFilterVisualToken
(
tokenName
,
tokenValue
);
input
.
value
=
''
;
input
.
value
=
`
${
inputValue
.
substr
(
0
,
left
)}${
word
}${
inputValue
.
substr
(
right
)}
`
;
// If we have added a tokenValue at the end of the input,
// add a space and set selection to the end
if
(
right
>=
inputValue
.
length
&&
tokenValue
!==
''
)
{
input
.
value
+=
'
'
;
newCaretPosition
=
input
.
value
.
length
;
}
gl
.
FilteredSearchDropdownManager
.
updateInputCaretPosition
(
newCaretPosition
,
input
);
if
(
clicked
)
{
gl
.
FilteredSearchVisualTokens
.
moveInputToTheRight
();
}
static
updateInputCaretPosition
(
selectionStart
,
input
)
{
// Reset the position
// Sometimes can end up at end of input
input
.
setSelectionRange
(
selectionStart
,
selectionStart
);
const
{
right
}
=
gl
.
DropdownUtils
.
getInputSelectionPosition
(
input
);
input
.
setSelectionRange
(
right
,
right
);
}
updateCurrentDropdownOffset
()
{
...
...
@@ -94,19 +74,14 @@
}
updateDropdownOffset
(
key
)
{
if
(
!
this
.
font
)
{
this
.
font
=
window
.
getComputedStyle
(
this
.
filteredSearchInput
).
font
;
}
const
input
=
this
.
filteredSearchInput
;
const
inputText
=
input
.
value
.
slice
(
0
,
input
.
selectionStart
);
const
filterIconPadding
=
27
;
let
offset
=
gl
.
text
.
getTextWidth
(
inputText
,
this
.
font
)
+
filterIconPadding
;
// Always align dropdown with the input field
let
offset
=
this
.
filteredSearchInput
.
getBoundingClientRect
().
left
-
document
.
querySelector
(
'
.scroll-container
'
).
getBoundingClientRect
().
left
;
const
currentDropdownWidth
=
this
.
mapping
[
key
].
element
.
clientWidth
===
0
?
200
:
this
.
mapping
[
key
].
element
.
clientWidth
;
const
offsetMaxWidth
=
this
.
filteredSearchInput
.
clientWidth
-
currentDropdownWidth
;
const
maxInputWidth
=
240
;
const
currentDropdownWidth
=
this
.
mapping
[
key
].
element
.
clientWidth
||
maxInputWidth
;
// Make sure offset never exceeds the input container
const
offsetMaxWidth
=
document
.
querySelector
(
'
.scroll-container
'
).
clientWidth
-
currentDropdownWidth
;
if
(
offsetMaxWidth
<
offset
)
{
offset
=
offsetMaxWidth
;
}
...
...
@@ -164,8 +139,8 @@
}
setDropdown
()
{
const
{
lastToken
,
searchToken
}
=
this
.
tokenizer
.
processTokens
(
gl
.
DropdownUtils
.
getSearchInput
(
this
.
filteredSearchInput
)
);
const
query
=
gl
.
DropdownUtils
.
getSearchQuery
();
const
{
lastToken
,
searchToken
}
=
this
.
tokenizer
.
processTokens
(
query
);
if
(
this
.
currentDropdown
)
{
this
.
updateCurrentDropdownOffset
();
...
...
app/assets/javascripts/filtered_search/filtered_search_manager.js
View file @
f44fb5cf
This diff is collapsed.
Click to expand it.
app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
0 → 100644
View file @
f44fb5cf
class
FilteredSearchVisualTokens
{
static
getLastVisualTokenBeforeInput
()
{
const
inputLi
=
document
.
querySelector
(
'
.input-token
'
);
const
lastVisualToken
=
inputLi
&&
inputLi
.
previousElementSibling
;
return
{
lastVisualToken
,
isLastVisualTokenValid
:
lastVisualToken
===
null
||
lastVisualToken
.
className
.
indexOf
(
'
filtered-search-term
'
)
!==
-
1
||
(
lastVisualToken
&&
lastVisualToken
.
querySelector
(
'
.value
'
)
!==
null
),
};
}
static
unselectTokens
()
{
const
otherTokens
=
document
.
querySelectorAll
(
'
.js-visual-token .selectable.selected
'
);
[].
forEach
.
call
(
otherTokens
,
t
=>
t
.
classList
.
remove
(
'
selected
'
));
}
static
selectToken
(
tokenButton
)
{
const
selected
=
tokenButton
.
classList
.
contains
(
'
selected
'
);
FilteredSearchVisualTokens
.
unselectTokens
();
if
(
!
selected
)
{
tokenButton
.
classList
.
add
(
'
selected
'
);
}
}
static
removeSelectedToken
()
{
const
selected
=
document
.
querySelector
(
'
.js-visual-token .selected
'
);
if
(
selected
)
{
const
li
=
selected
.
closest
(
'
.js-visual-token
'
);
li
.
parentElement
.
removeChild
(
li
);
}
}
static
createVisualTokenElementHTML
()
{
return
`
<div class="selectable" role="button">
<div class="name"></div>
<div class="value"></div>
</div>
`
;
}
static
addVisualTokenElement
(
name
,
value
,
isSearchTerm
)
{
const
li
=
document
.
createElement
(
'
li
'
);
li
.
classList
.
add
(
'
js-visual-token
'
);
li
.
classList
.
add
(
isSearchTerm
?
'
filtered-search-term
'
:
'
filtered-search-token
'
);
if
(
value
)
{
li
.
innerHTML
=
FilteredSearchVisualTokens
.
createVisualTokenElementHTML
();
li
.
querySelector
(
'
.value
'
).
innerText
=
value
;
}
else
{
li
.
innerHTML
=
'
<div class="name"></div>
'
;
}
li
.
querySelector
(
'
.name
'
).
innerText
=
name
;
const
tokensContainer
=
document
.
querySelector
(
'
.tokens-container
'
);
const
input
=
document
.
querySelector
(
'
.filtered-search
'
);
tokensContainer
.
insertBefore
(
li
,
input
.
parentElement
);
}
static
addValueToPreviousVisualTokenElement
(
value
)
{
const
{
lastVisualToken
,
isLastVisualTokenValid
}
=
FilteredSearchVisualTokens
.
getLastVisualTokenBeforeInput
();
if
(
!
isLastVisualTokenValid
&&
lastVisualToken
.
classList
.
contains
(
'
filtered-search-token
'
))
{
const
name
=
FilteredSearchVisualTokens
.
getLastTokenPartial
();
lastVisualToken
.
innerHTML
=
FilteredSearchVisualTokens
.
createVisualTokenElementHTML
();
lastVisualToken
.
querySelector
(
'
.name
'
).
innerText
=
name
;
lastVisualToken
.
querySelector
(
'
.value
'
).
innerText
=
value
;
}
}
static
addFilterVisualToken
(
tokenName
,
tokenValue
)
{
const
{
lastVisualToken
,
isLastVisualTokenValid
}
=
FilteredSearchVisualTokens
.
getLastVisualTokenBeforeInput
();
const
addVisualTokenElement
=
FilteredSearchVisualTokens
.
addVisualTokenElement
;
if
(
isLastVisualTokenValid
)
{
addVisualTokenElement
(
tokenName
,
tokenValue
);
}
else
{
const
previousTokenName
=
lastVisualToken
.
querySelector
(
'
.name
'
).
innerText
;
const
tokensContainer
=
document
.
querySelector
(
'
.tokens-container
'
);
tokensContainer
.
removeChild
(
lastVisualToken
);
const
value
=
tokenValue
||
tokenName
;
addVisualTokenElement
(
previousTokenName
,
value
);
}
}
static
addSearchVisualToken
(
searchTerm
)
{
const
{
lastVisualToken
}
=
FilteredSearchVisualTokens
.
getLastVisualTokenBeforeInput
();
if
(
lastVisualToken
&&
lastVisualToken
.
classList
.
contains
(
'
filtered-search-term
'
))
{
lastVisualToken
.
querySelector
(
'
.name
'
).
innerText
+=
`
${
searchTerm
}
`
;
}
else
{
FilteredSearchVisualTokens
.
addVisualTokenElement
(
searchTerm
,
null
,
true
);
}
}
static
getLastTokenPartial
()
{
const
{
lastVisualToken
}
=
FilteredSearchVisualTokens
.
getLastVisualTokenBeforeInput
();
if
(
!
lastVisualToken
)
return
''
;
const
value
=
lastVisualToken
.
querySelector
(
'
.value
'
);
const
name
=
lastVisualToken
.
querySelector
(
'
.name
'
);
const
valueText
=
value
?
value
.
innerText
:
''
;
const
nameText
=
name
?
name
.
innerText
:
''
;
return
valueText
||
nameText
;
}
static
removeLastTokenPartial
()
{
const
{
lastVisualToken
}
=
FilteredSearchVisualTokens
.
getLastVisualTokenBeforeInput
();
if
(
lastVisualToken
)
{
const
value
=
lastVisualToken
.
querySelector
(
'
.value
'
);
if
(
value
)
{
const
button
=
lastVisualToken
.
querySelector
(
'
.selectable
'
);
button
.
removeChild
(
value
);
lastVisualToken
.
innerHTML
=
button
.
innerHTML
;
}
else
{
lastVisualToken
.
closest
(
'
.tokens-container
'
).
removeChild
(
lastVisualToken
);
}
}
}
static
tokenizeInput
()
{
const
input
=
document
.
querySelector
(
'
.filtered-search
'
);
const
{
isLastVisualTokenValid
}
=
gl
.
FilteredSearchVisualTokens
.
getLastVisualTokenBeforeInput
();
if
(
input
.
value
)
{
if
(
isLastVisualTokenValid
)
{
gl
.
FilteredSearchVisualTokens
.
addSearchVisualToken
(
input
.
value
);
}
else
{
FilteredSearchVisualTokens
.
addValueToPreviousVisualTokenElement
(
input
.
value
);
}
input
.
value
=
''
;
}
}
static
editToken
(
token
)
{
const
input
=
document
.
querySelector
(
'
.filtered-search
'
);
FilteredSearchVisualTokens
.
tokenizeInput
();
// Replace token with input field
const
tokenContainer
=
token
.
parentElement
;
const
inputLi
=
input
.
parentElement
;
tokenContainer
.
replaceChild
(
inputLi
,
token
);
const
name
=
token
.
querySelector
(
'
.name
'
);
const
value
=
token
.
querySelector
(
'
.value
'
);
if
(
token
.
classList
.
contains
(
'
filtered-search-token
'
))
{
FilteredSearchVisualTokens
.
addFilterVisualToken
(
name
.
innerText
);
input
.
value
=
value
.
innerText
;
}
else
{
// token is a search term
input
.
value
=
name
.
innerText
;
}
// Opens dropdown
const
inputEvent
=
new
Event
(
'
input
'
);
input
.
dispatchEvent
(
inputEvent
);
// Adds cursor to input
input
.
focus
();
}
static
moveInputToTheRight
()
{
const
input
=
document
.
querySelector
(
'
.filtered-search
'
);
const
inputLi
=
input
.
parentElement
;
const
tokenContainer
=
document
.
querySelector
(
'
.tokens-container
'
);
if
(
!
tokenContainer
.
lastElementChild
.
isEqualNode
(
inputLi
))
{
FilteredSearchVisualTokens
.
tokenizeInput
();
const
{
isLastVisualTokenValid
}
=
gl
.
FilteredSearchVisualTokens
.
getLastVisualTokenBeforeInput
();
if
(
!
isLastVisualTokenValid
)
{
const
lastPartial
=
gl
.
FilteredSearchVisualTokens
.
getLastTokenPartial
();
gl
.
FilteredSearchVisualTokens
.
removeLastTokenPartial
();
gl
.
FilteredSearchVisualTokens
.
addSearchVisualToken
(
lastPartial
);
}
tokenContainer
.
removeChild
(
inputLi
);
tokenContainer
.
appendChild
(
inputLi
);
}
}
}
window
.
gl
=
window
.
gl
||
{};
gl
.
FilteredSearchVisualTokens
=
FilteredSearchVisualTokens
;
app/assets/stylesheets/framework/filters.scss
View file @
f44fb5cf
...
...
@@ -64,6 +64,89 @@
-webkit-flex-direction
:
column
;
flex-direction
:
column
;
}
.tokens-container
{
display
:
-
webkit-flex
;
display
:
flex
;
flex
:
1
;
-webkit-flex
:
1
;
padding-left
:
30px
;
position
:
relative
;
margin-bottom
:
0
;
}
.input-token
{
flex
:
1
;
-webkit-flex
:
1
;
}
.filtered-search-token
+
.input-token
:not
(
:last-child
)
{
max-width
:
200px
;
}
}
.filtered-search-token
,
.filtered-search-term
{
display
:
-
webkit-flex
;
display
:
flex
;
margin-top
:
5px
;
margin-bottom
:
5px
;
.selectable
{
display
:
-
webkit-flex
;
display
:
flex
;
}
.name
,
.value
{
display
:
inline-block
;
padding
:
2px
7px
;
}
.name
{
background-color
:
$filter-name-resting-color
;
color
:
$filter-name-text-color
;
border-radius
:
2px
0
0
2px
;
margin-right
:
1px
;
text-transform
:
capitalize
;
}
.value
{
background-color
:
$white-normal
;
color
:
$filter-value-text-color
;
border-radius
:
0
2px
2px
0
;
margin-right
:
5px
;
}
.selected
{
.name
{
background-color
:
$filter-name-selected-color
;
}
.value
{
background-color
:
$filter-value-selected-color
;
}
}
}
.filtered-search-term
{
.name
{
background-color
:
inherit
;
color
:
$black
;
text-transform
:
none
;
}
.selectable
{
cursor
:
text
;
}
}
.scroll-container
{
display
:
-
webkit-flex
;
display
:
flex
;
overflow-x
:
scroll
;
white-space
:
nowrap
;
width
:
100%
;
}
.filtered-search-input-container
{
...
...
@@ -71,6 +154,9 @@
display
:
flex
;
position
:
relative
;
width
:
100%
;
border
:
1px
solid
$border-color
;
background-color
:
$white-light
;
max-width
:
87%
;
@media
(
max-width
:
$screen-xs-min
)
{
-webkit-flex
:
1
1
100%
;
...
...
@@ -87,12 +173,22 @@
}
.form-control
{
padding-left
:
25px
;
position
:
relative
;
min-width
:
200px
;
padding-left
:
0
;
padding-right
:
25px
;
border-color
:
transparent
;
&
:focus
~
.fa-filter
{
color
:
$common-gray-dark
;
}
&
:focus
,
&
:hover
{
outline
:
none
;
border-color
:
transparent
;
box-shadow
:
none
;
}
}
.fa-filter
{
...
...
@@ -109,12 +205,13 @@
.clear-search
{
width
:
35px
;
background-color
:
transparen
t
;
background-color
:
$white-ligh
t
;
border
:
none
;
position
:
absolute
;
right
:
0
;
height
:
100%
;
outline
:
none
;
z-index
:
1
;
&
:hover
.fa-times
{
color
:
$common-gray-dark
;
...
...
app/assets/stylesheets/framework/variables.scss
View file @
f44fb5cf
...
...
@@ -540,3 +540,12 @@ Pipeline Graph
$stage-hover-bg
:
#eaf3fc
;
$stage-hover-border
:
#d1e7fc
;
$action-icon-color
:
#d6d6d6
;
/*
Filtered Search
*/
$filter-name-resting-color
:
#f8f8f8
;
$filter-name-text-color
:
rgba
(
0
,
0
,
0
,
0
.55
);
$filter-value-text-color
:
rgba
(
0
,
0
,
0
,
0
.85
);
$filter-name-selected-color
:
#ebebeb
;
$filter-value-selected-color
:
#d7d7d7
;
app/views/shared/issuable/_search_bar.html.haml
View file @
f44fb5cf
...
...
@@ -11,6 +11,9 @@
class:
"check_all_issues left"
.issues-other-filters.filtered-search-container
.filtered-search-input-container
.scroll-container
%ul
.tokens-container.list-unstyled
%li
.input-token
%input
.form-control.filtered-search
{
placeholder:
'Search or filter results...'
,
'data-id'
=>
'filtered-search'
,
'data-project-id'
=>
@project
.
id
,
'data-username-params'
=>
@users
.
to_json
(
only:
[
:id
,
:username
]),
'data-base-endpoint'
=>
namespace_project_path
(
@project
.
namespace
,
@project
)
}
=
icon
(
'filter'
)
%button
.clear-search.hidden
{
type:
'button'
}
...
...
spec/features/issues/filtered_search/dropdown_assignee_spec.rb
View file @
f44fb5cf
require
'rails_helper'
describe
'Dropdown assignee'
,
:feature
,
:js
do
include
FilteredSearchHelpers
include
WaitForAjax
let!
(
:project
)
{
create
(
:empty_project
)
}
let!
(
:user
)
{
create
(
:user
,
name:
'administrator'
,
username:
'root'
)
}
let!
(
:user_john
)
{
create
(
:user
,
name:
'John'
,
username:
'th0mas'
)
}
...
...
@@ -133,7 +136,8 @@ describe 'Dropdown assignee', :feature, :js do
click_assignee
(
user_jacob
.
name
)
expect
(
page
).
to
have_css
(
js_dropdown_assignee
,
visible:
false
)
expect
(
filtered_search
.
value
).
to
eq
(
"assignee:@
#{
user_jacob
.
username
}
"
)
expect_tokens
([{
name:
'assignee'
,
value:
"@
#{
user_jacob
.
username
}
"
}])
expect_filtered_search_input_empty
end
it
'fills in the assignee username when the assignee has been filtered'
do
...
...
@@ -141,14 +145,16 @@ describe 'Dropdown assignee', :feature, :js do
click_assignee
(
user
.
name
)
expect
(
page
).
to
have_css
(
js_dropdown_assignee
,
visible:
false
)
expect
(
filtered_search
.
value
).
to
eq
(
"assignee:@
#{
user
.
username
}
"
)
expect_tokens
([{
name:
'assignee'
,
value:
"@
#{
user
.
username
}
"
}])
expect_filtered_search_input_empty
end
it
'selects `no assignee`'
do
find
(
'#js-dropdown-assignee .filter-dropdown-item'
,
text:
'No Assignee'
).
click
expect
(
page
).
to
have_css
(
js_dropdown_assignee
,
visible:
false
)
expect
(
filtered_search
.
value
).
to
eq
(
"assignee:none "
)
expect_tokens
([{
name:
'assignee'
,
value:
'none'
}])
expect_filtered_search_input_empty
end
end
...
...
spec/features/issues/filtered_search/dropdown_author_spec.rb
View file @
f44fb5cf
require
'rails_helper'
describe
'Dropdown author'
,
js:
true
,
feature:
true
do
include
FilteredSearchHelpers
include
WaitForAjax
let!
(
:project
)
{
create
(
:empty_project
)
}
...
...
@@ -121,14 +122,16 @@ describe 'Dropdown author', js: true, feature: true do
click_author
(
user_jacob
.
name
)
expect
(
page
).
to
have_css
(
js_dropdown_author
,
visible:
false
)
expect
(
filtered_search
.
value
).
to
eq
(
"author:@
#{
user_jacob
.
username
}
"
)
expect_tokens
([{
name:
'author'
,
value:
"@
#{
user_jacob
.
username
}
"
}])
expect_filtered_search_input_empty
end
it
'fills in the author username when the author has been filtered'
do
click_author
(
user
.
name
)
expect
(
page
).
to
have_css
(
js_dropdown_author
,
visible:
false
)
expect
(
filtered_search
.
value
).
to
eq
(
"author:@
#{
user
.
username
}
"
)
expect_tokens
([{
name:
'author'
,
value:
"@
#{
user
.
username
}
"
}])
expect_filtered_search_input_empty
end
end
...
...
spec/features/issues/filtered_search/dropdown_hint_spec.rb
View file @
f44fb5cf
require
'rails_helper'
describe
'Dropdown hint'
,
js:
true
,
feature:
true
do
include
FilteredSearchHelpers
include
WaitForAjax
let!
(
:project
)
{
create
(
:empty_project
)
}
...
...
@@ -66,7 +67,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect
(
page
).
to
have_css
(
js_dropdown_hint
,
visible:
false
)
expect
(
page
).
to
have_css
(
'#js-dropdown-author'
,
visible:
true
)
expect
(
filtered_search
.
value
).
to
eq
(
'author:'
)
expect_tokens
([{
name:
'author'
}])
expect_filtered_search_input_empty
end
it
'opens the assignee dropdown when you click on assignee'
do
...
...
@@ -74,7 +76,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect
(
page
).
to
have_css
(
js_dropdown_hint
,
visible:
false
)
expect
(
page
).
to
have_css
(
'#js-dropdown-assignee'
,
visible:
true
)
expect
(
filtered_search
.
value
).
to
eq
(
'assignee:'
)
expect_tokens
([{
name:
'assignee'
}])
expect_filtered_search_input_empty
end
it
'opens the milestone dropdown when you click on milestone'
do
...
...
@@ -82,7 +85,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect
(
page
).
to
have_css
(
js_dropdown_hint
,
visible:
false
)
expect
(
page
).
to
have_css
(
'#js-dropdown-milestone'
,
visible:
true
)
expect
(
filtered_search
.
value
).
to
eq
(
'milestone:'
)
expect_tokens
([{
name:
'milestone'
}])
expect_filtered_search_input_empty
end
it
'opens the label dropdown when you click on label'
do
...
...
@@ -90,7 +94,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect
(
page
).
to
have_css
(
js_dropdown_hint
,
visible:
false
)
expect
(
page
).
to
have_css
(
'#js-dropdown-label'
,
visible:
true
)
expect
(
filtered_search
.
value
).
to
eq
(
'label:'
)
expect_tokens
([{
name:
'label'
}])
expect_filtered_search_input_empty
end
end
...
...
@@ -101,7 +106,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect
(
page
).
to
have_css
(
js_dropdown_hint
,
visible:
false
)
expect
(
page
).
to
have_css
(
'#js-dropdown-author'
,
visible:
true
)
expect
(
filtered_search
.
value
).
to
eq
(
'author:'
)
expect_tokens
([{
name:
'author'
}])
expect_filtered_search_input_empty
end
it
'opens the assignee dropdown when you click on assignee'
do
...
...
@@ -110,7 +116,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect
(
page
).
to
have_css
(
js_dropdown_hint
,
visible:
false
)
expect
(
page
).
to
have_css
(
'#js-dropdown-assignee'
,
visible:
true
)
expect
(
filtered_search
.
value
).
to
eq
(
'assignee:'
)
expect_tokens
([{
name:
'assignee'
}])
expect_filtered_search_input_empty
end
it
'opens the milestone dropdown when you click on milestone'
do
...
...
@@ -119,7 +126,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect
(
page
).
to
have_css
(
js_dropdown_hint
,
visible:
false
)
expect
(
page
).
to
have_css
(
'#js-dropdown-milestone'
,
visible:
true
)
expect
(
filtered_search
.
value
).
to
eq
(
'milestone:'
)
expect_tokens
([{
name:
'milestone'
}])
expect_filtered_search_input_empty
end
it
'opens the label dropdown when you click on label'
do
...
...
@@ -128,7 +136,46 @@ describe 'Dropdown hint', js: true, feature: true do
expect
(
page
).
to
have_css
(
js_dropdown_hint
,
visible:
false
)
expect
(
page
).
to
have_css
(
'#js-dropdown-label'
,
visible:
true
)
expect
(
filtered_search
.
value
).
to
eq
(
'label:'
)
expect_tokens
([{
name:
'label'
}])
expect_filtered_search_input_empty
end
end
describe
'reselecting from dropdown'
do
it
'reuses existing author text'
do
filtered_search
.
send_keys
(
'author:'
)
filtered_search
.
send_keys
(
:backspace
)
click_hint
(
'author'
)
expect_tokens
([{
name:
'author'
}])
expect_filtered_search_input_empty
end
it
'reuses existing assignee text'
do
filtered_search
.
send_keys
(
'assignee:'
)
filtered_search
.
send_keys
(
:backspace
)
click_hint
(
'assignee'
)
expect_tokens
([{
name:
'assignee'
}])
expect_filtered_search_input_empty
end
it
'reuses existing milestone text'
do
filtered_search
.
send_keys
(
'milestone:'
)
filtered_search
.
send_keys
(
:backspace
)
click_hint
(
'milestone'
)
expect_tokens
([{
name:
'milestone'
}])
expect_filtered_search_input_empty
end
it
'reuses existing label text'
do
filtered_search
.
send_keys
(
'label:'
)
filtered_search
.
send_keys
(
:backspace
)
click_hint
(
'label'
)
expect_tokens
([{
name:
'label'
}])
expect_filtered_search_input_empty
end
end
end
spec/features/issues/filtered_search/dropdown_label_spec.rb
View file @
f44fb5cf
...
...
@@ -51,7 +51,8 @@ describe 'Dropdown label', js: true, feature: true do
filtered_search
.
native
.
send_keys
(
:down
,
:down
,
:enter
)
expect
(
filtered_search
.
value
).
to
eq
(
"label:~
#{
bug_label
.
title
}
"
)
expect_tokens
([{
name:
'label'
,
value:
"~
#{
bug_label
.
title
}
"
}])
expect_filtered_search_input_empty
end
end
...
...
@@ -92,7 +93,7 @@ describe 'Dropdown label', js: true, feature: true do
end
it
'filters by case-insensitive name with or without symbol'
do
search_for_label
(
'b'
)
filtered_search
.
send_keys
(
'b'
)
expect
(
filter_dropdown
.
find
(
'.filter-dropdown-item'
,
text:
bug_label
.
title
)).
to
be_visible
expect
(
filter_dropdown
.
find
(
'.filter-dropdown-item'
,
text:
uppercase_label
.
title
)).
to
be_visible
...
...
@@ -101,7 +102,7 @@ describe 'Dropdown label', js: true, feature: true do
clear_search_field
init_label_search
search_for_label
(
'~bu'
)
filtered_search
.
send_keys
(
'~bu'
)
expect
(
filter_dropdown
.
find
(
'.filter-dropdown-item'
,
text:
bug_label
.
title
)).
to
be_visible
expect
(
filter_dropdown
.
find
(
'.filter-dropdown-item'
,
text:
uppercase_label
.
title
)).
to
be_visible
...
...
@@ -180,7 +181,8 @@ describe 'Dropdown label', js: true, feature: true do
click_label
(
bug_label
.
title
)
expect
(
page
).
not_to
have_css
(
js_dropdown_label
)
expect
(
filtered_search
.
value
).
to
eq
(
"label:~
#{
bug_label
.
title
}
"
)
expect_tokens
([{
name:
'label'
,
value:
"~
#{
bug_label
.
title
}
"
}])
expect_filtered_search_input_empty
end
it
'fills in the label name when the label is partially filled'
do
...
...
@@ -188,49 +190,56 @@ describe 'Dropdown label', js: true, feature: true do
click_label
(
bug_label
.
title
)
expect
(
page
).
not_to
have_css
(
js_dropdown_label
)
expect
(
filtered_search
.
value
).
to
eq
(
"label:~
#{
bug_label
.
title
}
"
)
expect_tokens
([{
name:
'label'
,
value:
"~
#{
bug_label
.
title
}
"
}])
expect_filtered_search_input_empty
end
it
'fills in the label name that contains multiple words'
do
click_label
(
two_words_label
.
title
)
expect
(
page
).
not_to
have_css
(
js_dropdown_label
)
expect
(
filtered_search
.
value
).
to
eq
(
"label:~
\"
#{
two_words_label
.
title
}
\"
"
)
expect_tokens
([{
name:
'label'
,
value:
"
\"
#{
two_words_label
.
title
}
\"
"
}])
expect_filtered_search_input_empty
end
it
'fills in the label name that contains multiple words and is very long'
do
click_label
(
long_label
.
title
)
expect
(
page
).
not_to
have_css
(
js_dropdown_label
)
expect
(
filtered_search
.
value
).
to
eq
(
"label:~
\"
#{
long_label
.
title
}
\"
"
)
expect_tokens
([{
name:
'label'
,
value:
"
\"
#{
long_label
.
title
}
\"
"
}])
expect_filtered_search_input_empty
end
it
'fills in the label name that contains double quotes'
do
click_label
(
wont_fix_label
.
title
)
expect
(
page
).
not_to
have_css
(
js_dropdown_label
)
expect
(
filtered_search
.
value
).
to
eq
(
"label:~'
#{
wont_fix_label
.
title
}
' "
)
expect_tokens
([{
name:
'label'
,
value:
"~'
#{
wont_fix_label
.
title
}
'"
}])
expect_filtered_search_input_empty
end
it
'fills in the label name with the correct capitalization'
do
click_label
(
uppercase_label
.
title
)
expect
(
page
).
not_to
have_css
(
js_dropdown_label
)
expect
(
filtered_search
.
value
).
to
eq
(
"label:~
#{
uppercase_label
.
title
}
"
)
expect_tokens
([{
name:
'label'
,
value:
"~
#{
uppercase_label
.
title
}
"
}])
expect_filtered_search_input_empty
end
it
'fills in the label name with special characters'
do
click_label
(
special_label
.
title
)
expect
(
page
).
not_to
have_css
(
js_dropdown_label
)
expect
(
filtered_search
.
value
).
to
eq
(
"label:~
#{
special_label
.
title
}
"
)
expect_tokens
([{
name:
'label'
,
value:
"~
#{
special_label
.
title
}
"
}])
expect_filtered_search_input_empty
end
it
'selects `no label`'
do
find
(
"
#{
js_dropdown_label
}
.filter-dropdown-item"
,
text:
'No Label'
).
click
expect
(
page
).
not_to
have_css
(
js_dropdown_label
)
expect
(
filtered_search
.
value
).
to
eq
(
"label:none "
)
expect_tokens
([{
name:
'label'
,
value:
'none'
}])
expect_filtered_search_input_empty
end
end
...
...
spec/features/issues/filtered_search/dropdown_milestone_spec.rb
View file @
f44fb5cf
require
'rails_helper'
describe
'Dropdown milestone'
,
js:
true
,
feature:
true
do
include
FilteredSearchHelpers
include
WaitForAjax
let!
(
:project
)
{
create
(
:empty_project
)
}
...
...
@@ -127,7 +128,8 @@ describe 'Dropdown milestone', js: true, feature: true do
click_milestone
(
milestone
.
title
)
expect
(
page
).
to
have_css
(
js_dropdown_milestone
,
visible:
false
)
expect
(
filtered_search
.
value
).
to
eq
(
"milestone:%
#{
milestone
.
title
}
"
)
expect_tokens
([{
name:
'milestone'
,
value:
"%
#{
milestone
.
title
}
"
}])
expect_filtered_search_input_empty
end
it
'fills in the milestone name when the milestone is partially filled'
do
...
...
@@ -135,56 +137,64 @@ describe 'Dropdown milestone', js: true, feature: true do
click_milestone
(
milestone
.
title
)
expect
(
page
).
to
have_css
(
js_dropdown_milestone
,
visible:
false
)
expect
(
filtered_search
.
value
).
to
eq
(
"milestone:%
#{
milestone
.
title
}
"
)
expect_tokens
([{
name:
'milestone'
,
value:
"%
#{
milestone
.
title
}
"
}])
expect_filtered_search_input_empty
end
it
'fills in the milestone name that contains multiple words'
do
click_milestone
(
two_words_milestone
.
title
)
expect
(
page
).
to
have_css
(
js_dropdown_milestone
,
visible:
false
)
expect
(
filtered_search
.
value
).
to
eq
(
"milestone:%
\"
#{
two_words_milestone
.
title
}
\"
"
)
expect_tokens
([{
name:
'milestone'
,
value:
"%
\"
#{
two_words_milestone
.
title
}
\"
"
}])
expect_filtered_search_input_empty
end
it
'fills in the milestone name that contains multiple words and is very long'
do
click_milestone
(
long_milestone
.
title
)
expect
(
page
).
to
have_css
(
js_dropdown_milestone
,
visible:
false
)
expect
(
filtered_search
.
value
).
to
eq
(
"milestone:%
\"
#{
long_milestone
.
title
}
\"
"
)
expect_tokens
([{
name:
'milestone'
,
value:
"%
\"
#{
long_milestone
.
title
}
\"
"
}])
expect_filtered_search_input_empty
end
it
'fills in the milestone name that contains double quotes'
do
click_milestone
(
wont_fix_milestone
.
title
)
expect
(
page
).
to
have_css
(
js_dropdown_milestone
,
visible:
false
)
expect
(
filtered_search
.
value
).
to
eq
(
"milestone:%'
#{
wont_fix_milestone
.
title
}
' "
)
expect_tokens
([{
name:
'milestone'
,
value:
"%'
#{
wont_fix_milestone
.
title
}
'"
}])
expect_filtered_search_input_empty
end
it
'fills in the milestone name with the correct capitalization'
do
click_milestone
(
uppercase_milestone
.
title
)
expect
(
page
).
to
have_css
(
js_dropdown_milestone
,
visible:
false
)
expect
(
filtered_search
.
value
).
to
eq
(
"milestone:%
#{
uppercase_milestone
.
title
}
"
)
expect_tokens
([{
name:
'milestone'
,
value:
"%
#{
uppercase_milestone
.
title
}
"
}])
expect_filtered_search_input_empty
end
it
'fills in the milestone name with special characters'
do
click_milestone
(
special_milestone
.
title
)
expect
(
page
).
to
have_css
(
js_dropdown_milestone
,
visible:
false
)
expect
(
filtered_search
.
value
).
to
eq
(
"milestone:%
#{
special_milestone
.
title
}
"
)
expect_tokens
([{
name:
'milestone'
,
value:
"%
#{
special_milestone
.
title
}
"
}])
expect_filtered_search_input_empty
end
it
'selects `no milestone`'
do
click_static_milestone
(
'No Milestone'
)
expect
(
page
).
to
have_css
(
js_dropdown_milestone
,
visible:
false
)
expect
(
filtered_search
.
value
).
to
eq
(
"milestone:none "
)
expect_tokens
([{
name:
'milestone'
,
value:
'none'
}])
expect_filtered_search_input_empty
end
it
'selects `upcoming milestone`'
do
click_static_milestone
(
'Upcoming'
)
expect
(
page
).
to
have_css
(
js_dropdown_milestone
,
visible:
false
)
expect
(
filtered_search
.
value
).
to
eq
(
"milestone:upcoming "
)
expect_tokens
([{
name:
'milestone'
,
value:
'upcoming'
}])
expect_filtered_search_input_empty
end
end
...
...
spec/features/issues/filtered_search/filter_issues_spec.rb
View file @
f44fb5cf
This diff is collapsed.
Click to expand it.
spec/features/issues/filtered_search/search_bar_spec.rb
View file @
f44fb5cf
require
'rails_helper'
describe
'Search bar'
,
js:
true
,
feature:
true
do
include
FilteredSearchHelpers
include
WaitForAjax
let!
(
:project
)
{
create
(
:empty_project
)
}
...
...
@@ -32,7 +33,8 @@ describe 'Search bar', js: true, feature: true do
it
'selects item'
do
filtered_search
.
native
.
send_keys
(
:down
,
:down
,
:enter
)
expect
(
filtered_search
.
value
).
to
eq
(
'author:'
)
expect_tokens
([{
name:
'author'
}])
expect_filtered_search_input_empty
end
end
...
...
spec/features/issues/filtered_search/visual_tokens_spec.rb
0 → 100644
View file @
f44fb5cf
This diff is collapsed.
Click to expand it.
spec/features/merge_requests/filter_by_labels_spec.rb
View file @
f44fb5cf
...
...
@@ -70,7 +70,7 @@ feature 'Issue filtering by Labels', feature: true, js: true do
context
'filter by label enhancement and bug in issues list'
do
before
do
input_filtered_search
(
'label:~bug label:~enhancement'
)
input_filtered_search
(
'label:~bug label:~enhancement
'
)
end
it
'applies the filters'
do
...
...
spec/features/merge_requests/filter_by_milestone_spec.rb
View file @
f44fb5cf
...
...
@@ -25,6 +25,9 @@ feature 'Merge Request filtering by Milestone', feature: true do
visit_merge_requests
(
project
)
input_filtered_search
(
'milestone:none'
)
expect_tokens
([{
name:
'milestone'
,
value:
'none'
}])
expect_filtered_search_input_empty
expect
(
page
).
to
have_issuable_counts
(
open:
1
,
closed:
0
,
all:
1
)
expect
(
page
).
to
have_css
(
'.merge-request'
,
count:
1
)
end
...
...
spec/features/merge_requests/filter_merge_requests_spec.rb
View file @
f44fb5cf
...
...
@@ -24,6 +24,11 @@ describe 'Filter merge requests', feature: true do
describe
'for assignee from mr#index'
do
let
(
:search_query
)
{
"assignee:@
#{
user
.
username
}
"
}
def
expect_assignee_visual_tokens
expect_tokens
([{
name:
'assignee'
,
value:
"@
#{
user
.
username
}
"
}])
expect_filtered_search_input_empty
end
before
do
input_filtered_search
(
search_query
)
...
...
@@ -32,25 +37,30 @@ describe 'Filter merge requests', feature: true do
context
'assignee'
,
js:
true
do
it
'updates to current user'
do
expect_
filtered_search_input
(
search_query
)
expect_
assignee_visual_tokens
(
)
end
it
'does not change when closed link is clicked'
do
find
(
'.issues-state-filters a'
,
text:
"Closed"
).
click
expect_
filtered_search_input
(
search_query
)
expect_
assignee_visual_tokens
(
)
end
it
'does not change when all link is clicked'
do
find
(
'.issues-state-filters a'
,
text:
"All"
).
click
expect_
filtered_search_input
(
search_query
)
expect_
assignee_visual_tokens
(
)
end
end
end
describe
'for milestone from mr#index'
do
let
(
:search_query
)
{
"milestone:%
#{
milestone
.
title
}
"
}
let
(
:search_query
)
{
"milestone:%
\"
#{
milestone
.
title
}
\"
"
}
def
expect_milestone_visual_tokens
expect_tokens
([{
name:
'milestone'
,
value:
"%
\"
#{
milestone
.
title
}
\"
"
}])
expect_filtered_search_input_empty
end
before
do
input_filtered_search
(
search_query
)
...
...
@@ -60,19 +70,19 @@ describe 'Filter merge requests', feature: true do
context
'milestone'
,
js:
true
do
it
'updates to current milestone'
do
expect_
filtered_search_input
(
search_query
)
expect_
milestone_visual_tokens
(
)
end
it
'does not change when closed link is clicked'
do
find
(
'.issues-state-filters a'
,
text:
"Closed"
).
click
expect_
filtered_search_input
(
search_query
)
expect_
milestone_visual_tokens
(
)
end
it
'does not change when all link is clicked'
do
find
(
'.issues-state-filters a'
,
text:
"All"
).
click
expect_
filtered_search_input
(
search_query
)
expect_
milestone_visual_tokens
(
)
end
end
end
...
...
@@ -82,35 +92,44 @@ describe 'Filter merge requests', feature: true do
input_filtered_search
(
'label:none'
)
expect_mr_list_count
(
1
)
expect_filtered_search_input
(
'label:none'
)
expect_tokens
([{
name:
'label'
,
value:
'none'
}])
expect_filtered_search_input_empty
end
it
'filters by a label'
do
input_filtered_search
(
"label:~
#{
label
.
title
}
"
)
expect_mr_list_count
(
0
)
expect_filtered_search_input
(
"label:~
#{
label
.
title
}
"
)
expect_tokens
([{
name:
'label'
,
value:
"~
#{
label
.
title
}
"
}])
expect_filtered_search_input_empty
end
it
"filters by `won't fix` and another label"
do
input_filtered_search
(
"label:~
\"
#{
wontfix
.
title
}
\"
label:~
#{
label
.
title
}
"
)
expect_mr_list_count
(
0
)
expect_filtered_search_input
(
"label:~
\"
#{
wontfix
.
title
}
\"
label:~
#{
label
.
title
}
"
)
expect_tokens
([
{
name:
'label'
,
value:
"~
\"
#{
wontfix
.
title
}
\"
"
},
{
name:
'label'
,
value:
"~
#{
label
.
title
}
"
}
])
expect_filtered_search_input_empty
end
it
"filters by `won't fix` label followed by another label after page load"
do
input_filtered_search
(
"label:~
\"
#{
wontfix
.
title
}
\"
"
)
expect_mr_list_count
(
0
)
expect_filtered_search_input
(
"label:~
\"
#{
wontfix
.
title
}
\"
"
)
input_filtered_search_keys
(
" label:~
#{
label
.
title
}
"
)
expect_tokens
([{
name:
'label'
,
value:
"~
\"
#{
wontfix
.
title
}
\"
"
}])
expect_filtered_search_input_empty
expect_filtered_search_input
(
"label:~
\"
#{
wontfix
.
title
}
\"
label:~
#{
label
.
title
}
"
)
input_filtered_search_keys
(
"
label:~
#{
label
.
title
}
"
)
expect_mr_list_count
(
0
)
expect_filtered_search_input
(
"label:~
\"
#{
wontfix
.
title
}
\"
label:~
#{
label
.
title
}
"
)
expect_tokens
([
{
name:
'label'
,
value:
"~
\"
#{
wontfix
.
title
}
\"
"
},
{
name:
'label'
,
value:
"~
#{
label
.
title
}
"
}
])
expect_filtered_search_input_empty
end
end
...
...
@@ -121,9 +140,10 @@ describe 'Filter merge requests', feature: true do
input_filtered_search
(
"assignee:@
#{
user
.
username
}
"
)
expect_mr_list_count
(
1
)
expect_filtered_search_input
(
"assignee:@
#{
user
.
username
}
"
)
expect_tokens
([{
name:
'assignee'
,
value:
"@
#{
user
.
username
}
"
}])
expect_filtered_search_input_empty
input_filtered_search_keys
(
"
label:~
#{
label
.
title
}
"
)
input_filtered_search_keys
(
"
label:~
#{
label
.
title
}
"
)
expect_mr_list_count
(
1
)
...
...
@@ -131,20 +151,28 @@ describe 'Filter merge requests', feature: true do
end
context
'assignee and label'
,
js:
true
do
def
expect_assignee_label_visual_tokens
expect_tokens
([
{
name:
'assignee'
,
value:
"@
#{
user
.
username
}
"
},
{
name:
'label'
,
value:
"~
#{
label
.
title
}
"
}
])
expect_filtered_search_input_empty
end
it
'updates to current assignee and label'
do
expect_
filtered_search_input
(
search_query
)
expect_
assignee_label_visual_tokens
(
)
end
it
'does not change when closed link is clicked'
do
find
(
'.issues-state-filters a'
,
text:
"Closed"
).
click
expect_
filtered_search_input
(
search_query
)
expect_
assignee_label_visual_tokens
(
)
end
it
'does not change when all link is clicked'
do
find
(
'.issues-state-filters a'
,
text:
"All"
).
click
expect_
filtered_search_input
(
search_query
)
expect_
assignee_label_visual_tokens
(
)
end
end
end
...
...
@@ -195,6 +223,8 @@ describe 'Filter merge requests', feature: true do
input_filtered_search_keys
(
' label:~bug'
)
expect_mr_list_count
(
1
)
expect_tokens
([{
name:
'label'
,
value:
'~bug'
}])
expect_filtered_search_input
(
'Bug'
)
end
it
'filters by text and milestone'
do
...
...
@@ -206,6 +236,8 @@ describe 'Filter merge requests', feature: true do
input_filtered_search_keys
(
' milestone:%8'
)
expect_mr_list_count
(
1
)
expect_tokens
([{
name:
'milestone'
,
value:
'%8'
}])
expect_filtered_search_input
(
'Bug'
)
end
it
'filters by text and assignee'
do
...
...
@@ -217,6 +249,8 @@ describe 'Filter merge requests', feature: true do
input_filtered_search_keys
(
" assignee:@
#{
user
.
username
}
"
)
expect_mr_list_count
(
1
)
expect_tokens
([{
name:
'assignee'
,
value:
"@
#{
user
.
username
}
"
}])
expect_filtered_search_input
(
'Bug'
)
end
it
'filters by text and author'
do
...
...
@@ -228,6 +262,8 @@ describe 'Filter merge requests', feature: true do
input_filtered_search_keys
(
" author:@
#{
user
.
username
}
"
)
expect_mr_list_count
(
1
)
expect_tokens
([{
name:
'author'
,
value:
"@
#{
user
.
username
}
"
}])
expect_filtered_search_input
(
'Bug'
)
end
end
end
...
...
@@ -266,7 +302,8 @@ describe 'Filter merge requests', feature: true do
it
'filter by current user'
do
visit
namespace_project_merge_requests_path
(
project
.
namespace
,
project
,
assignee_id:
user
.
id
)
expect_filtered_search_input
(
"assignee:@
#{
user
.
username
}
"
)
expect_tokens
([{
name:
'assignee'
,
value:
"@
#{
user
.
username
}
"
}])
expect_filtered_search_input_empty
end
it
'filter by new user'
do
...
...
@@ -275,7 +312,8 @@ describe 'Filter merge requests', feature: true do
visit
namespace_project_merge_requests_path
(
project
.
namespace
,
project
,
assignee_id:
new_user
.
id
)
expect_filtered_search_input
(
"assignee:@
#{
new_user
.
username
}
"
)
expect_tokens
([{
name:
'assignee'
,
value:
"@
#{
new_user
.
username
}
"
}])
expect_filtered_search_input_empty
end
end
...
...
@@ -283,7 +321,8 @@ describe 'Filter merge requests', feature: true do
it
'filter by current user'
do
visit
namespace_project_merge_requests_path
(
project
.
namespace
,
project
,
author_id:
user
.
id
)
expect_filtered_search_input
(
"author:@
#{
user
.
username
}
"
)
expect_tokens
([{
name:
'author'
,
value:
"@
#{
user
.
username
}
"
}])
expect_filtered_search_input_empty
end
it
'filter by new user'
do
...
...
@@ -292,7 +331,8 @@ describe 'Filter merge requests', feature: true do
visit
namespace_project_merge_requests_path
(
project
.
namespace
,
project
,
author_id:
new_user
.
id
)
expect_filtered_search_input
(
"author:@
#{
new_user
.
username
}
"
)
expect_tokens
([{
name:
'author'
,
value:
"@
#{
new_user
.
username
}
"
}])
expect_filtered_search_input_empty
end
end
end
spec/features/merge_requests/reset_filters_spec.rb
View file @
f44fb5cf
require
'rails_helper'
feature
'
Issues filter reset
button'
,
feature:
true
,
js:
true
do
feature
'
Merge requests filter clear
button'
,
feature:
true
,
js:
true
do
include
FilteredSearchHelpers
include
MergeRequestHelpers
include
WaitForAjax
...
...
@@ -24,67 +24,93 @@ feature 'Issues filter reset button', feature: true, js: true do
context
'when a milestone filter has been applied'
do
it
'resets the milestone filter'
do
visit_merge_requests
(
project
,
milestone_title:
milestone
.
title
)
expect
(
page
).
to
have_css
(
merge_request_css
,
count:
1
)
expect
(
get_filtered_search_placeholder
).
to
eq
(
''
)
reset_filters
expect
(
page
).
to
have_css
(
merge_request_css
,
count:
2
)
expect
(
get_filtered_search_placeholder
).
to
eq
(
default_placeholder
)
end
end
context
'when a label filter has been applied'
do
it
'resets the label filter'
do
visit_merge_requests
(
project
,
label_name:
bug
.
name
)
expect
(
page
).
to
have_css
(
merge_request_css
,
count:
1
)
expect
(
get_filtered_search_placeholder
).
to
eq
(
''
)
reset_filters
expect
(
page
).
to
have_css
(
merge_request_css
,
count:
2
)
expect
(
get_filtered_search_placeholder
).
to
eq
(
default_placeholder
)
end
end
context
'when a text search has been conducted'
do
it
'resets the text search filter'
do
visit_merge_requests
(
project
,
search:
'Bug'
)
expect
(
page
).
to
have_css
(
merge_request_css
,
count:
1
)
expect
(
get_filtered_search_placeholder
).
to
eq
(
''
)
reset_filters
expect
(
page
).
to
have_css
(
merge_request_css
,
count:
2
)
expect
(
get_filtered_search_placeholder
).
to
eq
(
default_placeholder
)
end
end
context
'when author filter has been applied'
do
it
'resets the author filter'
do
visit_merge_requests
(
project
,
author_username:
user
.
username
)
expect
(
page
).
to
have_css
(
merge_request_css
,
count:
1
)
expect
(
get_filtered_search_placeholder
).
to
eq
(
''
)
reset_filters
expect
(
page
).
to
have_css
(
merge_request_css
,
count:
2
)
expect
(
get_filtered_search_placeholder
).
to
eq
(
default_placeholder
)
end
end
context
'when assignee filter has been applied'
do
it
'resets the assignee filter'
do
visit_merge_requests
(
project
,
assignee_username:
user
.
username
)
expect
(
page
).
to
have_css
(
merge_request_css
,
count:
1
)
expect
(
get_filtered_search_placeholder
).
to
eq
(
''
)
reset_filters
expect
(
page
).
to
have_css
(
merge_request_css
,
count:
2
)
expect
(
get_filtered_search_placeholder
).
to
eq
(
default_placeholder
)
end
end
context
'when all filters have been applied'
do
it
'
reset
s all filters'
do
it
'
clear
s all filters'
do
visit_merge_requests
(
project
,
assignee_username:
user
.
username
,
author_username:
user
.
username
,
milestone_title:
milestone
.
title
,
label_name:
bug
.
name
,
search:
'Bug'
)
expect
(
page
).
to
have_css
(
merge_request_css
,
count:
0
)
expect
(
get_filtered_search_placeholder
).
to
eq
(
''
)
reset_filters
expect
(
page
).
to
have_css
(
merge_request_css
,
count:
2
)
expect
(
get_filtered_search_placeholder
).
to
eq
(
default_placeholder
)
end
end
context
'when no filters have been applied'
do
it
'the
reset link
should not be visible'
do
it
'the
clear button
should not be visible'
do
visit_merge_requests
(
project
)
expect
(
page
).
to
have_css
(
merge_request_css
,
count:
2
)
expect
(
get_filtered_search_placeholder
).
to
eq
(
default_placeholder
)
expect
(
page
).
not_to
have_css
(
clear_search_css
)
end
end
...
...
spec/features/search_spec.rb
View file @
f44fb5cf
require
'spec_helper'
describe
"Search"
,
feature:
true
do
include
FilteredSearchHelpers
include
WaitForAjax
let
(
:user
)
{
create
(
:user
)
}
...
...
@@ -170,7 +171,8 @@ describe "Search", feature: true do
sleep
2
expect
(
page
).
to
have_selector
(
'.filtered-search'
)
expect
(
find
(
'.filtered-search'
).
value
).
to
eq
(
"assignee:@
#{
user
.
username
}
"
)
expect_tokens
([{
name:
'assignee'
,
value:
"@
#{
user
.
username
}
"
}])
expect_filtered_search_input_empty
end
it
'takes user to her issues page when issues authored is clicked'
do
...
...
@@ -178,7 +180,8 @@ describe "Search", feature: true do
sleep
2
expect
(
page
).
to
have_selector
(
'.filtered-search'
)
expect
(
find
(
'.filtered-search'
).
value
).
to
eq
(
"author:@
#{
user
.
username
}
"
)
expect_tokens
([{
name:
'author'
,
value:
"@
#{
user
.
username
}
"
}])
expect_filtered_search_input_empty
end
it
'takes user to her MR page when MR assigned is clicked'
do
...
...
@@ -186,7 +189,8 @@ describe "Search", feature: true do
sleep
2
expect
(
page
).
to
have_selector
(
'.merge-requests-holder'
)
expect
(
find
(
'.filtered-search'
).
value
).
to
eq
(
"assignee:@
#{
user
.
username
}
"
)
expect_tokens
([{
name:
'assignee'
,
value:
"@
#{
user
.
username
}
"
}])
expect_filtered_search_input_empty
end
it
'takes user to her MR page when MR authored is clicked'
do
...
...
@@ -194,7 +198,8 @@ describe "Search", feature: true do
sleep
2
expect
(
page
).
to
have_selector
(
'.merge-requests-holder'
)
expect
(
find
(
'.filtered-search'
).
value
).
to
eq
(
"author:@
#{
user
.
username
}
"
)
expect_tokens
([{
name:
'author'
,
value:
"@
#{
user
.
username
}
"
}])
expect_filtered_search_input_empty
end
end
...
...
spec/javascripts/filtered_search/dropdown_user_spec.js
View file @
f44fb5cf
...
...
@@ -18,9 +18,7 @@ require('~/filtered_search/dropdown_user');
it
(
'
should not return the double quote found in value
'
,
()
=>
{
spyOn
(
gl
.
FilteredSearchTokenizer
,
'
processTokens
'
).
and
.
returnValue
({
lastToken
:
{
value
:
'
"johnny appleseed
'
,
},
lastToken
:
'
"johnny appleseed
'
,
});
expect
(
dropdownUser
.
getSearchInput
()).
toBe
(
'
johnny appleseed
'
);
...
...
@@ -28,9 +26,7 @@ require('~/filtered_search/dropdown_user');
it
(
'
should not return the single quote found in value
'
,
()
=>
{
spyOn
(
gl
.
FilteredSearchTokenizer
,
'
processTokens
'
).
and
.
returnValue
({
lastToken
:
{
value
:
'
\'
larry boy
'
,
},
lastToken
:
'
\'
larry boy
'
,
});
expect
(
dropdownUser
.
getSearchInput
()).
toBe
(
'
larry boy
'
);
...
...
spec/javascripts/filtered_search/dropdown_utils_spec.js
View file @
f44fb5cf
...
...
@@ -45,7 +45,7 @@ require('~/filtered_search/filtered_search_dropdown_manager');
});
it
(
'
should filter without symbol
'
,
()
=>
{
input
.
value
=
'
:
roo
'
;
input
.
value
=
'
roo
'
;
const
updatedItem
=
gl
.
DropdownUtils
.
filterWithSymbol
(
'
@
'
,
input
,
item
);
expect
(
updatedItem
.
droplab_hidden
).
toBe
(
false
);
...
...
@@ -58,69 +58,62 @@ require('~/filtered_search/filtered_search_dropdown_manager');
expect
(
updatedItem
.
droplab_hidden
).
toBe
(
false
);
});
it
(
'
should filter with colon
'
,
()
=>
{
input
.
value
=
'
roo
'
;
const
updatedItem
=
gl
.
DropdownUtils
.
filterWithSymbol
(
'
@
'
,
input
,
item
);
expect
(
updatedItem
.
droplab_hidden
).
toBe
(
false
);
});
describe
(
'
filters multiple word title
'
,
()
=>
{
const
multipleWordItem
=
{
title
:
'
Community Contributions
'
,
};
it
(
'
should filter with double quote
'
,
()
=>
{
input
.
value
=
'
label:
"
'
;
input
.
value
=
'
"
'
;
const
updatedItem
=
gl
.
DropdownUtils
.
filterWithSymbol
(
'
~
'
,
input
,
multipleWordItem
);
expect
(
updatedItem
.
droplab_hidden
).
toBe
(
false
);
});
it
(
'
should filter with double quote and symbol
'
,
()
=>
{
input
.
value
=
'
label:
~"
'
;
input
.
value
=
'
~"
'
;
const
updatedItem
=
gl
.
DropdownUtils
.
filterWithSymbol
(
'
~
'
,
input
,
multipleWordItem
);
expect
(
updatedItem
.
droplab_hidden
).
toBe
(
false
);
});
it
(
'
should filter with double quote and multiple words
'
,
()
=>
{
input
.
value
=
'
label:
"community con
'
;
input
.
value
=
'
"community con
'
;
const
updatedItem
=
gl
.
DropdownUtils
.
filterWithSymbol
(
'
~
'
,
input
,
multipleWordItem
);
expect
(
updatedItem
.
droplab_hidden
).
toBe
(
false
);
});
it
(
'
should filter with double quote, symbol and multiple words
'
,
()
=>
{
input
.
value
=
'
label:
~"community con
'
;
input
.
value
=
'
~"community con
'
;
const
updatedItem
=
gl
.
DropdownUtils
.
filterWithSymbol
(
'
~
'
,
input
,
multipleWordItem
);
expect
(
updatedItem
.
droplab_hidden
).
toBe
(
false
);
});
it
(
'
should filter with single quote
'
,
()
=>
{
input
.
value
=
'
label:
\'
'
;
input
.
value
=
'
\'
'
;
const
updatedItem
=
gl
.
DropdownUtils
.
filterWithSymbol
(
'
~
'
,
input
,
multipleWordItem
);
expect
(
updatedItem
.
droplab_hidden
).
toBe
(
false
);
});
it
(
'
should filter with single quote and symbol
'
,
()
=>
{
input
.
value
=
'
label:
~
\'
'
;
input
.
value
=
'
~
\'
'
;
const
updatedItem
=
gl
.
DropdownUtils
.
filterWithSymbol
(
'
~
'
,
input
,
multipleWordItem
);
expect
(
updatedItem
.
droplab_hidden
).
toBe
(
false
);
});
it
(
'
should filter with single quote and multiple words
'
,
()
=>
{
input
.
value
=
'
label:
\'
community con
'
;
input
.
value
=
'
\'
community con
'
;
const
updatedItem
=
gl
.
DropdownUtils
.
filterWithSymbol
(
'
~
'
,
input
,
multipleWordItem
);
expect
(
updatedItem
.
droplab_hidden
).
toBe
(
false
);
});
it
(
'
should filter with single quote, symbol and multiple words
'
,
()
=>
{
input
.
value
=
'
label:
~
\'
community con
'
;
input
.
value
=
'
~
\'
community con
'
;
const
updatedItem
=
gl
.
DropdownUtils
.
filterWithSymbol
(
'
~
'
,
input
,
multipleWordItem
);
expect
(
updatedItem
.
droplab_hidden
).
toBe
(
false
);
...
...
spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js
View file @
f44fb5cf
require
(
'
~/extensions/array
'
);
require
(
'
~/filtered_search/filtered_search_visual_tokens
'
);
require
(
'
~/filtered_search/filtered_search_tokenizer
'
);
require
(
'
~/filtered_search/filtered_search_dropdown_manager
'
);
...
...
@@ -14,24 +15,44 @@ require('~/filtered_search/filtered_search_dropdown_manager');
}
beforeEach
(()
=>
{
const
input
=
document
.
createElement
(
'
input
'
);
input
.
classList
.
add
(
'
filtered-search
'
);
document
.
body
.
appendChild
(
input
);
});
afterEach
(()
=>
{
document
.
querySelector
(
'
.filtered-search
'
).
outerHTML
=
''
;
setFixtures
(
`
<ul class="tokens-container">
<li class="input-token">
<input class="filtered-search">
</li>
</ul>
`
)
;
});
describe
(
'
input has no existing value
'
,
()
=>
{
it
(
'
should add just tokenName
'
,
()
=>
{
gl
.
FilteredSearchDropdownManager
.
addWordToInput
(
'
milestone
'
);
expect
(
getInputValue
()).
toBe
(
'
milestone:
'
);
const
token
=
document
.
querySelector
(
'
.tokens-container .js-visual-token
'
);
expect
(
token
.
classList
.
contains
(
'
filtered-search-token
'
)).
toEqual
(
true
);
expect
(
token
.
querySelector
(
'
.name
'
).
innerText
).
toBe
(
'
milestone
'
);
expect
(
getInputValue
()).
toBe
(
''
);
});
it
(
'
should add tokenName and tokenValue
'
,
()
=>
{
gl
.
FilteredSearchDropdownManager
.
addWordToInput
(
'
label
'
);
let
token
=
document
.
querySelector
(
'
.tokens-container .js-visual-token
'
);
expect
(
token
.
classList
.
contains
(
'
filtered-search-token
'
)).
toEqual
(
true
);
expect
(
token
.
querySelector
(
'
.name
'
).
innerText
).
toBe
(
'
label
'
);
expect
(
getInputValue
()).
toBe
(
''
);
gl
.
FilteredSearchDropdownManager
.
addWordToInput
(
'
label
'
,
'
none
'
);
expect
(
getInputValue
()).
toBe
(
'
label:none
'
);
// We have to get that reference again
// Because gl.FilteredSearchDropdownManager deletes the previous token
token
=
document
.
querySelector
(
'
.tokens-container .js-visual-token
'
);
expect
(
token
.
classList
.
contains
(
'
filtered-search-token
'
)).
toEqual
(
true
);
expect
(
token
.
querySelector
(
'
.name
'
).
innerText
).
toBe
(
'
label
'
);
expect
(
token
.
querySelector
(
'
.value
'
).
innerText
).
toBe
(
'
none
'
);
expect
(
getInputValue
()).
toBe
(
''
);
});
});
...
...
@@ -39,19 +60,40 @@ require('~/filtered_search/filtered_search_dropdown_manager');
it
(
'
should be able to just add tokenName
'
,
()
=>
{
setInputValue
(
'
a
'
);
gl
.
FilteredSearchDropdownManager
.
addWordToInput
(
'
author
'
);
expect
(
getInputValue
()).
toBe
(
'
author:
'
);
const
token
=
document
.
querySelector
(
'
.tokens-container .js-visual-token
'
);
expect
(
token
.
classList
.
contains
(
'
filtered-search-token
'
)).
toEqual
(
true
);
expect
(
token
.
querySelector
(
'
.name
'
).
innerText
).
toBe
(
'
author
'
);
expect
(
getInputValue
()).
toBe
(
''
);
});
it
(
'
should replace tokenValue
'
,
()
=>
{
setInputValue
(
'
author:roo
'
);
gl
.
FilteredSearchDropdownManager
.
addWordToInput
(
'
author
'
,
'
@root
'
);
expect
(
getInputValue
()).
toBe
(
'
author:@root
'
);
gl
.
FilteredSearchDropdownManager
.
addWordToInput
(
'
author
'
);
setInputValue
(
'
roo
'
);
gl
.
FilteredSearchDropdownManager
.
addWordToInput
(
null
,
'
@root
'
);
const
token
=
document
.
querySelector
(
'
.tokens-container .js-visual-token
'
);
expect
(
token
.
classList
.
contains
(
'
filtered-search-token
'
)).
toEqual
(
true
);
expect
(
token
.
querySelector
(
'
.name
'
).
innerText
).
toBe
(
'
author
'
);
expect
(
token
.
querySelector
(
'
.value
'
).
innerText
).
toBe
(
'
@root
'
);
expect
(
getInputValue
()).
toBe
(
''
);
});
it
(
'
should add tokenValues containing spaces
'
,
()
=>
{
setInputValue
(
'
label:~"test
'
);
gl
.
FilteredSearchDropdownManager
.
addWordToInput
(
'
label
'
);
setInputValue
(
'
"test
'
);
gl
.
FilteredSearchDropdownManager
.
addWordToInput
(
'
label
'
,
'
~
\'
"test me"
\'
'
);
expect
(
getInputValue
()).
toBe
(
'
label:~
\'
"test me"
\'
'
);
const
token
=
document
.
querySelector
(
'
.tokens-container .js-visual-token
'
);
expect
(
token
.
classList
.
contains
(
'
filtered-search-token
'
)).
toEqual
(
true
);
expect
(
token
.
querySelector
(
'
.name
'
).
innerText
).
toBe
(
'
label
'
);
expect
(
token
.
querySelector
(
'
.value
'
).
innerText
).
toBe
(
'
~
\'
"test me"
\'
'
);
expect
(
getInputValue
()).
toBe
(
''
);
});
});
});
...
...
spec/javascripts/filtered_search/filtered_search_manager_spec.js
View file @
f44fb5cf
...
...
@@ -4,64 +4,244 @@ require('~/filtered_search/filtered_search_token_keys');
require
(
'
~/filtered_search/filtered_search_tokenizer
'
);
require
(
'
~/filtered_search/filtered_search_dropdown_manager
'
);
require
(
'
~/filtered_search/filtered_search_manager
'
);
const
FilteredSearchSpecHelper
=
require
(
'
../helpers/filtered_search_spec_helper
'
);
(()
=>
{
describe
(
'
Filtered Search Manager
'
,
()
=>
{
describe
(
'
search
'
,
()
=>
{
let
input
;
let
manager
;
const
defaultParams
=
'
?scope=all&utf8=✓&state=opened
'
;
let
tokensContainer
;
const
placeholder
=
'
Search or filter results...
'
;
function
getInput
()
{
return
document
.
querySelector
(
'
.filtered-search
'
);
function
dispatchBackspaceEvent
(
element
,
eventType
)
{
const
backspaceKey
=
8
;
const
event
=
new
Event
(
eventType
);
event
.
keyCode
=
backspaceKey
;
element
.
dispatchEvent
(
event
);
}
function
dispatchDeleteEvent
(
element
,
eventType
)
{
const
deleteKey
=
46
;
const
event
=
new
Event
(
eventType
);
event
.
keyCode
=
deleteKey
;
element
.
dispatchEvent
(
event
);
}
beforeEach
(()
=>
{
setFixtures
(
`
<input type='text' class='filtered-search' />
<div class="filtered-search-input-container">
<form>
<ul class="tokens-container list-unstyled">
${
FilteredSearchSpecHelper
.
createInputHTML
(
placeholder
)}
</ul>
<button class="clear-search" type="button">
<i class="fa fa-times"></i>
</button>
</form>
</div>
`
);
spyOn
(
gl
.
FilteredSearchManager
.
prototype
,
'
bindEvents
'
).
and
.
callFake
(()
=>
{});
spyOn
(
gl
.
FilteredSearchManager
.
prototype
,
'
cleanup
'
).
and
.
callFake
(()
=>
{});
spyOn
(
gl
.
FilteredSearchManager
.
prototype
,
'
loadSearchParamsFromURL
'
).
and
.
callFake
(()
=>
{});
spyOn
(
gl
.
FilteredSearchManager
.
prototype
,
'
tokenChange
'
).
and
.
callFake
(()
=>
{});
spyOn
(
gl
.
FilteredSearchDropdownManager
.
prototype
,
'
setDropdown
'
).
and
.
callFake
(()
=>
{});
spyOn
(
gl
.
FilteredSearchDropdownManager
.
prototype
,
'
updateDropdownOffset
'
).
and
.
callFake
(()
=>
{});
spyOn
(
gl
.
utils
,
'
getParameterByName
'
).
and
.
returnValue
(
null
);
spyOn
(
gl
.
FilteredSearchVisualTokens
,
'
unselectTokens
'
).
and
.
callThrough
();
input
=
document
.
querySelector
(
'
.filtered-search
'
);
tokensContainer
=
document
.
querySelector
(
'
.tokens-container
'
);
manager
=
new
gl
.
FilteredSearchManager
();
});
afterEach
(()
=>
{
getInput
().
outerHTML
=
''
;
});
describe
(
'
search
'
,
()
=>
{
const
defaultParams
=
'
?scope=all&utf8=✓&state=opened
'
;
it
(
'
should search with a single word
'
,
()
=>
{
getInput
()
.
value
=
'
searchTerm
'
;
it
(
'
should search with a single word
'
,
(
done
)
=>
{
input
.
value
=
'
searchTerm
'
;
spyOn
(
gl
.
utils
,
'
visitUrl
'
).
and
.
callFake
((
url
)
=>
{
expect
(
url
).
toEqual
(
`
${
defaultParams
}
&search=searchTerm`
);
done
();
});
manager
.
search
();
});
it
(
'
should search with multiple words
'
,
()
=>
{
getInput
()
.
value
=
'
awesome search terms
'
;
it
(
'
should search with multiple words
'
,
(
done
)
=>
{
input
.
value
=
'
awesome search terms
'
;
spyOn
(
gl
.
utils
,
'
visitUrl
'
).
and
.
callFake
((
url
)
=>
{
expect
(
url
).
toEqual
(
`
${
defaultParams
}
&search=awesome+search+terms`
);
done
();
});
manager
.
search
();
});
it
(
'
should search with special characters
'
,
()
=>
{
getInput
()
.
value
=
'
~!@#$%^&*()_+{}:<>,.?/
'
;
it
(
'
should search with special characters
'
,
(
done
)
=>
{
input
.
value
=
'
~!@#$%^&*()_+{}:<>,.?/
'
;
spyOn
(
gl
.
utils
,
'
visitUrl
'
).
and
.
callFake
((
url
)
=>
{
expect
(
url
).
toEqual
(
`
${
defaultParams
}
&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`
);
done
();
});
manager
.
search
();
});
});
describe
(
'
handleInputPlaceholder
'
,
()
=>
{
it
(
'
should render placeholder when there is no input
'
,
()
=>
{
expect
(
input
.
placeholder
).
toEqual
(
placeholder
);
});
it
(
'
should not render placeholder when there is input
'
,
()
=>
{
input
.
value
=
'
test words
'
;
const
event
=
new
Event
(
'
input
'
);
input
.
dispatchEvent
(
event
);
expect
(
input
.
placeholder
).
toEqual
(
''
);
});
it
(
'
should not render placeholder when there are tokens and no input
'
,
()
=>
{
tokensContainer
.
innerHTML
=
FilteredSearchSpecHelper
.
createTokensContainerHTML
(
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
label
'
,
'
~bug
'
),
);
const
event
=
new
Event
(
'
input
'
);
input
.
dispatchEvent
(
event
);
expect
(
input
.
placeholder
).
toEqual
(
''
);
});
});
describe
(
'
checkForBackspace
'
,
()
=>
{
describe
(
'
tokens and no input
'
,
()
=>
{
beforeEach
(()
=>
{
tokensContainer
.
innerHTML
=
FilteredSearchSpecHelper
.
createTokensContainerHTML
(
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
label
'
,
'
~bug
'
),
);
});
it
(
'
removes last token
'
,
()
=>
{
spyOn
(
gl
.
FilteredSearchVisualTokens
,
'
removeLastTokenPartial
'
).
and
.
callThrough
();
dispatchBackspaceEvent
(
input
,
'
keyup
'
);
expect
(
gl
.
FilteredSearchVisualTokens
.
removeLastTokenPartial
).
toHaveBeenCalled
();
});
it
(
'
sets the input
'
,
()
=>
{
spyOn
(
gl
.
FilteredSearchVisualTokens
,
'
getLastTokenPartial
'
).
and
.
callThrough
();
dispatchDeleteEvent
(
input
,
'
keyup
'
);
expect
(
gl
.
FilteredSearchVisualTokens
.
getLastTokenPartial
).
toHaveBeenCalled
();
expect
(
input
.
value
).
toEqual
(
'
~bug
'
);
});
});
it
(
'
does not remove token or change input when there is existing input
'
,
()
=>
{
spyOn
(
gl
.
FilteredSearchVisualTokens
,
'
removeLastTokenPartial
'
).
and
.
callThrough
();
spyOn
(
gl
.
FilteredSearchVisualTokens
,
'
getLastTokenPartial
'
).
and
.
callThrough
();
input
.
value
=
'
text
'
;
dispatchDeleteEvent
(
input
,
'
keyup
'
);
expect
(
gl
.
FilteredSearchVisualTokens
.
removeLastTokenPartial
).
not
.
toHaveBeenCalled
();
expect
(
gl
.
FilteredSearchVisualTokens
.
getLastTokenPartial
).
not
.
toHaveBeenCalled
();
expect
(
input
.
value
).
toEqual
(
'
text
'
);
});
});
describe
(
'
removeSelectedToken
'
,
()
=>
{
function
getVisualTokens
()
{
return
tokensContainer
.
querySelectorAll
(
'
.js-visual-token
'
);
}
beforeEach
(()
=>
{
tokensContainer
.
innerHTML
=
FilteredSearchSpecHelper
.
createTokensContainerHTML
(
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
milestone
'
,
'
none
'
,
true
),
);
});
it
(
'
removes selected token when the backspace key is pressed
'
,
()
=>
{
expect
(
getVisualTokens
().
length
).
toEqual
(
1
);
dispatchBackspaceEvent
(
document
,
'
keydown
'
);
expect
(
getVisualTokens
().
length
).
toEqual
(
0
);
});
it
(
'
removes selected token when the delete key is pressed
'
,
()
=>
{
expect
(
getVisualTokens
().
length
).
toEqual
(
1
);
dispatchDeleteEvent
(
document
,
'
keydown
'
);
expect
(
getVisualTokens
().
length
).
toEqual
(
0
);
});
it
(
'
updates the input placeholder after removal
'
,
()
=>
{
manager
.
handleInputPlaceholder
();
expect
(
input
.
placeholder
).
toEqual
(
''
);
expect
(
getVisualTokens
().
length
).
toEqual
(
1
);
dispatchBackspaceEvent
(
document
,
'
keydown
'
);
expect
(
input
.
placeholder
).
not
.
toEqual
(
''
);
expect
(
getVisualTokens
().
length
).
toEqual
(
0
);
});
it
(
'
updates the clear button after removal
'
,
()
=>
{
manager
.
toggleClearSearchButton
();
const
clearButton
=
document
.
querySelector
(
'
.clear-search
'
);
expect
(
clearButton
.
classList
.
contains
(
'
hidden
'
)).
toEqual
(
false
);
expect
(
getVisualTokens
().
length
).
toEqual
(
1
);
dispatchBackspaceEvent
(
document
,
'
keydown
'
);
expect
(
clearButton
.
classList
.
contains
(
'
hidden
'
)).
toEqual
(
true
);
expect
(
getVisualTokens
().
length
).
toEqual
(
0
);
});
});
describe
(
'
unselects token
'
,
()
=>
{
beforeEach
(()
=>
{
tokensContainer
.
innerHTML
=
FilteredSearchSpecHelper
.
createTokensContainerHTML
(
`
${
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
label
'
,
'
~bug
'
,
true
)}
${
FilteredSearchSpecHelper
.
createSearchVisualTokenHTML
(
'
search term
'
)}
${
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
label
'
,
'
~awesome
'
)}
`
);
});
it
(
'
unselects token when input is clicked
'
,
()
=>
{
const
selectedToken
=
tokensContainer
.
querySelector
(
'
.js-visual-token .selected
'
);
expect
(
selectedToken
.
classList
.
contains
(
'
selected
'
)).
toEqual
(
true
);
expect
(
gl
.
FilteredSearchVisualTokens
.
unselectTokens
).
not
.
toHaveBeenCalled
();
// Click directly on input attached to document
// so that the click event will propagate properly
document
.
querySelector
(
'
.filtered-search
'
).
click
();
expect
(
gl
.
FilteredSearchVisualTokens
.
unselectTokens
).
toHaveBeenCalled
();
expect
(
selectedToken
.
classList
.
contains
(
'
selected
'
)).
toEqual
(
false
);
});
it
(
'
unselects token when document.body is clicked
'
,
()
=>
{
const
selectedToken
=
tokensContainer
.
querySelector
(
'
.js-visual-token .selected
'
);
expect
(
selectedToken
.
classList
.
contains
(
'
selected
'
)).
toEqual
(
true
);
expect
(
gl
.
FilteredSearchVisualTokens
.
unselectTokens
).
not
.
toHaveBeenCalled
();
document
.
body
.
click
();
expect
(
selectedToken
.
classList
.
contains
(
'
selected
'
)).
toEqual
(
false
);
expect
(
gl
.
FilteredSearchVisualTokens
.
unselectTokens
).
toHaveBeenCalled
();
});
});
});
})();
spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
0 → 100644
View file @
f44fb5cf
This diff is collapsed.
Click to expand it.
spec/javascripts/helpers/filtered_search_spec_helper.js
0 → 100644
View file @
f44fb5cf
class
FilteredSearchSpecHelper
{
static
createFilterVisualTokenHTML
(
name
,
value
,
isSelected
)
{
return
FilteredSearchSpecHelper
.
createFilterVisualToken
(
name
,
value
,
isSelected
).
outerHTML
;
}
static
createFilterVisualToken
(
name
,
value
,
isSelected
=
false
)
{
const
li
=
document
.
createElement
(
'
li
'
);
li
.
classList
.
add
(
'
js-visual-token
'
,
'
filtered-search-token
'
);
li
.
innerHTML
=
`
<div class="selectable
${
isSelected
?
'
selected
'
:
''
}
" role="button">
<div class="name">
${
name
}
</div>
<div class="value">
${
value
}
</div>
</div>
`
;
return
li
;
}
static
createNameFilterVisualTokenHTML
(
name
)
{
return
`
<li class="js-visual-token filtered-search-token">
<div class="name">
${
name
}
</div>
</li>
`
;
}
static
createSearchVisualTokenHTML
(
name
)
{
return
`
<li class="js-visual-token filtered-search-term">
<div class="name">
${
name
}
</div>
</li>
`
;
}
static
createInputHTML
(
placeholder
=
''
)
{
return
`
<li class="input-token">
<input type='text' class='filtered-search' placeholder='
${
placeholder
}
' />
</li>
`
;
}
static
createTokensContainerHTML
(
html
,
inputPlaceholder
)
{
return
`
${
html
}
${
FilteredSearchSpecHelper
.
createInputHTML
(
inputPlaceholder
)}
`
;
}
}
module
.
exports
=
FilteredSearchSpecHelper
;
spec/support/filtered_search_helpers.rb
View file @
f44fb5cf
...
...
@@ -3,16 +3,20 @@ module FilteredSearchHelpers
page
.
find
(
'.filtered-search'
)
end
# Enables input to be set (similar to copy and paste)
def
input_filtered_search
(
search_term
,
submit:
true
)
filtered_search
.
set
(
search_term
)
# Add an extra space to engage visual tokens
filtered_search
.
set
(
"
#{
search_term
}
"
)
if
submit
filtered_search
.
send_keys
(
:enter
)
end
end
# Enables input to be added character by character
def
input_filtered_search_keys
(
search_term
)
filtered_search
.
send_keys
(
search_term
)
# Add an extra space to engage visual tokens
filtered_search
.
send_keys
(
"
#{
search_term
}
"
)
filtered_search
.
send_keys
(
:enter
)
end
...
...
@@ -34,4 +38,32 @@ module FilteredSearchHelpers
# This ensures the dropdown is shown
expect
(
find
(
'#js-dropdown-label'
)).
not_to
have_css
(
'.filter-dropdown-loading'
)
end
def
expect_filtered_search_input_empty
expect
(
find
(
'.filtered-search'
).
value
).
to
eq
(
''
)
end
# Iterates through each visual token inside
# .tokens-container to make sure the correct names and values are rendered
def
expect_tokens
(
tokens
)
page
.
find
'.filtered-search-input-container .tokens-container'
do
page
.
all
(
:css
,
'.tokens-container li'
).
each_with_index
do
|
el
,
index
|
token_name
=
tokens
[
index
][
:name
]
token_value
=
tokens
[
index
][
:value
]
expect
(
el
.
find
(
'.name'
)).
to
have_content
(
token_name
)
if
token_value
expect
(
el
.
find
(
'.value'
)).
to
have_content
(
token_value
)
end
end
end
end
def
default_placeholder
'Search or filter results...'
end
def
get_filtered_search_placeholder
find
(
'.filtered-search'
)[
'placeholder'
]
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