Show current user immediately in issuable filters
This commit is contained in:
parent
5cb8ad6c57
commit
f2b34fb208
11 changed files with 266 additions and 22 deletions
|
@ -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];
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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]))
|
||||
|
|
|
@ -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' } }
|
||||
|
|
11
app/views/shared/issuable/_user_dropdown_item.html.haml
Normal file
11
app/views/shared/issuable/_user_dropdown_item.html.haml
Normal 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
|
4
changelogs/unreleased/winh-current-user-filter.yml
Normal file
4
changelogs/unreleased/winh-current-user-filter.yml
Normal file
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Show current user immediately in issuable filters
|
||||
merge_request: 11630
|
||||
author:
|
|
@ -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:')
|
||||
|
|
|
@ -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:')
|
||||
|
|
|
@ -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
|
||||
|
|
72
spec/javascripts/droplab/plugins/ajax_filter_spec.js
Normal file
72
spec/javascripts/droplab/plugins/ajax_filter_spec.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue