summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorwinh <winnie@gitlab.com>2017-05-18 20:53:14 +0200
committerwinh <winnie@gitlab.com>2017-06-02 16:30:37 +0200
commit0583916d2d9ad19ae342a13ff2a31c9e3bb76547 (patch)
tree967b2e45b0f60de7b4a0880b35ef5408e05f6e6c
parentf032731e47f2ce1c2feb6ff866754202efb6844b (diff)
downloadgitlab-ce-winh-styled-people-search-bar.tar.gz
Style people in issuable search bar (!11402)winh-styled-people-search-bar
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_utils.js5
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js72
-rw-r--r--app/assets/stylesheets/framework/filters.scss1
-rw-r--r--changelogs/unreleased/winh-styled-people-search-bar.yml4
-rw-r--r--spec/features/boards/modal_filter_spec.rb4
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/visual_tokens_spec.rb4
-rw-r--r--spec/javascripts/filtered_search/dropdown_utils_spec.js29
-rw-r--r--spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js125
-rw-r--r--spec/javascripts/helpers/filtered_search_spec_helper.js13
10 files changed, 237 insertions, 22 deletions
diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js
index 5c02a7a53d3..ef8fe071012 100644
--- a/app/assets/javascripts/filtered_search/dropdown_utils.js
+++ b/app/assets/javascripts/filtered_search/dropdown_utils.js
@@ -102,10 +102,13 @@ class DropdownUtils {
if (token.classList.contains('js-visual-token')) {
const name = token.querySelector('.name');
const value = token.querySelector('.value');
+ const valueContainer = token.querySelector('.value-container');
const symbol = value && value.dataset.symbol ? value.dataset.symbol : '';
let valueText = '';
- if (value && value.innerText) {
+ if (valueContainer && valueContainer.dataset.originalValue) {
+ valueText = valueContainer.dataset.originalValue;
+ } else if (value && value.innerText) {
valueText = value.innerText;
}
diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
index bc1226f5879..e9278140af0 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
@@ -1,6 +1,7 @@
-import AjaxCache from '~/lib/utils/ajax_cache';
-import '~/flash'; /* global Flash */
+import AjaxCache from '../lib/utils/ajax_cache';
+import '../flash'; /* global Flash */
import FilteredSearchContainer from './container';
+import UsersCache from '../lib/utils/users_cache';
class FilteredSearchVisualTokens {
static getLastVisualTokenBeforeInput() {
@@ -82,12 +83,42 @@ class FilteredSearchVisualTokens {
.catch(() => new Flash('An error occurred while fetching label colors.'));
}
+ static updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) {
+ if (tokenValue === 'none') {
+ return Promise.resolve();
+ }
+
+ const username = tokenValue.replace(/^@/, '');
+ return UsersCache.retrieve(username)
+ .then((user) => {
+ if (!user) {
+ return;
+ }
+
+ /* eslint-disable no-param-reassign */
+ tokenValueContainer.dataset.originalValue = tokenValue;
+ tokenValueElement.innerHTML = `
+ <img class="avatar s20" src="${user.avatar_url}" alt="${user.name}'s avatar">
+ ${user.name}
+ `;
+ /* eslint-enable no-param-reassign */
+ })
+ // ignore error and leave username in the search bar
+ .catch(() => { });
+ }
+
static renderVisualTokenValue(parentElement, tokenName, tokenValue) {
const tokenValueContainer = parentElement.querySelector('.value-container');
- tokenValueContainer.querySelector('.value').innerText = tokenValue;
+ const tokenValueElement = tokenValueContainer.querySelector('.value');
+ tokenValueElement.innerText = tokenValue;
- if (tokenName.toLowerCase() === 'label') {
+ const tokenType = tokenName.toLowerCase();
+ if (tokenType === 'label') {
FilteredSearchVisualTokens.updateLabelTokenColor(tokenValueContainer, tokenValue);
+ } else if ((tokenType === 'author') || (tokenType === 'assignee')) {
+ FilteredSearchVisualTokens.updateUserTokenAppearance(
+ tokenValueContainer, tokenValueElement, tokenValue,
+ );
}
}
@@ -153,6 +184,12 @@ class FilteredSearchVisualTokens {
if (!lastVisualToken) return '';
+ const valueContainer = lastVisualToken.querySelector('.value-container');
+ const originalValue = valueContainer && valueContainer.dataset.originalValue;
+ if (originalValue) {
+ return originalValue;
+ }
+
const value = lastVisualToken.querySelector('.value');
const name = lastVisualToken.querySelector('.name');
@@ -205,17 +242,28 @@ class FilteredSearchVisualTokens {
const inputLi = input.parentElement;
tokenContainer.replaceChild(inputLi, token);
- const name = token.querySelector('.name');
- const value = token.querySelector('.value');
+ const nameElement = token.querySelector('.name');
+ let value;
- if (token.classList.contains('filtered-search-token') && value) {
- FilteredSearchVisualTokens.addFilterVisualToken(name.innerText);
- input.value = value.innerText;
- } else {
- // token is a search term
- input.value = name.innerText;
+ if (token.classList.contains('filtered-search-token')) {
+ FilteredSearchVisualTokens.addFilterVisualToken(nameElement.innerText);
+
+ const valueContainerElement = token.querySelector('.value-container');
+ value = valueContainerElement.dataset.originalValue;
+
+ if (!value) {
+ const valueElement = valueContainerElement.querySelector('.value');
+ value = valueElement.innerText;
+ }
}
+ // token is a search term
+ if (!value) {
+ value = nameElement.innerText;
+ }
+
+ input.value = value;
+
// Opens dropdown
const inputEvent = new Event('input');
input.dispatchEvent(inputEvent);
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 52c9c3c88d4..585f4871f5f 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -90,6 +90,7 @@
.filtered-search-term {
display: -webkit-flex;
display: flex;
+ flex-shrink: 0;
margin-top: 5px;
margin-bottom: 5px;
diff --git a/changelogs/unreleased/winh-styled-people-search-bar.yml b/changelogs/unreleased/winh-styled-people-search-bar.yml
new file mode 100644
index 00000000000..a088af37d8d
--- /dev/null
+++ b/changelogs/unreleased/winh-styled-people-search-bar.yml
@@ -0,0 +1,4 @@
+---
+title: Style people in issuable search bar
+merge_request: 11402
+author:
diff --git a/spec/features/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb
index ce132bfd979..b6de6143354 100644
--- a/spec/features/boards/modal_filter_spec.rb
+++ b/spec/features/boards/modal_filter_spec.rb
@@ -89,7 +89,7 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
page.within('.add-issues-modal') do
wait_for_requests
- expect(page).to have_selector('.js-visual-token', text: user2.username)
+ expect(page).to have_selector('.js-visual-token', text: user2.name)
expect(page).to have_selector('.card', count: 1)
end
end
@@ -125,7 +125,7 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
page.within('.add-issues-modal') do
wait_for_requests
- expect(page).to have_selector('.js-visual-token', text: user2.username)
+ expect(page).to have_selector('.js-visual-token', text: user2.name)
expect(page).to have_selector('.card', count: 1)
end
end
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index 7958ad7e24f..e5e4ba06b5a 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -6,7 +6,7 @@ describe 'Filter issues', js: true, feature: true do
let!(:group) { create(:group) }
let!(:project) { create(:project, group: group) }
- let!(:user) { create(:user, username: 'joe') }
+ let!(:user) { create(:user, username: 'joe', name: 'Joe') }
let!(:user2) { create(:user, username: 'jane') }
let!(:label) { create(:label, project: project) }
let!(:wontfix) { create(:label, project: project, title: "Won't fix") }
diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb
index 96e87c82d2c..dbbafc9e004 100644
--- a/spec/features/issues/filtered_search/visual_tokens_spec.rb
+++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb
@@ -2,6 +2,7 @@ require 'rails_helper'
describe 'Visual tokens', js: true, feature: true do
include FilteredSearchHelpers
+ include WaitForRequests
let!(:project) { create(:empty_project) }
let!(:user) { create(:user, name: 'administrator', username: 'root') }
@@ -70,7 +71,8 @@ describe 'Visual tokens', js: true, feature: true do
end
it 'changes value in visual token' do
- expect(first('.tokens-container .filtered-search-token .value').text).to eq("@#{user_rock.username}")
+ wait_for_requests
+ expect(first('.tokens-container .filtered-search-token .value').text).to eq("#{user_rock.name}")
end
it 'moves input to the right' do
diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js b/spec/javascripts/filtered_search/dropdown_utils_spec.js
index bb02abdeea2..f55726379f3 100644
--- a/spec/javascripts/filtered_search/dropdown_utils_spec.js
+++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js
@@ -2,8 +2,12 @@ import '~/extensions/array';
import '~/filtered_search/dropdown_utils';
import '~/filtered_search/filtered_search_tokenizer';
import '~/filtered_search/filtered_search_dropdown_manager';
+import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper';
describe('Dropdown Utils', () => {
+ const issueListFixture = 'issues/issue_list.html.raw';
+ preloadFixtures(issueListFixture);
+
describe('getEscapedText', () => {
it('should return same word when it has no space', () => {
const escaped = gl.DropdownUtils.getEscapedText('textWithoutSpace');
@@ -314,4 +318,29 @@ describe('Dropdown Utils', () => {
});
});
});
+
+ describe('getSearchQuery', () => {
+ let authorToken;
+
+ beforeEach(() => {
+ loadFixtures(issueListFixture);
+
+ authorToken = FilteredSearchSpecHelper.createFilterVisualToken('author', '@user');
+ const searchTermToken = FilteredSearchSpecHelper.createSearchVisualToken('search term');
+
+ const tokensContainer = document.querySelector('.tokens-container');
+ tokensContainer.appendChild(searchTermToken);
+ tokensContainer.appendChild(authorToken);
+ });
+
+ it('uses original value if present', () => {
+ const originalValue = 'original dance';
+ const valueContainer = authorToken.querySelector('.value-container');
+ valueContainer.dataset.originalValue = originalValue;
+
+ const searchQuery = gl.DropdownUtils.getSearchQuery();
+
+ expect(searchQuery).toBe(' search term author:original dance');
+ });
+ });
});
diff --git a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
index 39df072573e..fa4343ffbc8 100644
--- a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
@@ -1,4 +1,5 @@
import AjaxCache from '~/lib/utils/ajax_cache';
+import UsersCache from '~/lib/utils/users_cache';
import '~/filtered_search/filtered_search_visual_tokens';
import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper';
@@ -406,6 +407,22 @@ describe('Filtered Search Visual Tokens', () => {
expect(subject.getLastTokenPartial()).toEqual(value);
});
+ it('should get last token original value if available', () => {
+ const originalValue = '@user';
+ const valueContainer = authorToken.querySelector('.value-container');
+ valueContainer.dataset.originalValue = originalValue;
+ const avatar = document.createElement('img');
+ const valueElement = valueContainer.querySelector('.value');
+ valueElement.insertAdjacentElement('afterbegin', avatar);
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ authorToken.outerHTML,
+ );
+
+ const lastTokenValue = subject.getLastTokenPartial();
+
+ expect(lastTokenValue).toEqual(originalValue);
+ });
+
it('should get last token name if there is no value', () => {
const name = 'assignee';
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
@@ -534,6 +551,16 @@ describe('Filtered Search Visual Tokens', () => {
expect(input.value).toEqual('none');
});
+ it('input contains the original value if present', () => {
+ const originalValue = '@user';
+ const valueContainer = token.querySelector('.value-container');
+ valueContainer.dataset.originalValue = originalValue;
+
+ subject.editToken(token);
+
+ expect(input.value).toEqual(originalValue);
+ });
+
describe('selected token is a search term token', () => {
beforeEach(() => {
token = document.querySelector('.filtered-search-term');
@@ -633,6 +660,7 @@ describe('Filtered Search Visual Tokens', () => {
const milestoneToken = FilteredSearchSpecHelper.createFilterVisualToken('milestone', 'upcoming');
let updateLabelTokenColorSpy;
+ let updateUserTokenAppearanceSpy;
beforeEach(() => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
@@ -644,6 +672,24 @@ describe('Filtered Search Visual Tokens', () => {
spyOn(subject, 'updateLabelTokenColor');
updateLabelTokenColorSpy = subject.updateLabelTokenColor;
+
+ spyOn(subject, 'updateUserTokenAppearance');
+ updateUserTokenAppearanceSpy = subject.updateUserTokenAppearance;
+ });
+
+ it('renders a author token value element', () => {
+ const { tokenNameElement, tokenValueContainer, tokenValueElement } =
+ findElements(authorToken);
+ const tokenName = tokenNameElement.innerText;
+ const tokenValue = 'new value';
+
+ subject.renderVisualTokenValue(authorToken, tokenName, tokenValue);
+
+ expect(tokenValueElement.innerText).toBe(tokenValue);
+ expect(updateUserTokenAppearanceSpy.calls.count()).toBe(1);
+ const expectedArgs = [tokenValueContainer, tokenValueElement, tokenValue];
+ expect(updateUserTokenAppearanceSpy.calls.argsFor(0)).toEqual(expectedArgs);
+ expect(updateLabelTokenColorSpy.calls.count()).toBe(0);
});
it('renders a label token value element', () => {
@@ -658,6 +704,7 @@ describe('Filtered Search Visual Tokens', () => {
expect(updateLabelTokenColorSpy.calls.count()).toBe(1);
const expectedArgs = [tokenValueContainer, tokenValue];
expect(updateLabelTokenColorSpy.calls.argsFor(0)).toEqual(expectedArgs);
+ expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0);
});
it('renders a milestone token value element', () => {
@@ -669,6 +716,84 @@ describe('Filtered Search Visual Tokens', () => {
expect(tokenValueElement.innerText).toBe(tokenValue);
expect(updateLabelTokenColorSpy.calls.count()).toBe(0);
+ expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0);
+ });
+ });
+
+ describe('updateUserTokenAppearance', () => {
+ let usersCacheSpy;
+
+ beforeEach(() => {
+ spyOn(UsersCache, 'retrieve').and.callFake(username => usersCacheSpy(username));
+ });
+
+ it('ignores special value "none"', (done) => {
+ usersCacheSpy = (username) => {
+ expect(username).toBe('none');
+ done.fail('Should not resolve "none"!');
+ };
+ const { tokenValueContainer, tokenValueElement } = findElements(authorToken);
+
+ subject.updateUserTokenAppearance(tokenValueContainer, tokenValueElement, 'none')
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('ignores error if UsersCache throws', (done) => {
+ spyOn(window, 'Flash');
+ const dummyError = new Error('Earth rotated backwards');
+ const { tokenValueContainer, tokenValueElement } = findElements(authorToken);
+ const tokenValue = tokenValueElement.innerText;
+ usersCacheSpy = (username) => {
+ expect(`@${username}`).toBe(tokenValue);
+ return Promise.reject(dummyError);
+ };
+
+ subject.updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
+ .then(() => {
+ expect(window.Flash.calls.count()).toBe(0);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('does nothing if user cannot be found', (done) => {
+ const { tokenValueContainer, tokenValueElement } = findElements(authorToken);
+ const tokenValue = tokenValueElement.innerText;
+ usersCacheSpy = (username) => {
+ expect(`@${username}`).toBe(tokenValue);
+ return Promise.resolve(undefined);
+ };
+
+ subject.updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
+ .then(() => {
+ expect(tokenValueElement.innerText).toBe(tokenValue);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('replaces author token with avatar and display name', (done) => {
+ const dummyUser = {
+ name: 'Important Person',
+ avatar_url: 'https://host.invalid/mypics/avatar.png',
+ };
+ const { tokenValueContainer, tokenValueElement } = findElements(authorToken);
+ const tokenValue = tokenValueElement.innerText;
+ usersCacheSpy = (username) => {
+ expect(`@${username}`).toBe(tokenValue);
+ return Promise.resolve(dummyUser);
+ };
+
+ subject.updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
+ .then(() => {
+ expect(tokenValueContainer.dataset.originalValue).toBe(tokenValue);
+ expect(tokenValueElement.innerText.trim()).toBe(dummyUser.name);
+ const avatar = tokenValueElement.querySelector('img.avatar');
+ expect(avatar.src).toBe(dummyUser.avatar_url);
+ })
+ .then(done)
+ .catch(done.fail);
});
});
diff --git a/spec/javascripts/helpers/filtered_search_spec_helper.js b/spec/javascripts/helpers/filtered_search_spec_helper.js
index 0d7092a2357..8933dd5def4 100644
--- a/spec/javascripts/helpers/filtered_search_spec_helper.js
+++ b/spec/javascripts/helpers/filtered_search_spec_helper.js
@@ -30,12 +30,15 @@ export default class FilteredSearchSpecHelper {
`;
}
+ static createSearchVisualToken(name) {
+ const li = document.createElement('li');
+ li.classList.add('js-visual-token', 'filtered-search-term');
+ li.innerHTML = `<div class="name">${name}</div>`;
+ return li;
+ }
+
static createSearchVisualTokenHTML(name) {
- return `
- <li class="js-visual-token filtered-search-term">
- <div class="name">${name}</div>
- </li>
- `;
+ return FilteredSearchSpecHelper.createSearchVisualToken(name).outerHTML;
}
static createInputHTML(placeholder = '', value = '') {