提交 51c19616 编写于 作者: T Tim Zallmann

Merge branch 'winh-styled-people-search-bar' into 'master'

Style people in issuable search bar

Closes #30468

See merge request !11402
......@@ -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;
}
......
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);
......
......@@ -90,6 +90,7 @@
.filtered-search-term {
display: -webkit-flex;
display: flex;
flex-shrink: 0;
margin-top: 5px;
margin-bottom: 5px;
......@@ -239,7 +240,7 @@
width: 35px;
background-color: $white-light;
border: none;
position: absolute;
position: static;
right: 0;
height: 100%;
outline: none;
......
......@@ -26,8 +26,6 @@
%li.input-token
%input.form-control.filtered-search{ id: "filtered-search-#{type.to_s}", placeholder: 'Search or filter results...', data: { 'project-id' => @project.id, 'username-params' => @users.to_json(only: [:id, :username]), 'base-endpoint' => namespace_project_path(@project.namespace, @project) } }
= icon('filter')
%button.clear-search.hidden{ type: 'button' }
= icon('times')
#js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { action: 'submit' } }
......@@ -95,6 +93,8 @@
%span.dropdown-label-box{ style: 'background: {{color}}' }
%span.label-title.js-data-value
{{title}}
%button.clear-search.hidden{ type: 'button' }
= icon('times')
.filter-dropdown-container
- if type == :boards
- if can?(current_user, :admin_list, @project)
......
---
title: Style people in issuable search bar
merge_request: 11402
author:
......@@ -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
......
......@@ -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") }
......
......@@ -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
......
......@@ -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');
});
});
});
......@@ -36,6 +36,17 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller
render_issue(example.description, issue)
end
it 'issues/issue_list.html.raw' do |example|
create(:issue, project: project)
get :index,
namespace_id: project.namespace.to_param,
project_id: project
expect(response).to be_success
store_frontend_fixture(response, example.description)
end
private
def render_issue(fixture_file_name, issue)
......
......@@ -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 = '') {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册