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 isDownArrow = false;
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 = [];
for(var i = 0; i < itemElements.length; i++) {
var listItem = itemElements[i];

View file

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

View file

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

View file

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

View file

@ -46,30 +46,27 @@
%span.js-filter-tag.dropdown-light-content
{{tag}}
#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 } }
%li.filter-dropdown-item
%button.btn.btn-link.dropdown-user
%img.avatar{ alt: '{{name}}\'s avatar', width: '30', data: { src: '{{avatar_url}}' } }
.dropdown-user-details
%span
{{name}}
%span.dropdown-light-content
@{{username}}
= render 'shared/issuable/user_dropdown_item',
user: User.new(username: '{{username}}', name: '{{name}}'),
avatar: { lazy: true, url: '{{avatar_url}}' }
#js-dropdown-assignee.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'none' } }
%button.btn.btn-link
No Assignee
%li.divider
- if current_user
= render 'shared/issuable/user_dropdown_item',
user: current_user
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link.dropdown-user
%img.avatar{ alt: '{{name}}\'s avatar', width: '30', data: { src: '{{avatar_url}}' } }
.dropdown-user-details
%span
{{name}}
%span.dropdown-light-content
@{{username}}
= render 'shared/issuable/user_dropdown_item',
user: User.new(username: '{{username}}', name: '{{name}}'),
avatar: { lazy: true, url: '{{avatar_url}}' }
#js-dropdown-milestone.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
%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
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
it 'opens assignee dropdown with existing search term' do
filtered_search.set('searchTerm assignee:')

View file

@ -135,6 +135,25 @@ describe 'Dropdown author', js: true, feature: true do
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
it 'opens author dropdown with existing search term' do
filtered_search.set('searchTerm author:')

View file

@ -1,6 +1,8 @@
require 'rails_helper'
describe AvatarsHelper do
include ApplicationHelper
let(:user) { create(:user) }
describe '#user_avatar' do
@ -18,4 +20,103 @@ describe AvatarsHelper do
is_expected.to include(CGI.escapeHTML(user.avatar_url(size: 16)))
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

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);
});
});
});