Show current user immediately in issuable filters

This commit is contained in:
Winnie Hellmann 2017-06-02 07:42:18 +00:00 committed by Phil Hughes
parent 5cb8ad6c57
commit f2b34fb208
11 changed files with 266 additions and 22 deletions

View file

@ -8,7 +8,7 @@ const Keyboard = function () {
var isUpArrow = false; var isUpArrow = false;
var isDownArrow = false; var isDownArrow = false;
var removeHighlight = function removeHighlight(list) { var removeHighlight = function removeHighlight(list) {
var itemElements = Array.prototype.slice.call(list.list.querySelectorAll('li:not(.divider)'), 0); var itemElements = Array.prototype.slice.call(list.list.querySelectorAll('li:not(.divider):not(.hidden)'), 0);
var listItems = []; var listItems = [];
for(var i = 0; i < itemElements.length; i++) { for(var i = 0; i < itemElements.length; i++) {
var listItem = itemElements[i]; var listItem = itemElements[i];

View file

@ -63,6 +63,9 @@ const AjaxFilter = {
return AjaxCache.retrieve(url) return AjaxCache.retrieve(url)
.then((data) => { .then((data) => {
this._loadData(data, config); this._loadData(data, config);
if (config.onLoadingFinished) {
config.onLoadingFinished(data);
}
}) })
.catch(config.onError); .catch(config.onError);
}, },

View file

@ -18,6 +18,9 @@ class DropdownUser extends gl.FilteredSearchDropdown {
}, },
searchValueFunction: this.getSearchInput.bind(this), searchValueFunction: this.getSearchInput.bind(this),
loadingTemplate: this.loadingTemplate, loadingTemplate: this.loadingTemplate,
onLoadingFinished: () => {
this.hideCurrentUser();
},
onError() { onError() {
/* eslint-disable no-new */ /* eslint-disable no-new */
new Flash('An error occured fetching the dropdown data.'); new Flash('An error occured fetching the dropdown data.');
@ -28,6 +31,11 @@ class DropdownUser extends gl.FilteredSearchDropdown {
this.tokenKeys = tokenKeys; this.tokenKeys = tokenKeys;
} }
hideCurrentUser() {
const currenUserItem = this.dropdown.querySelector('.js-current-user');
currenUserItem.classList.add('hidden');
}
itemClicked(e) { itemClicked(e) {
super.itemClicked(e, super.itemClicked(e,
selected => selected.querySelector('.dropdown-light-content').innerText.trim()); selected => selected.querySelector('.dropdown-light-content').innerText.trim());

View file

@ -8,18 +8,28 @@ module AvatarsHelper
})) }))
end end
def user_avatar(options = {}) def user_avatar_without_link(options = {})
avatar_size = options[:size] || 16 avatar_size = options[:size] || 16
user_name = options[:user].try(:name) || options[:user_name] user_name = options[:user].try(:name) || options[:user_name]
css_class = options[:css_class] || '' css_class = options[:css_class] || ''
avatar_url = options[:url] || avatar_icon(options[:user] || options[:user_email], avatar_size)
avatar = image_tag( data_attributes = { container: 'body' }
avatar_icon(options[:user] || options[:user_email], avatar_size),
if options[:lazy]
data_attributes[:src] = avatar_url
end
image_tag(
options[:lazy] ? '' : avatar_url,
class: "avatar has-tooltip s#{avatar_size} #{css_class}", class: "avatar has-tooltip s#{avatar_size} #{css_class}",
alt: "#{user_name}'s avatar", alt: "#{user_name}'s avatar",
title: user_name, title: user_name,
data: { container: 'body' } data: data_attributes
) )
end
def user_avatar(options = {})
avatar = user_avatar_without_link(options)
if options[:user] if options[:user]
link_to(avatar, user_path(options[:user])) link_to(avatar, user_path(options[:user]))

View file

@ -46,30 +46,27 @@
%span.js-filter-tag.dropdown-light-content %span.js-filter-tag.dropdown-light-content
{{tag}} {{tag}}
#js-dropdown-author.filtered-search-input-dropdown-menu.dropdown-menu #js-dropdown-author.filtered-search-input-dropdown-menu.dropdown-menu
- if current_user
%ul{ data: { dropdown: true } }
= render 'shared/issuable/user_dropdown_item',
user: current_user
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item = render 'shared/issuable/user_dropdown_item',
%button.btn.btn-link.dropdown-user user: User.new(username: '{{username}}', name: '{{name}}'),
%img.avatar{ alt: '{{name}}\'s avatar', width: '30', data: { src: '{{avatar_url}}' } } avatar: { lazy: true, url: '{{avatar_url}}' }
.dropdown-user-details
%span
{{name}}
%span.dropdown-light-content
@{{username}}
#js-dropdown-assignee.filtered-search-input-dropdown-menu.dropdown-menu #js-dropdown-assignee.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } } %ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'none' } } %li.filter-dropdown-item{ data: { value: 'none' } }
%button.btn.btn-link %button.btn.btn-link
No Assignee No Assignee
%li.divider %li.divider
- if current_user
= render 'shared/issuable/user_dropdown_item',
user: current_user
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item = render 'shared/issuable/user_dropdown_item',
%button.btn.btn-link.dropdown-user user: User.new(username: '{{username}}', name: '{{name}}'),
%img.avatar{ alt: '{{name}}\'s avatar', width: '30', data: { src: '{{avatar_url}}' } } avatar: { lazy: true, url: '{{avatar_url}}' }
.dropdown-user-details
%span
{{name}}
%span.dropdown-light-content
@{{username}}
#js-dropdown-milestone.filtered-search-input-dropdown-menu.dropdown-menu #js-dropdown-milestone.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } } %ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'none' } } %li.filter-dropdown-item{ data: { value: 'none' } }

View file

@ -0,0 +1,11 @@
- user = local_assigns.fetch(:user)
- avatar = local_assigns.fetch(:avatar, { })
%li.filter-dropdown-item{ class: ('js-current-user' if user == current_user) }
%button.btn.btn-link.dropdown-user{ type: :button }
= user_avatar_without_link(user: user, lazy: avatar[:lazy], url: avatar[:url], size: 30)
.dropdown-user-details
%span
= user.name
%span.dropdown-light-content
= user.to_reference

View file

@ -0,0 +1,4 @@
---
title: Show current user immediately in issuable filters
merge_request: 11630
author:

View file

@ -157,6 +157,25 @@ describe 'Dropdown assignee', :feature, :js do
end end
end end
describe 'selecting from dropdown without Ajax call' do
before do
Gitlab::Testing::RequestBlockerMiddleware.block_requests!
filtered_search.set('assignee:')
end
after do
Gitlab::Testing::RequestBlockerMiddleware.allow_requests!
end
it 'selects current user' do
find('#js-dropdown-assignee .filter-dropdown-item', text: user.username).click
expect(page).to have_css(js_dropdown_assignee, visible: false)
expect_tokens([{ name: 'assignee', value: user.username }])
expect_filtered_search_input_empty
end
end
describe 'input has existing content' do describe 'input has existing content' do
it 'opens assignee dropdown with existing search term' do it 'opens assignee dropdown with existing search term' do
filtered_search.set('searchTerm assignee:') filtered_search.set('searchTerm assignee:')

View file

@ -135,6 +135,25 @@ describe 'Dropdown author', js: true, feature: true do
end end
end end
describe 'selecting from dropdown without Ajax call' do
before do
Gitlab::Testing::RequestBlockerMiddleware.block_requests!
filtered_search.set('author:')
end
after do
Gitlab::Testing::RequestBlockerMiddleware.allow_requests!
end
it 'selects current user' do
find('#js-dropdown-author .filter-dropdown-item', text: user.username).click
expect(page).to have_css(js_dropdown_author, visible: false)
expect_tokens([{ name: 'author', value: user.username }])
expect_filtered_search_input_empty
end
end
describe 'input has existing content' do describe 'input has existing content' do
it 'opens author dropdown with existing search term' do it 'opens author dropdown with existing search term' do
filtered_search.set('searchTerm author:') filtered_search.set('searchTerm author:')

View file

@ -1,6 +1,8 @@
require 'rails_helper' require 'rails_helper'
describe AvatarsHelper do describe AvatarsHelper do
include ApplicationHelper
let(:user) { create(:user) } let(:user) { create(:user) }
describe '#user_avatar' do describe '#user_avatar' do
@ -18,4 +20,103 @@ describe AvatarsHelper do
is_expected.to include(CGI.escapeHTML(user.avatar_url(size: 16))) is_expected.to include(CGI.escapeHTML(user.avatar_url(size: 16)))
end end
end end
describe '#user_avatar_without_link' do
let(:options) { { user: user } }
subject { helper.user_avatar_without_link(options) }
it 'displays user avatar' do
is_expected.to eq image_tag(
avatar_icon(user, 16),
class: 'avatar has-tooltip s16 ',
alt: "#{user.name}'s avatar",
title: user.name,
data: { container: 'body' }
)
end
context 'with css_class parameter' do
let(:options) { { user: user, css_class: '.cat-pics' } }
it 'uses provided css_class' do
is_expected.to eq image_tag(
avatar_icon(user, 16),
class: "avatar has-tooltip s16 #{options[:css_class]}",
alt: "#{user.name}'s avatar",
title: user.name,
data: { container: 'body' }
)
end
end
context 'with lazy parameter' do
let(:options) { { user: user, lazy: true } }
it 'uses data-src instead of src' do
is_expected.to eq image_tag(
'',
class: 'avatar has-tooltip s16 ',
alt: "#{user.name}'s avatar",
title: user.name,
data: { container: 'body', src: avatar_icon(user, 16) }
)
end
end
context 'with size parameter' do
let(:options) { { user: user, size: 99 } }
it 'uses provided size' do
is_expected.to eq image_tag(
avatar_icon(user, options[:size]),
class: "avatar has-tooltip s#{options[:size]} ",
alt: "#{user.name}'s avatar",
title: user.name,
data: { container: 'body' }
)
end
end
context 'with url parameter' do
let(:options) { { user: user, url: '/over/the/rainbow.png' } }
it 'uses provided url' do
is_expected.to eq image_tag(
options[:url],
class: 'avatar has-tooltip s16 ',
alt: "#{user.name}'s avatar",
title: user.name,
data: { container: 'body' }
)
end
end
context 'with user_name parameter' do
let(:options) { { user_name: 'Tinky Winky', user_email: 'no@f.un' } }
context 'with user parameter' do
let(:options) { { user: user, user_name: 'Tinky Winky' } }
it 'prefers user parameter' do
is_expected.to eq image_tag(
avatar_icon(user, 16),
class: 'avatar has-tooltip s16 ',
alt: "#{user.name}'s avatar",
title: user.name,
data: { container: 'body' }
)
end
end
it 'uses user_name and user_email parameter if user is not present' do
is_expected.to eq image_tag(
avatar_icon(options[:user_email], 16),
class: 'avatar has-tooltip s16 ',
alt: "#{options[:user_name]}'s avatar",
title: options[:user_name],
data: { container: 'body' }
)
end
end
end
end end

View file

@ -0,0 +1,72 @@
import AjaxCache from '~/lib/utils/ajax_cache';
import AjaxFilter from '~/droplab/plugins/ajax_filter';
describe('AjaxFilter', () => {
let dummyConfig;
const dummyData = 'dummy data';
let dummyList;
beforeEach(() => {
dummyConfig = {
endpoint: 'dummy endpoint',
searchKey: 'dummy search key',
};
dummyList = {
data: [],
list: document.createElement('div'),
};
AjaxFilter.hook = {
config: {
AjaxFilter: dummyConfig,
},
list: dummyList,
};
});
describe('trigger', () => {
let ajaxSpy;
beforeEach(() => {
spyOn(AjaxCache, 'retrieve').and.callFake(url => ajaxSpy(url));
spyOn(AjaxFilter, '_loadData');
dummyConfig.onLoadingFinished = jasmine.createSpy('spy');
const dynamicList = document.createElement('div');
dynamicList.dataset.dynamic = true;
dummyList.list.appendChild(dynamicList);
});
it('calls onLoadingFinished after loading data', (done) => {
ajaxSpy = (url) => {
expect(url).toBe('dummy endpoint?dummy search key=');
return Promise.resolve(dummyData);
};
AjaxFilter.trigger()
.then(() => {
expect(dummyConfig.onLoadingFinished.calls.count()).toBe(1);
})
.then(done)
.catch(done.fail);
});
it('does not call onLoadingFinished if Ajax call fails', (done) => {
const dummyError = new Error('My dummy is sick! :-(');
ajaxSpy = (url) => {
expect(url).toBe('dummy endpoint?dummy search key=');
return Promise.reject(dummyError);
};
AjaxFilter.trigger()
.then(done.fail)
.catch((error) => {
expect(error).toBe(dummyError);
expect(dummyConfig.onLoadingFinished.calls.count()).toBe(0);
})
.then(done)
.catch(done.fail);
});
});
});