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
be8e2b9c
Commit
be8e2b9c
authored
Aug 30, 2017
by
Phil Hughes
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'add-filter-by-my-reaction' into 'master'
Add filter by my reaction Closes #35618 See merge request !12962
parents
df8ca5aa
7187395e
Changes
26
Show whitespace changes
Inline
Side-by-side
Showing
26 changed files
with
693 additions
and
138 deletions
+693
-138
app/assets/javascripts/droplab/drop_down.js
app/assets/javascripts/droplab/drop_down.js
+7
-0
app/assets/javascripts/filtered_search/dropdown_emoji.js
app/assets/javascripts/filtered_search/dropdown_emoji.js
+82
-0
app/assets/javascripts/filtered_search/dropdown_hint.js
app/assets/javascripts/filtered_search/dropdown_hint.js
+1
-1
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_manager.js
...ripts/filtered_search/filtered_search_dropdown_manager.js
+5
-0
app/assets/javascripts/filtered_search/filtered_search_manager.js
...ts/javascripts/filtered_search/filtered_search_manager.js
+11
-3
app/assets/javascripts/filtered_search/filtered_search_token_keys.js
...javascripts/filtered_search/filtered_search_token_keys.js
+20
-0
app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
...ascripts/filtered_search/filtered_search_visual_tokens.js
+21
-0
app/assets/stylesheets/framework/filters.scss
app/assets/stylesheets/framework/filters.scss
+13
-1
app/controllers/autocomplete_controller.rb
app/controllers/autocomplete_controller.rb
+17
-1
app/finders/issuable_finder.rb
app/finders/issuable_finder.rb
+10
-0
app/models/concerns/awardable.rb
app/models/concerns/awardable.rb
+15
-0
app/views/shared/issuable/_search_bar.html.haml
app/views/shared/issuable/_search_bar.html.haml
+7
-0
changelogs/unreleased/add-filter-by-my-reaction.yml
changelogs/unreleased/add-filter-by-my-reaction.yml
+4
-0
config/routes.rb
config/routes.rb
+1
-0
spec/controllers/autocomplete_controller_spec.rb
spec/controllers/autocomplete_controller_spec.rb
+38
-0
spec/features/issues/filtered_search/dropdown_assignee_spec.rb
...features/issues/filtered_search/dropdown_assignee_spec.rb
+6
-0
spec/features/issues/filtered_search/dropdown_emoji_spec.rb
spec/features/issues/filtered_search/dropdown_emoji_spec.rb
+182
-0
spec/features/issues/filtered_search/dropdown_hint_spec.rb
spec/features/issues/filtered_search/dropdown_hint_spec.rb
+166
-122
spec/features/issues/filtered_search/dropdown_label_spec.rb
spec/features/issues/filtered_search/dropdown_label_spec.rb
+6
-0
spec/features/issues/filtered_search/dropdown_milestone_spec.rb
...eatures/issues/filtered_search/dropdown_milestone_spec.rb
+6
-0
spec/features/issues/filtered_search/search_bar_spec.rb
spec/features/issues/filtered_search/search_bar_spec.rb
+1
-1
spec/finders/issues_finder_spec.rb
spec/finders/issues_finder_spec.rb
+35
-0
spec/javascripts/droplab/drop_down_spec.js
spec/javascripts/droplab/drop_down_spec.js
+13
-2
spec/models/concerns/awardable_spec.rb
spec/models/concerns/awardable_spec.rb
+15
-7
spec/support/filtered_search_helpers.rb
spec/support/filtered_search_helpers.rb
+10
-0
No files found.
app/assets/javascripts/droplab/drop_down.js
View file @
be8e2b9c
...
...
@@ -85,6 +85,13 @@ class DropDown {
const
renderableList
=
this
.
list
.
querySelector
(
'
ul[data-dynamic]
'
)
||
this
.
list
;
renderableList
.
innerHTML
=
children
.
join
(
''
);
const
listEvent
=
new
CustomEvent
(
'
render.dl
'
,
{
detail
:
{
list
:
this
,
},
});
this
.
list
.
dispatchEvent
(
listEvent
);
}
renderChildren
(
data
)
{
...
...
app/assets/javascripts/filtered_search/dropdown_emoji.js
0 → 100644
View file @
be8e2b9c
/* global Flash */
import
Ajax
from
'
~/droplab/plugins/ajax
'
;
import
Filter
from
'
~/droplab/plugins/filter
'
;
import
'
./filtered_search_dropdown
'
;
class
DropdownEmoji
extends
gl
.
FilteredSearchDropdown
{
constructor
(
options
=
{})
{
super
(
options
);
this
.
config
=
{
Ajax
:
{
endpoint
:
`
${
gon
.
relative_url_root
||
''
}
/autocomplete/award_emojis`
,
method
:
'
setData
'
,
loadingTemplate
:
this
.
loadingTemplate
,
onError
()
{
/* eslint-disable no-new */
new
Flash
(
'
An error occured fetching the dropdown data.
'
);
/* eslint-enable no-new */
},
},
Filter
:
{
template
:
'
name
'
,
},
};
import
(
/* webpackChunkName: 'emoji' */
'
~/emoji
'
)
.
then
(({
glEmojiTag
})
=>
{
this
.
glEmojiTag
=
glEmojiTag
;
})
.
catch
(()
=>
{
/* ignore error and leave emoji name in the search bar */
});
this
.
unbindEvents
();
this
.
bindEvents
();
}
bindEvents
()
{
super
.
bindEvents
();
this
.
listRenderedWrapper
=
this
.
listRendered
.
bind
(
this
);
this
.
dropdown
.
addEventListener
(
'
render.dl
'
,
this
.
listRenderedWrapper
);
}
unbindEvents
()
{
this
.
dropdown
.
removeEventListener
(
'
render.dl
'
,
this
.
listRenderedWrapper
);
super
.
unbindEvents
();
}
listRendered
()
{
this
.
replaceEmojiElement
();
}
itemClicked
(
e
)
{
super
.
itemClicked
(
e
,
(
selected
)
=>
{
const
name
=
selected
.
querySelector
(
'
.js-data-value
'
).
innerText
.
trim
();
return
gl
.
DropdownUtils
.
getEscapedText
(
name
);
});
}
renderContent
(
forceShowList
=
false
)
{
this
.
droplab
.
changeHookList
(
this
.
hookId
,
this
.
dropdown
,
[
Ajax
,
Filter
],
this
.
config
);
super
.
renderContent
(
forceShowList
);
}
replaceEmojiElement
()
{
if
(
!
this
.
glEmojiTag
)
return
;
// Replace empty gl-emoji tag to real content
const
dropdownItems
=
[...
this
.
dropdown
.
querySelectorAll
(
'
.filter-dropdown-item
'
)];
dropdownItems
.
forEach
((
dropdownItem
)
=>
{
const
name
=
dropdownItem
.
querySelector
(
'
.js-data-value
'
).
innerText
;
const
emojiTag
=
this
.
glEmojiTag
(
name
);
const
emojiElement
=
dropdownItem
.
querySelector
(
'
gl-emoji
'
);
emojiElement
.
outerHTML
=
emojiTag
;
});
}
init
()
{
this
.
droplab
.
addHook
(
this
.
input
,
this
.
dropdown
,
[
Ajax
,
Filter
],
this
.
config
).
init
();
}
}
window
.
gl
=
window
.
gl
||
{};
gl
.
DropdownEmoji
=
DropdownEmoji
;
app/assets/javascripts/filtered_search/dropdown_hint.js
View file @
be8e2b9c
...
...
@@ -61,7 +61,7 @@ class DropdownHint extends gl.FilteredSearchDropdown {
.
map
(
tokenKey
=>
({
icon
:
`fa-
${
tokenKey
.
icon
}
`
,
hint
:
tokenKey
.
key
,
tag
:
`<
${
tokenKey
.
symbol
}${
tokenKey
.
key
}
>`
,
tag
:
`<
${
tokenKey
.
tag
}
>`
,
type
:
tokenKey
.
type
,
}));
...
...
app/assets/javascripts/filtered_search/filtered_search_bundle.js
View file @
be8e2b9c
import
'
./dropdown_emoji
'
;
import
'
./dropdown_hint
'
;
import
'
./dropdown_non_user
'
;
import
'
./dropdown_user
'
;
...
...
app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
View file @
be8e2b9c
...
...
@@ -58,6 +58,11 @@ class FilteredSearchDropdownManager {
},
element
:
this
.
container
.
querySelector
(
'
#js-dropdown-label
'
),
},
'
my-reaction
'
:
{
reference
:
null
,
gl
:
'
DropdownEmoji
'
,
element
:
this
.
container
.
querySelector
(
'
#js-dropdown-my-reaction
'
),
},
hint
:
{
reference
:
null
,
gl
:
'
DropdownHint
'
,
...
...
app/assets/javascripts/filtered_search/filtered_search_manager.js
View file @
be8e2b9c
...
...
@@ -439,8 +439,13 @@ class FilteredSearchManager {
const
match
=
this
.
filteredSearchTokenKeys
.
searchByKeyParam
(
keyParam
);
if
(
match
)
{
const
indexOf
=
keyParam
.
indexOf
(
'
_
'
);
const
sanitizedKey
=
indexOf
!==
-
1
?
keyParam
.
slice
(
0
,
keyParam
.
indexOf
(
'
_
'
))
:
keyParam
;
// Use lastIndexOf because the token key is allowed to contain underscore
// e.g. 'my_reaction' is the token key of 'my_reaction_emoji'
const
lastIndexOf
=
keyParam
.
lastIndexOf
(
'
_
'
);
let
sanitizedKey
=
lastIndexOf
!==
-
1
?
keyParam
.
slice
(
0
,
lastIndexOf
)
:
keyParam
;
// Replace underscore with hyphen in the sanitizedkey.
// e.g. 'my_reaction' => 'my-reaction'
sanitizedKey
=
sanitizedKey
.
replace
(
'
_
'
,
'
-
'
);
const
symbol
=
match
.
symbol
;
let
quotationsToUse
=
''
;
...
...
@@ -515,7 +520,10 @@ class FilteredSearchManager {
const
condition
=
this
.
filteredSearchTokenKeys
.
searchByConditionKeyValue
(
token
.
key
,
token
.
value
.
toLowerCase
());
const
{
param
}
=
this
.
filteredSearchTokenKeys
.
searchByKey
(
token
.
key
)
||
{};
const
keyParam
=
param
?
`
${
token
.
key
}
_
${
param
}
`
:
token
.
key
;
// Replace hyphen with underscore to use as request parameter
// e.g. 'my-reaction' => 'my_reaction'
const
underscoredKey
=
token
.
key
.
replace
(
'
-
'
,
'
_
'
);
const
keyParam
=
param
?
`
${
underscoredKey
}
_
${
param
}
`
:
underscoredKey
;
let
tokenPath
=
''
;
if
(
condition
)
{
...
...
app/assets/javascripts/filtered_search/filtered_search_token_keys.js
View file @
be8e2b9c
...
...
@@ -4,26 +4,42 @@ const tokenKeys = [{
param
:
'
username
'
,
symbol
:
'
@
'
,
icon
:
'
pencil
'
,
tag
:
'
@author
'
,
},
{
key
:
'
assignee
'
,
type
:
'
string
'
,
param
:
'
username
'
,
symbol
:
'
@
'
,
icon
:
'
user
'
,
tag
:
'
@assignee
'
,
},
{
key
:
'
milestone
'
,
type
:
'
string
'
,
param
:
'
title
'
,
symbol
:
'
%
'
,
icon
:
'
clock-o
'
,
tag
:
'
%milestone
'
,
},
{
key
:
'
label
'
,
type
:
'
array
'
,
param
:
'
name[]
'
,
symbol
:
'
~
'
,
icon
:
'
tag
'
,
tag
:
'
~label
'
,
}];
if
(
gon
.
current_user_id
)
{
// Appending tokenkeys only logged-in
tokenKeys
.
push
({
key
:
'
my-reaction
'
,
type
:
'
string
'
,
param
:
'
emoji
'
,
symbol
:
''
,
icon
:
'
thumbs-up
'
,
tag
:
'
emoji
'
,
});
}
const
alternativeTokenKeys
=
[{
key
:
'
label
'
,
type
:
'
string
'
,
...
...
@@ -84,6 +100,10 @@ class FilteredSearchTokenKeys {
return
tokenKeysWithAlternative
.
find
((
tokenKey
)
=>
{
let
tokenKeyParam
=
tokenKey
.
key
;
// Replace hyphen with underscore to compare keyParam with tokenKeyParam
// e.g. 'my-reaction' => 'my_reaction'
tokenKeyParam
=
tokenKeyParam
.
replace
(
'
-
'
,
'
_
'
);
if
(
tokenKey
.
param
)
{
tokenKeyParam
+=
`_
${
tokenKey
.
param
}
`
;
}
...
...
app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
View file @
be8e2b9c
...
...
@@ -132,6 +132,23 @@ class FilteredSearchVisualTokens {
.
catch
(()
=>
{
});
}
static
updateEmojiTokenAppearance
(
tokenValueContainer
,
tokenValueElement
,
tokenValue
)
{
const
container
=
tokenValueContainer
;
const
element
=
tokenValueElement
;
return
import
(
/* webpackChunkName: 'emoji' */
'
../emoji
'
)
.
then
((
Emoji
)
=>
{
if
(
!
Emoji
.
isEmojiNameValid
(
tokenValue
))
{
return
;
}
container
.
dataset
.
originalValue
=
tokenValue
;
element
.
innerHTML
=
Emoji
.
glEmojiTag
(
tokenValue
);
})
// ignore error and leave emoji name in the search bar
.
catch
(()
=>
{
});
}
static
renderVisualTokenValue
(
parentElement
,
tokenName
,
tokenValue
)
{
const
tokenValueContainer
=
parentElement
.
querySelector
(
'
.value-container
'
);
const
tokenValueElement
=
tokenValueContainer
.
querySelector
(
'
.value
'
);
...
...
@@ -144,6 +161,10 @@ class FilteredSearchVisualTokens {
FilteredSearchVisualTokens
.
updateUserTokenAppearance
(
tokenValueContainer
,
tokenValueElement
,
tokenValue
,
);
}
else
if
(
tokenType
===
'
my-reaction
'
)
{
FilteredSearchVisualTokens
.
updateEmojiTokenAppearance
(
tokenValueContainer
,
tokenValueElement
,
tokenValue
,
);
}
}
...
...
app/assets/stylesheets/framework/filters.scss
View file @
be8e2b9c
...
...
@@ -225,6 +225,18 @@
color
:
$common-gray-dark
;
}
gl-emoji
{
display
:
inline-block
;
font-family
:
inherit
;
font-size
:
inherit
;
vertical-align
:
inherit
;
img
{
height
:
18px
;
width
:
18px
;
}
}
.form-control
{
position
:
relative
;
min-width
:
200px
;
...
...
@@ -277,7 +289,7 @@
}
.filtered-search-input-dropdown-menu
{
max-height
:
2
25
px
;
max-height
:
2
60
px
;
max-width
:
280px
;
overflow
:
auto
;
...
...
app/controllers/autocomplete_controller.rb
View file @
be8e2b9c
class
AutocompleteController
<
ApplicationController
skip_before_action
:authenticate_user!
,
only:
[
:users
]
AWARD_EMOJI_MAX
=
100
skip_before_action
:authenticate_user!
,
only:
[
:users
,
:award_emojis
]
before_action
:load_project
,
only:
[
:users
]
before_action
:find_users
,
only:
[
:users
]
...
...
@@ -48,6 +50,20 @@ class AutocompleteController < ApplicationController
render
json:
projects
.
to_json
(
only:
[
:id
,
:name_with_namespace
],
methods: :name_with_namespace
)
end
def
award_emojis
emoji_with_count
=
AwardEmoji
.
limit
(
AWARD_EMOJI_MAX
)
.
where
(
user:
current_user
)
.
group
(
:name
)
.
order
(
count: :desc
,
name: :asc
)
.
count
# Transform from hash to array to guarantee json order
# e.g. { 'thumbsup' => 2, 'thumbsdown' = 1 }
# => [{ name: 'thumbsup' }, { name: 'thumbsdown' }]
render
json:
emoji_with_count
.
map
{
|
k
,
v
|
{
name:
k
}
}
end
private
def
find_users
...
...
app/finders/issuable_finder.rb
View file @
be8e2b9c
...
...
@@ -18,6 +18,7 @@
# sort: string
# non_archived: boolean
# iids: integer[]
# my_reaction_emoji: string
#
class
IssuableFinder
include
CreatedAtFilter
...
...
@@ -46,6 +47,7 @@ class IssuableFinder
items
=
by_iids
(
items
)
items
=
by_milestone
(
items
)
items
=
by_label
(
items
)
items
=
by_my_reaction_emoji
(
items
)
# Filtering by project HAS TO be the last because we use the project IDs yielded by the issuable query thus far
items
=
by_project
(
items
)
...
...
@@ -371,6 +373,14 @@ class IssuableFinder
items
end
def
by_my_reaction_emoji
(
items
)
if
params
[
:my_reaction_emoji
].
present?
&&
current_user
items
=
items
.
awarded
(
current_user
,
params
[
:my_reaction_emoji
])
end
items
end
def
by_due_date
(
items
)
if
due_date?
if
filter_by_no_due_date?
...
...
app/models/concerns/awardable.rb
View file @
be8e2b9c
...
...
@@ -11,6 +11,21 @@ module Awardable
end
module
ClassMethods
def
awarded
(
user
,
name
)
sql
=
<<~
EOL
EXISTS (
SELECT TRUE
FROM award_emoji
WHERE user_id = :user_id AND
name = :name AND
awardable_type = :awardable_type AND
awardable_id =
#{
self
.
arel_table
.
name
}
.id
)
EOL
where
(
sql
,
user_id:
user
.
id
,
name:
name
,
awardable_type:
self
.
name
)
end
def
order_upvotes_desc
order_votes_desc
(
AwardEmoji
::
UPVOTE_NAME
)
end
...
...
app/views/shared/issuable/_search_bar.html.haml
View file @
be8e2b9c
...
...
@@ -93,6 +93,13 @@
%span
.dropdown-label-box
{
style:
'
background:
{{
color
}}
'
}
%span
.label-title.js-data-value
{{title}}
#js-dropdown-my-reaction
.filtered-search-input-dropdown-menu.dropdown-menu
%ul
.filter-dropdown
{
data:
{
dynamic:
true
,
dropdown:
true
}
}
%li
.filter-dropdown-item
%button
.btn.btn-link
%gl-emoji
%span
.js-data-value.prepend-left-10
{{name}}
%button
.clear-search.hidden
{
type:
'button'
}
=
icon
(
'times'
)
.filter-dropdown-container
...
...
changelogs/unreleased/add-filter-by-my-reaction.yml
0 → 100644
View file @
be8e2b9c
---
title
:
Add my reaction filter to search bar
merge_request
:
12962
author
:
Hiroyuki Sato
config/routes.rb
View file @
be8e2b9c
...
...
@@ -27,6 +27,7 @@ Rails.application.routes.draw do
get
'/autocomplete/users'
=>
'autocomplete#users'
get
'/autocomplete/users/:id'
=>
'autocomplete#user'
get
'/autocomplete/projects'
=>
'autocomplete#projects'
get
'/autocomplete/award_emojis'
=>
'autocomplete#award_emojis'
# Search
get
'search'
=>
'search#show'
...
...
spec/controllers/autocomplete_controller_spec.rb
View file @
be8e2b9c
...
...
@@ -339,4 +339,42 @@ describe AutocompleteController do
end
end
end
context
'GET award_emojis'
do
let
(
:user2
)
{
create
(
:user
)
}
let!
(
:award_emoji1
)
{
create_list
(
:award_emoji
,
2
,
user:
user
,
name:
'thumbsup'
)
}
let!
(
:award_emoji2
)
{
create_list
(
:award_emoji
,
1
,
user:
user
,
name:
'thumbsdown'
)
}
let!
(
:award_emoji3
)
{
create_list
(
:award_emoji
,
3
,
user:
user
,
name:
'star'
)
}
let!
(
:award_emoji4
)
{
create_list
(
:award_emoji
,
1
,
user:
user
,
name:
'tea'
)
}
context
'unauthorized user'
do
it
'returns empty json'
do
get
:award_emojis
expect
(
json_response
).
to
be_empty
end
end
context
'sign in as user without award emoji'
do
it
'returns empty json'
do
sign_in
(
user2
)
get
:award_emojis
expect
(
json_response
).
to
be_empty
end
end
context
'sign in as user with award emoji'
do
it
'returns json sorted by name count'
do
sign_in
(
user
)
get
:award_emojis
expect
(
json_response
.
count
).
to
eq
4
expect
(
json_response
[
0
]).
to
match
(
'name'
=>
'star'
)
expect
(
json_response
[
1
]).
to
match
(
'name'
=>
'thumbsup'
)
expect
(
json_response
[
2
]).
to
match
(
'name'
=>
'tea'
)
expect
(
json_response
[
3
]).
to
match
(
'name'
=>
'thumbsdown'
)
end
end
end
end
spec/features/issues/filtered_search/dropdown_assignee_spec.rb
View file @
be8e2b9c
...
...
@@ -204,6 +204,12 @@ describe 'Dropdown assignee', :js do
expect
(
page
).
to
have_css
(
js_dropdown_assignee
,
visible:
true
)
end
it
'opens assignee dropdown with existing my-reaction'
do
filtered_search
.
set
(
'my-reaction:star assignee:'
)
expect
(
page
).
to
have_css
(
js_dropdown_assignee
,
visible:
true
)
end
end
describe
'caching requests'
do
...
...
spec/features/issues/filtered_search/dropdown_emoji_spec.rb
0 → 100644
View file @
be8e2b9c
require
'rails_helper'
describe
'Dropdown emoji'
,
js:
true
do
include
FilteredSearchHelpers
let!
(
:project
)
{
create
(
:project
,
:public
)
}
let!
(
:user
)
{
create
(
:user
,
name:
'administrator'
,
username:
'root'
)
}
let!
(
:issue
)
{
create
(
:issue
,
project:
project
)
}
let!
(
:award_emoji_star
)
{
create
(
:award_emoji
,
name:
'star'
,
user:
user
,
awardable:
issue
)
}
let
(
:filtered_search
)
{
find
(
'.filtered-search'
)
}
let
(
:js_dropdown_emoji
)
{
'#js-dropdown-my-reaction'
}
def
send_keys_to_filtered_search
(
input
)
input
.
split
(
""
).
each
do
|
i
|
filtered_search
.
send_keys
(
i
)
end
sleep
0.5
wait_for_requests
end
def
dropdown_emoji_size
page
.
all
(
'#js-dropdown-my-reaction .filter-dropdown .filter-dropdown-item'
).
size
end
def
click_emoji
(
text
)
find
(
'#js-dropdown-my-reaction .filter-dropdown .filter-dropdown-item'
,
text:
text
).
click
end
before
do
project
.
team
<<
[
user
,
:master
]
create_list
(
:award_emoji
,
2
,
user:
user
,
name:
'thumbsup'
)
create_list
(
:award_emoji
,
1
,
user:
user
,
name:
'thumbsdown'
)
create_list
(
:award_emoji
,
3
,
user:
user
,
name:
'star'
)
create_list
(
:award_emoji
,
1
,
user:
user
,
name:
'tea'
)
end
context
'when user not logged in'
do
before
do
visit
project_issues_path
(
project
)
end
describe
'behavior'
do
it
'does not open when the search bar has my-reaction:'
do
filtered_search
.
set
(
'my-reaction:'
)
expect
(
page
).
not_to
have_css
(
js_dropdown_emoji
)
end
end
end
context
'when user loggged in'
do
before
do
sign_in
(
user
)
visit
project_issues_path
(
project
)
end
describe
'behavior'
do
it
'opens when the search bar has my-reaction:'
do
filtered_search
.
set
(
'my-reaction:'
)
expect
(
page
).
to
have_css
(
js_dropdown_emoji
,
visible:
true
)
end
it
'closes when the search bar is unfocused'
do
find
(
'body'
).
click
()
expect
(
page
).
to
have_css
(
js_dropdown_emoji
,
visible:
false
)
end
it
'should show loading indicator when opened'
do
filtered_search
.
set
(
'my-reaction:'
)
expect
(
page
).
to
have_css
(
'#js-dropdown-my-reaction .filter-dropdown-loading'
,
visible:
true
)
end
it
'should hide loading indicator when loaded'
do
send_keys_to_filtered_search
(
'my-reaction:'
)
expect
(
page
).
not_to
have_css
(
'#js-dropdown-my-reaction .filter-dropdown-loading'
)
end
it
'should load all the emojis when opened'
do
send_keys_to_filtered_search
(
'my-reaction:'
)
expect
(
dropdown_emoji_size
).
to
eq
(
4
)
end
it
'shows the most populated emoji at top of dropdown'
do
send_keys_to_filtered_search
(
'my-reaction:'
)
expect
(
first
(
'#js-dropdown-my-reaction li'
)).
to
have_content
(
award_emoji_star
.
name
)
end
end
describe
'filtering'
do
before
do
filtered_search
.
set
(
'my-reaction'
)
send_keys_to_filtered_search
(
':'
)
end
it
'filters by name'
do
send_keys_to_filtered_search
(
'up'
)
expect
(
dropdown_emoji_size
).
to
eq
(
1
)
end
it
'filters by case insensitive name'
do
send_keys_to_filtered_search
(
'Up'
)
expect
(
dropdown_emoji_size
).
to
eq
(
1
)
end
end
describe
'selecting from dropdown'
do
before
do
filtered_search
.
set
(
'my-reaction'
)
send_keys_to_filtered_search
(
':'
)
end
it
'fills in the my-reaction name'
do
click_emoji
(
'thumbsup'
)
wait_for_requests
expect
(
page
).
to
have_css
(
js_dropdown_emoji
,
visible:
false
)
expect_tokens
([
emoji_token
(
'thumbsup'
)])
expect_filtered_search_input_empty
end
end
describe
'input has existing content'
do
it
'opens my-reaction dropdown with existing search term'
do
filtered_search
.
set
(
'searchTerm my-reaction:'
)
expect
(
page
).
to
have_css
(
js_dropdown_emoji
,
visible:
true
)
end
it
'opens my-reaction dropdown with existing assignee'
do
filtered_search
.
set
(
'assignee:@user my-reaction:'
)
expect
(
page
).
to
have_css
(
js_dropdown_emoji
,
visible:
true
)
end
it
'opens my-reaction dropdown with existing label'
do
filtered_search
.
set
(
'label:~bug my-reaction:'
)
expect
(
page
).
to
have_css
(
js_dropdown_emoji
,
visible:
true
)
end
it
'opens my-reaction dropdown with existing milestone'
do
filtered_search
.
set
(
'milestone:%v1.0 my-reaction:'
)
expect
(
page
).
to
have_css
(
js_dropdown_emoji
,
visible:
true
)
end
it
'opens my-reaction dropdown with existing my-reaction'
do
filtered_search
.
set
(
'my-reaction:star my-reaction:'
)
expect
(
page
).
to
have_css
(
js_dropdown_emoji
,
visible:
true
)
end
end
describe
'caching requests'
do
it
'caches requests after the first load'
do
filtered_search
.
set
(
'my-reaction'
)
send_keys_to_filtered_search
(
':'
)
initial_size
=
dropdown_emoji_size
expect
(
initial_size
).
to
be
>
0
create_list
(
:award_emoji
,
1
,
user:
user
,
name:
'smile'
)
find
(
'.filtered-search-box .clear-search'
).
click
filtered_search
.
set
(
'my-reaction'
)
send_keys_to_filtered_search
(
':'
)
expect
(
dropdown_emoji_size
).
to
eq
(
initial_size
)
end
end
end
end
spec/features/issues/filtered_search/dropdown_hint_spec.rb
View file @
be8e2b9c
...
...
@@ -3,7 +3,7 @@ require 'rails_helper'
describe
'Dropdown hint'
,
:js
do
include
FilteredSearchHelpers
let!
(
:project
)
{
create
(
:project
)
}
let!
(
:project
)
{
create
(
:project
,
:public
)
}
let!
(
:user
)
{
create
(
:user
)
}
let
(
:filtered_search
)
{
find
(
'.filtered-search'
)
}
let
(
:js_dropdown_hint
)
{
'#js-dropdown-hint'
}
...
...
@@ -14,8 +14,23 @@ describe 'Dropdown hint', :js do
before
do
project
.
team
<<
[
user
,
:master
]
sign_in
(
user
)
create
(
:issue
,
project:
project
)
end
context
'when user not logged in'
do
before
do
visit
project_issues_path
(
project
)
end
it
'does not exist my-reaction dropdown item'
do
expect
(
page
).
to
have_css
(
js_dropdown_hint
,
visible:
false
)
expect
(
page
).
not_to
have_content
(
'my-reaction'
)
end
end
context
'when user logged in'
do
before
do
sign_in
(
user
)
visit
project_issues_path
(
project
)
end
...
...
@@ -50,7 +65,7 @@ describe 'Dropdown hint', :js do
it
'filters with text'
do
filtered_search
.
set
(
'a'
)
expect
(
find
(
js_dropdown_hint
)).
to
have_selector
(
'.filter-dropdown .filter-dropdown-item'
,
count:
3
)
expect
(
find
(
js_dropdown_hint
)).
to
have_selector
(
'.filter-dropdown .filter-dropdown-item'
,
count:
4
)
end
end
...
...
@@ -94,6 +109,15 @@ describe 'Dropdown hint', :js do
expect_tokens
([{
name:
'label'
}])
expect_filtered_search_input_empty
end
it
'opens the emoji dropdown when you click on my-reaction'
do
click_hint
(
'my-reaction'
)
expect
(
page
).
to
have_css
(
js_dropdown_hint
,
visible:
false
)
expect
(
page
).
to
have_css
(
'#js-dropdown-my-reaction'
,
visible:
true
)
expect_tokens
([{
name:
'my-reaction'
}])
expect_filtered_search_input_empty
end
end
describe
'selecting from dropdown with some input'
do
...
...
@@ -136,6 +160,16 @@ describe 'Dropdown hint', :js do
expect_tokens
([{
name:
'label'
}])
expect_filtered_search_input_empty
end
it
'opens the emoji dropdown when you click on my-reaction'
do
filtered_search
.
set
(
'my'
)
click_hint
(
'my-reaction'
)
expect
(
page
).
to
have_css
(
js_dropdown_hint
,
visible:
false
)
expect
(
page
).
to
have_css
(
'#js-dropdown-my-reaction'
,
visible:
true
)
expect_tokens
([{
name:
'my-reaction'
}])
expect_filtered_search_input_empty
end
end
describe
'reselecting from dropdown'
do
...
...
@@ -174,5 +208,15 @@ describe 'Dropdown hint', :js do
expect_tokens
([{
name:
'label'
}])
expect_filtered_search_input_empty
end
it
'reuses existing emoji text'
do
filtered_search
.
send_keys
(
'my-reaction:'
)
filtered_search
.
send_keys
(
:backspace
)
click_hint
(
'my-reaction'
)
expect_tokens
([{
name:
'my-reaction'
}])
expect_filtered_search_input_empty
end
end
end
end
spec/features/issues/filtered_search/dropdown_label_spec.rb
View file @
be8e2b9c
...
...
@@ -270,6 +270,12 @@ describe 'Dropdown label', js: true do
expect
(
page
).
to
have_css
(
js_dropdown_label
)
end
it
'opens label dropdown with existing my-reaction'
do
filtered_search
.
set
(
'my-reaction:star label:'
)
expect
(
page
).
to
have_css
(
js_dropdown_label
)
end
end
describe
'caching requests'
do
...
...
spec/features/issues/filtered_search/dropdown_milestone_spec.rb
View file @
be8e2b9c
...
...
@@ -242,6 +242,12 @@ describe 'Dropdown milestone', :js do
expect
(
page
).
to
have_css
(
js_dropdown_milestone
,
visible:
true
)
end
it
'opens milestone dropdown with existing my-reaction'
do
filtered_search
.
set
(
'my-reaction:star milestone:'
)
expect
(
page
).
to
have_css
(
js_dropdown_milestone
,
visible:
true
)
end
end
describe
'caching requests'
do
...
...
spec/features/issues/filtered_search/search_bar_spec.rb
View file @
be8e2b9c
...
...
@@ -100,7 +100,7 @@ describe 'Search bar', js: true do
find
(
'.filtered-search-box .clear-search'
).
click
filtered_search
.
click
expect
(
find
(
'#js-dropdown-hint'
)).
to
have_selector
(
'.filter-dropdown .filter-dropdown-item'
,
count:
4
)
expect
(
find
(
'#js-dropdown-hint'
)).
to
have_selector
(
'.filter-dropdown .filter-dropdown-item'
,
count:
5
)
expect
(
get_left_style
(
find
(
'#js-dropdown-hint'
)[
'style'
])).
to
eq
(
hint_offset
)
end
end
...
...
spec/finders/issues_finder_spec.rb
View file @
be8e2b9c
...
...
@@ -10,6 +10,9 @@ describe IssuesFinder do
set
(
:issue1
)
{
create
(
:issue
,
author:
user
,
assignees:
[
user
],
project:
project1
,
milestone:
milestone
,
title:
'gitlab'
,
created_at:
1
.
week
.
ago
)
}
set
(
:issue2
)
{
create
(
:issue
,
author:
user
,
assignees:
[
user
],
project:
project2
,
description:
'gitlab'
)
}
set
(
:issue3
)
{
create
(
:issue
,
author:
user2
,
assignees:
[
user2
],
project:
project2
,
title:
'tanuki'
,
description:
'tanuki'
,
created_at:
1
.
week
.
from_now
)
}
set
(
:award_emoji1
)
{
create
(
:award_emoji
,
name:
'thumbsup'
,
user:
user
,
awardable:
issue1
)
}
set
(
:award_emoji2
)
{
create
(
:award_emoji
,
name:
'thumbsup'
,
user:
user2
,
awardable:
issue2
)
}
set
(
:award_emoji3
)
{
create
(
:award_emoji
,
name:
'thumbsdown'
,
user:
user
,
awardable:
issue3
)
}
describe
'#execute'
do
set
(
:closed_issue
)
{
create
(
:issue
,
author:
user2
,
assignees:
[
user2
],
project:
project2
,
state:
'closed'
)
}
...
...
@@ -26,6 +29,10 @@ describe IssuesFinder do
issue1
issue2
issue3
award_emoji1
award_emoji2
award_emoji3
end
context
'scope: all'
do
...
...
@@ -250,6 +257,34 @@ describe IssuesFinder do
end
end
context
'filtering by reaction name'
do
context
'user searches by "thumbsup" reaction'
do
let
(
:params
)
{
{
my_reaction_emoji:
'thumbsup'
}
}
it
'returns issues that the user thumbsup to'
do
expect
(
issues
).
to
contain_exactly
(
issue1
)
end
end
context
'user2 searches by "thumbsup" reaction'
do
let
(
:search_user
)
{
user2
}
let
(
:params
)
{
{
my_reaction_emoji:
'thumbsup'
}
}
it
'returns issues that the user2 thumbsup to'
do
expect
(
issues
).
to
contain_exactly
(
issue2
)
end
end
context
'user searches by "thumbsdown" reaction'
do
let
(
:params
)
{
{
my_reaction_emoji:
'thumbsdown'
}
}
it
'returns issues that the user thumbsdown to'
do
expect
(
issues
).
to
contain_exactly
(
issue3
)
end
end
end
context
'when the user is unauthorized'
do
let
(
:search_user
)
{
nil
}
...
...
spec/javascripts/droplab/drop_down_spec.js
View file @
be8e2b9c
...
...
@@ -351,14 +351,17 @@ describe('DropDown', function () {
describe
(
'
render
'
,
function
()
{
beforeEach
(
function
()
{
this
.
list
=
{
querySelector
:
()
=>
{}
};
this
.
list
=
{
querySelector
:
()
=>
{}
,
dispatchEvent
:
()
=>
{}
};
this
.
dropdown
=
{
renderChildren
:
()
=>
{},
list
:
this
.
list
};
this
.
renderableList
=
{};
this
.
data
=
[
0
,
1
];
this
.
customEvent
=
{};
spyOn
(
this
.
dropdown
,
'
renderChildren
'
).
and
.
callFake
(
data
=>
data
);
spyOn
(
this
.
list
,
'
querySelector
'
).
and
.
returnValue
(
this
.
renderableList
);
spyOn
(
this
.
list
,
'
dispatchEvent
'
);
spyOn
(
this
.
data
,
'
map
'
).
and
.
callThrough
();
spyOn
(
window
,
'
CustomEvent
'
).
and
.
returnValue
(
this
.
customEvent
);
DropDown
.
prototype
.
render
.
call
(
this
.
dropdown
,
this
.
data
);
});
...
...
@@ -375,6 +378,14 @@ describe('DropDown', function () {
expect
(
this
.
renderableList
.
innerHTML
).
toBe
(
'
01
'
);
});
it
(
'
should call render.dl
'
,
function
()
{
expect
(
window
.
CustomEvent
).
toHaveBeenCalledWith
(
'
render.dl
'
,
jasmine
.
any
(
Object
));
});
it
(
'
should call dispatchEvent with the customEvent
'
,
function
()
{
expect
(
this
.
list
.
dispatchEvent
).
toHaveBeenCalledWith
(
this
.
customEvent
);
});
describe
(
'
if no data argument is passed
'
,
function
()
{
beforeEach
(
function
()
{
this
.
data
.
map
.
calls
.
reset
();
...
...
@@ -394,7 +405,7 @@ describe('DropDown', function () {
describe
(
'
if no dynamic list is present
'
,
function
()
{
beforeEach
(
function
()
{
this
.
list
=
{
querySelector
:
()
=>
{}
};
this
.
list
=
{
querySelector
:
()
=>
{}
,
dispatchEvent
:
()
=>
{}
};
this
.
dropdown
=
{
renderChildren
:
()
=>
{},
list
:
this
.
list
};
this
.
data
=
[
0
,
1
];
...
...
spec/models/concerns/awardable_spec.rb
View file @
be8e2b9c
...
...
@@ -12,11 +12,9 @@ describe Awardable do
describe
"ClassMethods"
do
let!
(
:issue2
)
{
create
(
:issue
)
}
let!
(
:award_emoji2
)
{
create
(
:award_emoji
,
awardable:
issue2
)
}
before
do
create
(
:award_emoji
,
awardable:
issue2
)
end
describe
"orders"
do
it
"orders on upvotes"
do
expect
(
Issue
.
order_upvotes_desc
.
to_a
).
to
eq
[
issue2
,
issue
]
end
...
...
@@ -26,6 +24,16 @@ describe Awardable do
end
end
describe
".awarded"
do
it
"filters by user and emoji name"
do
expect
(
Issue
.
awarded
(
award_emoji
.
user
,
"thumbsup"
)).
to
be_empty
expect
(
Issue
.
awarded
(
award_emoji
.
user
,
"thumbsdown"
)).
to
eq
[
issue
]
expect
(
Issue
.
awarded
(
award_emoji2
.
user
,
"thumbsup"
)).
to
eq
[
issue2
]
expect
(
Issue
.
awarded
(
award_emoji2
.
user
,
"thumbsdown"
)).
to
be_empty
end
end
end
describe
"#upvotes"
do
it
"counts the number of upvotes"
do
expect
(
issue
.
upvotes
).
to
be
0
...
...
spec/support/filtered_search_helpers.rb
View file @
be8e2b9c
...
...
@@ -58,11 +58,17 @@ module FilteredSearchHelpers
page
.
all
(
:css
,
'.tokens-container li .selectable'
).
each_with_index
do
|
el
,
index
|
token_name
=
tokens
[
index
][
:name
]
token_value
=
tokens
[
index
][
:value
]
token_emoji
=
tokens
[
index
][
:emoji_name
]
expect
(
el
.
find
(
'.name'
)).
to
have_content
(
token_name
)
if
token_value
expect
(
el
.
find
(
'.value'
)).
to
have_content
(
token_value
)
end
# gl-emoji content is blank when the emoji unicode is not supported
if
token_emoji
selector
=
%(gl-emoji[data-name="#{token_emoji}"])
expect
(
el
.
find
(
'.value'
)).
to
have_css
(
selector
)
end
end
end
end
...
...
@@ -89,6 +95,10 @@ module FilteredSearchHelpers
create_token
(
'Label'
,
label_name
,
symbol
)
end
def
emoji_token
(
emoji_name
=
nil
)
{
name:
'My-Reaction'
,
emoji_name:
emoji_name
}
end
def
default_placeholder
'Search or filter results...'
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