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 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];
|
||||||
|
|
|
@ -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);
|
||||||
},
|
},
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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]))
|
||||||
|
|
|
@ -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' } }
|
||||||
|
|
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
|
||||||
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:')
|
||||||
|
|
|
@ -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:')
|
||||||
|
|
|
@ -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
|
||||||
|
|
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