Add filter by my reaction
This commit is contained in:
parent
df8ca5aaab
commit
7187395ef1
26 changed files with 696 additions and 141 deletions
|
@ -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) {
|
||||
|
|
82
app/assets/javascripts/filtered_search/dropdown_emoji.js
Normal file
82
app/assets/javascripts/filtered_search/dropdown_emoji.js
Normal file
|
@ -0,0 +1,82 @@
|
|||
/* 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;
|
|
@ -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,
|
||||
}));
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import './dropdown_emoji';
|
||||
import './dropdown_hint';
|
||||
import './dropdown_non_user';
|
||||
import './dropdown_user';
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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}`;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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: 225px;
|
||||
max-height: 260px;
|
||||
max-width: 280px;
|
||||
overflow: auto;
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
4
changelogs/unreleased/add-filter-by-my-reaction.yml
Normal file
4
changelogs/unreleased/add-filter-by-my-reaction.yml
Normal file
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Add my reaction filter to search bar
|
||||
merge_request: 12962
|
||||
author: Hiroyuki Sato
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
182
spec/features/issues/filtered_search/dropdown_emoji_spec.rb
Normal file
182
spec/features/issues/filtered_search/dropdown_emoji_spec.rb
Normal file
|
@ -0,0 +1,182 @@
|
|||
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
|
|
@ -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,165 +14,209 @@ describe 'Dropdown hint', :js do
|
|||
|
||||
before do
|
||||
project.team << [user, :master]
|
||||
sign_in(user)
|
||||
create(:issue, project: project)
|
||||
|
||||
visit project_issues_path(project)
|
||||
end
|
||||
|
||||
describe 'behavior' do
|
||||
context 'when user not logged in' do
|
||||
before do
|
||||
expect(page).to have_css(js_dropdown_hint, visible: false)
|
||||
filtered_search.click
|
||||
visit project_issues_path(project)
|
||||
end
|
||||
|
||||
it 'opens when the search bar is first focused' do
|
||||
expect(page).to have_css(js_dropdown_hint, visible: true)
|
||||
end
|
||||
|
||||
it 'closes when the search bar is unfocused' do
|
||||
find('body').click
|
||||
|
||||
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
|
||||
|
||||
describe 'filtering' do
|
||||
it 'does not filter `Press Enter or click to search`' do
|
||||
filtered_search.set('randomtext')
|
||||
|
||||
hint_dropdown = find(js_dropdown_hint)
|
||||
|
||||
expect(hint_dropdown).to have_content('Press Enter or click to search')
|
||||
expect(hint_dropdown).to have_selector('.filter-dropdown .filter-dropdown-item', count: 0)
|
||||
end
|
||||
|
||||
it 'filters with text' do
|
||||
filtered_search.set('a')
|
||||
|
||||
expect(find(js_dropdown_hint)).to have_selector('.filter-dropdown .filter-dropdown-item', count: 3)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'selecting from dropdown with no input' do
|
||||
context 'when user logged in' do
|
||||
before do
|
||||
filtered_search.click
|
||||
sign_in(user)
|
||||
|
||||
visit project_issues_path(project)
|
||||
end
|
||||
|
||||
it 'opens the author dropdown when you click on author' do
|
||||
click_hint('author')
|
||||
describe 'behavior' do
|
||||
before do
|
||||
expect(page).to have_css(js_dropdown_hint, visible: false)
|
||||
filtered_search.click
|
||||
end
|
||||
|
||||
expect(page).to have_css(js_dropdown_hint, visible: false)
|
||||
expect(page).to have_css('#js-dropdown-author', visible: true)
|
||||
expect_tokens([{ name: 'author' }])
|
||||
expect_filtered_search_input_empty
|
||||
it 'opens when the search bar is first focused' do
|
||||
expect(page).to have_css(js_dropdown_hint, visible: true)
|
||||
end
|
||||
|
||||
it 'closes when the search bar is unfocused' do
|
||||
find('body').click
|
||||
|
||||
expect(page).to have_css(js_dropdown_hint, visible: false)
|
||||
end
|
||||
end
|
||||
|
||||
it 'opens the assignee dropdown when you click on assignee' do
|
||||
click_hint('assignee')
|
||||
describe 'filtering' do
|
||||
it 'does not filter `Press Enter or click to search`' do
|
||||
filtered_search.set('randomtext')
|
||||
|
||||
expect(page).to have_css(js_dropdown_hint, visible: false)
|
||||
expect(page).to have_css('#js-dropdown-assignee', visible: true)
|
||||
expect_tokens([{ name: 'assignee' }])
|
||||
expect_filtered_search_input_empty
|
||||
hint_dropdown = find(js_dropdown_hint)
|
||||
|
||||
expect(hint_dropdown).to have_content('Press Enter or click to search')
|
||||
expect(hint_dropdown).to have_selector('.filter-dropdown .filter-dropdown-item', count: 0)
|
||||
end
|
||||
|
||||
it 'filters with text' do
|
||||
filtered_search.set('a')
|
||||
|
||||
expect(find(js_dropdown_hint)).to have_selector('.filter-dropdown .filter-dropdown-item', count: 4)
|
||||
end
|
||||
end
|
||||
|
||||
it 'opens the milestone dropdown when you click on milestone' do
|
||||
click_hint('milestone')
|
||||
describe 'selecting from dropdown with no input' do
|
||||
before do
|
||||
filtered_search.click
|
||||
end
|
||||
|
||||
expect(page).to have_css(js_dropdown_hint, visible: false)
|
||||
expect(page).to have_css('#js-dropdown-milestone', visible: true)
|
||||
expect_tokens([{ name: 'milestone' }])
|
||||
expect_filtered_search_input_empty
|
||||
it 'opens the author dropdown when you click on author' do
|
||||
click_hint('author')
|
||||
|
||||
expect(page).to have_css(js_dropdown_hint, visible: false)
|
||||
expect(page).to have_css('#js-dropdown-author', visible: true)
|
||||
expect_tokens([{ name: 'author' }])
|
||||
expect_filtered_search_input_empty
|
||||
end
|
||||
|
||||
it 'opens the assignee dropdown when you click on assignee' do
|
||||
click_hint('assignee')
|
||||
|
||||
expect(page).to have_css(js_dropdown_hint, visible: false)
|
||||
expect(page).to have_css('#js-dropdown-assignee', visible: true)
|
||||
expect_tokens([{ name: 'assignee' }])
|
||||
expect_filtered_search_input_empty
|
||||
end
|
||||
|
||||
it 'opens the milestone dropdown when you click on milestone' do
|
||||
click_hint('milestone')
|
||||
|
||||
expect(page).to have_css(js_dropdown_hint, visible: false)
|
||||
expect(page).to have_css('#js-dropdown-milestone', visible: true)
|
||||
expect_tokens([{ name: 'milestone' }])
|
||||
expect_filtered_search_input_empty
|
||||
end
|
||||
|
||||
it 'opens the label dropdown when you click on label' do
|
||||
click_hint('label')
|
||||
|
||||
expect(page).to have_css(js_dropdown_hint, visible: false)
|
||||
expect(page).to have_css('#js-dropdown-label', visible: true)
|
||||
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
|
||||
|
||||
it 'opens the label dropdown when you click on label' do
|
||||
click_hint('label')
|
||||
describe 'selecting from dropdown with some input' do
|
||||
it 'opens the author dropdown when you click on author' do
|
||||
filtered_search.set('auth')
|
||||
click_hint('author')
|
||||
|
||||
expect(page).to have_css(js_dropdown_hint, visible: false)
|
||||
expect(page).to have_css('#js-dropdown-label', visible: true)
|
||||
expect_tokens([{ name: 'label' }])
|
||||
expect_filtered_search_input_empty
|
||||
end
|
||||
end
|
||||
expect(page).to have_css(js_dropdown_hint, visible: false)
|
||||
expect(page).to have_css('#js-dropdown-author', visible: true)
|
||||
expect_tokens([{ name: 'author' }])
|
||||
expect_filtered_search_input_empty
|
||||
end
|
||||
|
||||
describe 'selecting from dropdown with some input' do
|
||||
it 'opens the author dropdown when you click on author' do
|
||||
filtered_search.set('auth')
|
||||
click_hint('author')
|
||||
it 'opens the assignee dropdown when you click on assignee' do
|
||||
filtered_search.set('assign')
|
||||
click_hint('assignee')
|
||||
|
||||
expect(page).to have_css(js_dropdown_hint, visible: false)
|
||||
expect(page).to have_css('#js-dropdown-author', visible: true)
|
||||
expect_tokens([{ name: 'author' }])
|
||||
expect_filtered_search_input_empty
|
||||
expect(page).to have_css(js_dropdown_hint, visible: false)
|
||||
expect(page).to have_css('#js-dropdown-assignee', visible: true)
|
||||
expect_tokens([{ name: 'assignee' }])
|
||||
expect_filtered_search_input_empty
|
||||
end
|
||||
|
||||
it 'opens the milestone dropdown when you click on milestone' do
|
||||
filtered_search.set('mile')
|
||||
click_hint('milestone')
|
||||
|
||||
expect(page).to have_css(js_dropdown_hint, visible: false)
|
||||
expect(page).to have_css('#js-dropdown-milestone', visible: true)
|
||||
expect_tokens([{ name: 'milestone' }])
|
||||
expect_filtered_search_input_empty
|
||||
end
|
||||
|
||||
it 'opens the label dropdown when you click on label' do
|
||||
filtered_search.set('lab')
|
||||
click_hint('label')
|
||||
|
||||
expect(page).to have_css(js_dropdown_hint, visible: false)
|
||||
expect(page).to have_css('#js-dropdown-label', visible: true)
|
||||
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
|
||||
|
||||
it 'opens the assignee dropdown when you click on assignee' do
|
||||
filtered_search.set('assign')
|
||||
click_hint('assignee')
|
||||
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(page).to have_css(js_dropdown_hint, visible: false)
|
||||
expect(page).to have_css('#js-dropdown-assignee', visible: true)
|
||||
expect_tokens([{ name: 'assignee' }])
|
||||
expect_filtered_search_input_empty
|
||||
end
|
||||
expect_tokens([{ name: 'author' }])
|
||||
expect_filtered_search_input_empty
|
||||
end
|
||||
|
||||
it 'opens the milestone dropdown when you click on milestone' do
|
||||
filtered_search.set('mile')
|
||||
click_hint('milestone')
|
||||
it 'reuses existing assignee text' do
|
||||
filtered_search.send_keys('assignee:')
|
||||
filtered_search.send_keys(:backspace)
|
||||
click_hint('assignee')
|
||||
|
||||
expect(page).to have_css(js_dropdown_hint, visible: false)
|
||||
expect(page).to have_css('#js-dropdown-milestone', visible: true)
|
||||
expect_tokens([{ name: 'milestone' }])
|
||||
expect_filtered_search_input_empty
|
||||
end
|
||||
expect_tokens([{ name: 'assignee' }])
|
||||
expect_filtered_search_input_empty
|
||||
end
|
||||
|
||||
it 'opens the label dropdown when you click on label' do
|
||||
filtered_search.set('lab')
|
||||
click_hint('label')
|
||||
it 'reuses existing milestone text' do
|
||||
filtered_search.send_keys('milestone:')
|
||||
filtered_search.send_keys(:backspace)
|
||||
click_hint('milestone')
|
||||
|
||||
expect(page).to have_css(js_dropdown_hint, visible: false)
|
||||
expect(page).to have_css('#js-dropdown-label', visible: true)
|
||||
expect_tokens([{ name: 'label' }])
|
||||
expect_filtered_search_input_empty
|
||||
end
|
||||
end
|
||||
expect_tokens([{ name: 'milestone' }])
|
||||
expect_filtered_search_input_empty
|
||||
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')
|
||||
it 'reuses existing label text' do
|
||||
filtered_search.send_keys('label:')
|
||||
filtered_search.send_keys(:backspace)
|
||||
click_hint('label')
|
||||
|
||||
expect_tokens([{ name: 'author' }])
|
||||
expect_filtered_search_input_empty
|
||||
end
|
||||
expect_tokens([{ name: 'label' }])
|
||||
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')
|
||||
it 'reuses existing emoji text' do
|
||||
filtered_search.send_keys('my-reaction:')
|
||||
filtered_search.send_keys(:backspace)
|
||||
click_hint('my-reaction')
|
||||
|
||||
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
|
||||
expect_tokens([{ name: 'my-reaction' }])
|
||||
expect_filtered_search_input_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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];
|
||||
|
||||
|
|
|
@ -12,17 +12,25 @@ 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)
|
||||
describe "orders" do
|
||||
it "orders on upvotes" do
|
||||
expect(Issue.order_upvotes_desc.to_a).to eq [issue2, issue]
|
||||
end
|
||||
|
||||
it "orders on downvotes" do
|
||||
expect(Issue.order_downvotes_desc.to_a).to eq [issue, issue2]
|
||||
end
|
||||
end
|
||||
|
||||
it "orders on upvotes" do
|
||||
expect(Issue.order_upvotes_desc.to_a).to eq [issue2, issue]
|
||||
end
|
||||
|
||||
it "orders on downvotes" do
|
||||
expect(Issue.order_downvotes_desc.to_a).to eq [issue, issue2]
|
||||
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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue