提交 c8c38f94 编写于 作者: P Phil Hughes

Merge branch '36409-frontend-for-clarifying-the-usefulness-of-the-search-bar' into 'master'

Resolve "Frontend for clarifying the usefulness of the search bar"

Closes #36409

See merge request gitlab-org/gitlab-ce!20537
/* eslint-disable no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, consistent-return, object-shorthand, prefer-template, quotes, class-methods-use-this, no-lonely-if, no-else-return, vars-on-top, max-len */
/* eslint-disable no-return-assign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, consistent-return, object-shorthand, prefer-template, class-methods-use-this, no-lonely-if, vars-on-top, max-len */
import $ from 'jquery';
import { escape, throttle } from 'underscore';
import { s__, sprintf } from '~/locale';
import { getIdenticonBackgroundClass, getIdenticonTitle } from '~/helpers/avatar_helper';
import axios from './lib/utils/axios_utils';
import DropdownUtils from './filtered_search/dropdown_utils';
import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from './lib/utils/common_utils';
import {
isInGroupsPage,
isInProjectPage,
getGroupSlug,
getProjectSlug,
spriteIcon,
} from './lib/utils/common_utils';
/**
* Search input in top navigation bar.
......@@ -52,6 +61,7 @@ function setSearchOptions() {
if ($dashboardOptionsDataEl.length) {
gl.dashboardOptions = {
name: s__('SearchAutocomplete|All GitLab'),
issuesPath: $dashboardOptionsDataEl.data('issuesPath'),
mrPath: $dashboardOptionsDataEl.data('mrPath'),
};
......@@ -69,8 +79,8 @@ export default class SearchAutocomplete {
this.projectRef = projectRef || (this.optsEl.data('autocompleteProjectRef') || '');
this.dropdown = this.wrap.find('.dropdown');
this.dropdownToggle = this.wrap.find('.js-dropdown-search-toggle');
this.dropdownMenu = this.dropdown.find('.dropdown-menu');
this.dropdownContent = this.dropdown.find('.dropdown-content');
this.locationBadgeEl = this.getElement('.location-badge');
this.scopeInputEl = this.getElement('#scope');
this.searchInput = this.getElement('.search-input');
this.projectInputEl = this.getElement('#search_project_id');
......@@ -78,6 +88,7 @@ export default class SearchAutocomplete {
this.searchCodeInputEl = this.getElement('#search_code');
this.repositoryInputEl = this.getElement('#repository_ref');
this.clearInput = this.getElement('.js-clear-input');
this.scrollFadeInitialized = false;
this.saveOriginalState();
// Only when user is logged in
......@@ -98,17 +109,18 @@ export default class SearchAutocomplete {
this.onSearchInputFocus = this.onSearchInputFocus.bind(this);
this.onSearchInputKeyUp = this.onSearchInputKeyUp.bind(this);
this.onSearchInputKeyDown = this.onSearchInputKeyDown.bind(this);
this.setScrollFade = this.setScrollFade.bind(this);
}
getElement(selector) {
return this.wrap.find(selector);
}
saveOriginalState() {
return this.originalState = this.serializeState();
return (this.originalState = this.serializeState());
}
saveTextLength() {
return this.lastTextLength = this.searchInput.val().length;
return (this.lastTextLength = this.searchInput.val().length);
}
createAutocomplete() {
......@@ -117,6 +129,7 @@ export default class SearchAutocomplete {
filterable: true,
filterRemote: true,
highlight: true,
icon: true,
enterCallback: false,
filterInput: 'input#search',
search: {
......@@ -154,60 +167,87 @@ export default class SearchAutocomplete {
this.loadingSuggestions = true;
return axios.get(this.autocompletePath, {
params: {
project_id: this.projectId,
project_ref: this.projectRef,
term: term,
},
}).then((response) => {
// Hide dropdown menu if no suggestions returns
if (!response.data.length) {
this.disableAutocomplete();
return;
}
return axios
.get(this.autocompletePath, {
params: {
project_id: this.projectId,
project_ref: this.projectRef,
term: term,
},
})
.then(response => {
// Hide dropdown menu if no suggestions returns
if (!response.data.length) {
this.disableAutocomplete();
return;
}
const data = [];
// List results
let firstCategory = true;
let lastCategory;
for (let i = 0, len = response.data.length; i < len; i += 1) {
const suggestion = response.data[i];
// Add group header before list each group
if (lastCategory !== suggestion.category) {
if (!firstCategory) {
data.push('separator');
}
if (firstCategory) {
firstCategory = false;
const data = [];
// List results
let firstCategory = true;
let lastCategory;
for (let i = 0, len = response.data.length; i < len; i += 1) {
const suggestion = response.data[i];
// Add group header before list each group
if (lastCategory !== suggestion.category) {
if (!firstCategory) {
data.push('separator');
}
if (firstCategory) {
firstCategory = false;
}
data.push({
header: suggestion.category,
});
lastCategory = suggestion.category;
}
data.push({
header: suggestion.category,
id: `${suggestion.category.toLowerCase()}-${suggestion.id}`,
icon: this.getAvatar(suggestion),
category: suggestion.category,
text: suggestion.label,
url: suggestion.url,
});
lastCategory = suggestion.category;
}
data.push({
id: `${suggestion.category.toLowerCase()}-${suggestion.id}`,
category: suggestion.category,
text: suggestion.label,
url: suggestion.url,
});
}
// Add option to proceed with the search
if (data.length) {
data.push('separator');
data.push({
text: `Result name contains "${term}"`,
url: `/search?search=${term}&project_id=${this.projectInputEl.val()}&group_id=${this.groupInputEl.val()}`,
});
}
// Add option to proceed with the search
if (data.length) {
const icon = spriteIcon('search', 's16 inline-search-icon');
let template;
callback(data);
if (this.projectInputEl.val()) {
template = s__('SearchAutocomplete|in this project');
}
if (this.groupInputEl.val()) {
template = s__('SearchAutocomplete|in this group');
}
this.loadingSuggestions = false;
}).catch(() => {
this.loadingSuggestions = false;
});
data.unshift('separator');
data.unshift({
icon,
text: term,
template: s__('SearchAutocomplete|in all GitLab'),
url: `/search?search=${term}`,
});
if (template) {
data.unshift({
icon,
text: term,
template,
url: `/search?search=${term}&project_id=${this.projectInputEl.val()}&group_id=${this.groupInputEl.val()}`,
});
}
}
callback(data);
this.loadingSuggestions = false;
this.highlightFirstRow();
this.setScrollFade();
})
.catch(() => {
this.loadingSuggestions = false;
});
}
getCategoryContents() {
......@@ -236,21 +276,21 @@ export default class SearchAutocomplete {
const issueItems = [
{
text: 'Issues assigned to me',
text: s__('SearchAutocomplete|Issues assigned to me'),
url: `${issuesPath}/?assignee_id=${userId}`,
},
{
text: "Issues I've created",
text: s__("SearchAutocomplete|Issues I've created"),
url: `${issuesPath}/?author_id=${userId}`,
},
];
const mergeRequestItems = [
{
text: 'Merge requests assigned to me',
text: s__('SearchAutocomplete|Merge requests assigned to me'),
url: `${mrPath}/?assignee_id=${userId}`,
},
{
text: "Merge requests I've created",
text: s__("SearchAutocomplete|Merge requests I've created"),
url: `${mrPath}/?author_id=${userId}`,
},
];
......@@ -259,7 +299,7 @@ export default class SearchAutocomplete {
if (issuesDisabled) {
items = baseItems.concat(mergeRequestItems);
} else {
items = baseItems.concat(...issueItems, 'separator', ...mergeRequestItems);
items = baseItems.concat(...issueItems, ...mergeRequestItems);
}
return items;
}
......@@ -272,8 +312,6 @@ export default class SearchAutocomplete {
search_code: this.searchCodeInputEl.val(),
repository_ref: this.repositoryInputEl.val(),
scope: this.scopeInputEl.val(),
// Location badge
_location: this.locationBadgeEl.text(),
};
}
......@@ -283,10 +321,12 @@ export default class SearchAutocomplete {
this.searchInput.on('focus', this.onSearchInputFocus);
this.searchInput.on('blur', this.onSearchInputBlur);
this.clearInput.on('click', this.onClearInputClick);
this.locationBadgeEl.on('click', () => this.searchInput.focus());
this.dropdownContent.on('scroll', throttle(this.setScrollFade, 250));
}
enableAutocomplete() {
this.setScrollFade();
// No need to enable anything if user is not logged in
if (!gon.current_user_id) {
return;
......@@ -308,10 +348,6 @@ export default class SearchAutocomplete {
onSearchInputKeyUp(e) {
switch (e.keyCode) {
case KEYCODE.BACKSPACE:
// when trying to remove the location badge
if (this.lastTextLength === 0 && this.badgePresent()) {
this.removeLocationBadge();
}
// When removing the last character and no badge is present
if (this.lastTextLength === 1) {
this.disableAutocomplete();
......@@ -372,37 +408,13 @@ export default class SearchAutocomplete {
}
}
addLocationBadge(item) {
var badgeText, category, value;
category = item.category != null ? item.category + ": " : '';
value = item.value != null ? item.value : '';
badgeText = "" + category + value;
this.locationBadgeEl.text(badgeText).show();
return this.wrap.addClass('has-location-badge');
}
hasLocationBadge() {
return this.wrap.is('.has-location-badge');
}
restoreOriginalState() {
var i, input, inputs, len;
inputs = Object.keys(this.originalState);
for (i = 0, len = inputs.length; i < len; i += 1) {
input = inputs[i];
this.getElement("#" + input).val(this.originalState[input]);
this.getElement('#' + input).val(this.originalState[input]);
}
if (this.originalState._location === '') {
return this.locationBadgeEl.hide();
} else {
return this.addLocationBadge({
value: this.originalState._location,
});
}
}
badgePresent() {
return this.locationBadgeEl.length;
}
resetSearchState() {
......@@ -411,22 +423,11 @@ export default class SearchAutocomplete {
results = [];
for (i = 0, len = inputs.length; i < len; i += 1) {
input = inputs[i];
// _location isnt a input
if (input === '_location') {
break;
}
results.push(this.getElement("#" + input).val(''));
results.push(this.getElement('#' + input).val(''));
}
return results;
}
removeLocationBadge() {
this.locationBadgeEl.hide();
this.resetSearchState();
this.wrap.removeClass('has-location-badge');
return this.disableAutocomplete();
}
disableAutocomplete() {
if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('show')) {
this.searchInput.addClass('disabled');
......@@ -444,23 +445,57 @@ export default class SearchAutocomplete {
onClick(item, $el, e) {
if (window.location.pathname.indexOf(item.url) !== -1) {
if (!e.metaKey) e.preventDefault();
if (!this.badgePresent) {
if (item.category === 'Projects') {
this.projectInputEl.val(item.id);
this.addLocationBadge({
value: 'This project',
});
}
if (item.category === 'Groups') {
this.groupInputEl.val(item.id);
this.addLocationBadge({
value: 'This group',
});
}
if (item.category === 'Projects') {
this.projectInputEl.val(item.id);
}
if (item.category === 'Groups') {
this.groupInputEl.val(item.id);
}
$el.removeClass('is-active');
this.disableAutocomplete();
return this.searchInput.val('').focus();
}
}
highlightFirstRow() {
this.searchInput.data('glDropdown').highlightRowAtIndex(null, 0);
}
getAvatar(item) {
if (!Object.hasOwnProperty.call(item, 'avatar_url')) {
return false;
}
const { label, id } = item;
const avatarUrl = item.avatar_url;
const avatar = avatarUrl
? `<img class="search-item-avatar" src="${avatarUrl}" />`
: `<div class="s16 avatar identicon ${getIdenticonBackgroundClass(id)}">${getIdenticonTitle(
escape(label),
)}</div>`;
return avatar;
}
isScrolledUp() {
const el = this.dropdownContent[0];
const currentPosition = this.contentClientHeight + el.scrollTop;
return currentPosition < this.maxPosition;
}
initScrollFade() {
const el = this.dropdownContent[0];
this.scrollFadeInitialized = true;
this.contentClientHeight = el.clientHeight;
this.maxPosition = el.scrollHeight;
this.dropdownMenu.addClass('dropdown-content-faded-mask');
}
setScrollFade() {
this.initScrollFade();
this.dropdownMenu.toggleClass('fade-out', !this.isScrolledUp());
}
}
......@@ -3,7 +3,6 @@
*/
@mixin gitlab-theme(
$location-badge-color,
$search-and-nav-links,
$active-tab-border,
$border-and-box-shadow,
......@@ -119,12 +118,6 @@
}
}
.location-badge {
color: $location-badge-color;
background-color: rgba($search-and-nav-links, 0.1);
border-right: 1px solid $sidebar-text;
}
.search-input::placeholder {
color: rgba($search-and-nav-links, 0.8);
}
......@@ -141,10 +134,6 @@
background-color: $white-light;
}
.location-badge {
color: $gl-text-color;
}
.search-input-wrap {
.search-icon {
fill: rgba($search-and-nav-links, 0.8);
......@@ -200,7 +189,6 @@
body {
&.ui-indigo {
@include gitlab-theme(
$indigo-100,
$indigo-200,
$indigo-500,
$indigo-700,
......@@ -212,7 +200,6 @@ body {
&.ui-light-indigo {
@include gitlab-theme(
$indigo-100,
$indigo-200,
$indigo-500,
$indigo-500,
......@@ -224,7 +211,6 @@ body {
&.ui-blue {
@include gitlab-theme(
$theme-blue-100,
$theme-blue-200,
$theme-blue-500,
$theme-blue-700,
......@@ -236,7 +222,6 @@ body {
&.ui-light-blue {
@include gitlab-theme(
$theme-light-blue-100,
$theme-light-blue-200,
$theme-light-blue-500,
$theme-light-blue-500,
......@@ -248,7 +233,6 @@ body {
&.ui-green {
@include gitlab-theme(
$theme-green-100,
$theme-green-200,
$theme-green-500,
$theme-green-700,
......@@ -260,7 +244,6 @@ body {
&.ui-light-green {
@include gitlab-theme(
$theme-green-100,
$theme-green-200,
$theme-green-500,
$theme-green-500,
......@@ -272,7 +255,6 @@ body {
&.ui-red {
@include gitlab-theme(
$theme-red-100,
$theme-red-200,
$theme-red-500,
$theme-red-700,
......@@ -284,7 +266,6 @@ body {
&.ui-light-red {
@include gitlab-theme(
$theme-light-red-100,
$theme-light-red-200,
$theme-light-red-500,
$theme-light-red-500,
......@@ -296,7 +277,6 @@ body {
&.ui-dark {
@include gitlab-theme(
$theme-gray-100,
$theme-gray-200,
$theme-gray-500,
$theme-gray-700,
......@@ -308,7 +288,6 @@ body {
&.ui-light {
@include gitlab-theme(
$theme-gray-900,
$theme-gray-700,
$theme-gray-800,
$theme-gray-700,
......@@ -357,10 +336,6 @@ body {
&:hover {
background-color: $white-light;
box-shadow: inset 0 0 0 1px $blue-200;
.location-badge {
box-shadow: inset 0 0 0 1px $blue-200;
}
}
}
......@@ -373,13 +348,6 @@ body {
color: $gl-text-color;
}
}
.location-badge {
color: $theme-gray-700;
box-shadow: inset 0 0 0 1px $border-color;
background-color: $nav-badge-bg;
border-right: 0;
}
}
.nav-sidebar li.active {
......
......@@ -467,7 +467,8 @@ $award-emoji-positive-add-lines: #bb9c13;
*/
$search-input-border-color: rgba($blue-400, 0.8);
$search-input-focus-shadow-color: $dropdown-input-focus-shadow;
$search-input-width: 220px;
$search-input-width: 240px;
$search-input-active-width: 320px;
$location-badge-active-bg: $blue-500;
$location-icon-color: #e7e9ed;
......
$search-dropdown-max-height: 400px;
$search-avatar-size: 16px;
.search-results {
.search-result-row {
border-bottom: 1px solid $border-color;
......@@ -24,8 +27,9 @@
box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%);
}
input[type="checkbox"]:hover {
box-shadow: 0 0 2px 2px lighten($search-input-focus-shadow-color, 20%), 0 0 0 1px lighten($search-input-focus-shadow-color, 20%);
input[type='checkbox']:hover {
box-shadow: 0 0 2px 2px lighten($search-input-focus-shadow-color, 20%),
0 0 0 1px lighten($search-input-focus-shadow-color, 20%);
}
.search {
......@@ -40,24 +44,15 @@ input[type="checkbox"]:hover {
height: 32px;
border: 0;
border-radius: $border-radius-default;
transition: border-color ease-in-out $default-transition-duration, background-color ease-in-out $default-transition-duration;
transition: border-color ease-in-out $default-transition-duration,
background-color ease-in-out $default-transition-duration,
width ease-in-out $default-transition-duration;
&:hover {
box-shadow: none;
}
}
.location-badge {
white-space: nowrap;
height: 32px;
font-size: 12px;
margin: -4px 4px -4px -4px;
line-height: 25px;
padding: 4px 8px;
border-radius: $border-radius-default 0 0 $border-radius-default;
transition: border-color ease-in-out $default-transition-duration;
}
.search-input {
border: 0;
font-size: 14px;
......@@ -104,17 +99,28 @@ input[type="checkbox"]:hover {
}
.dropdown-header {
text-transform: uppercase;
font-size: 11px;
// Necessary because glDropdown doesn't support a second style of headers
font-weight: $gl-font-weight-bold;
// .dropdown-menu li has 1px side padding
padding: $gl-padding-8 17px;
color: $gl-text-color;
font-size: $gl-font-size;
line-height: 16px;
}
// Custom dropdown positioning
.dropdown-menu {
left: -5px;
max-height: $search-dropdown-max-height;
overflow: auto;
@include media-breakpoint-up(xl) {
width: $search-input-active-width;
}
}
.dropdown-content {
max-height: none;
max-height: $search-dropdown-max-height - 18px;
}
}
......@@ -124,6 +130,10 @@ input[type="checkbox"]:hover {
border-color: $dropdown-input-focus-border;
box-shadow: none;
@include media-breakpoint-up(xl) {
width: $search-input-active-width;
}
.search-input-wrap {
.search-icon,
.clear-icon {
......@@ -141,12 +151,6 @@ input[type="checkbox"]:hover {
color: $gl-text-color-tertiary;
}
}
.location-badge {
transition: all $default-transition-duration;
background-color: $nav-badge-bg;
border-color: $border-color;
}
}
&.has-value {
......@@ -160,10 +164,24 @@ input[type="checkbox"]:hover {
}
}
&.has-location-badge {
.search-input-wrap {
width: 68%;
}
.inline-search-icon {
position: relative;
margin-right: 4px;
color: $gl-text-color-secondary;
}
.identicon,
.search-item-avatar {
flex-basis: $search-avatar-size;
flex-shrink: 0;
margin-right: 4px;
}
.search-item-avatar {
width: $search-avatar-size;
height: $search-avatar-size;
border-radius: 50%;
border: 1px solid $avatar-border;
}
}
......
......@@ -82,16 +82,16 @@ module SearchHelper
ref = @ref || @project.repository.root_ref
[
{ category: "Current Project", label: "Files", url: project_tree_path(@project, ref) },
{ category: "Current Project", label: "Commits", url: project_commits_path(@project, ref) },
{ category: "Current Project", label: "Network", url: project_network_path(@project, ref) },
{ category: "Current Project", label: "Graph", url: project_graph_path(@project, ref) },
{ category: "Current Project", label: "Issues", url: project_issues_path(@project) },
{ category: "Current Project", label: "Merge Requests", url: project_merge_requests_path(@project) },
{ category: "Current Project", label: "Milestones", url: project_milestones_path(@project) },
{ category: "Current Project", label: "Snippets", url: project_snippets_path(@project) },
{ category: "Current Project", label: "Members", url: project_project_members_path(@project) },
{ category: "Current Project", label: "Wiki", url: project_wikis_path(@project) }
{ category: "In this project", label: "Files", url: project_tree_path(@project, ref) },
{ category: "In this project", label: "Commits", url: project_commits_path(@project, ref) },
{ category: "In this project", label: "Network", url: project_network_path(@project, ref) },
{ category: "In this project", label: "Graph", url: project_graph_path(@project, ref) },
{ category: "In this project", label: "Issues", url: project_issues_path(@project) },
{ category: "In this project", label: "Merge Requests", url: project_merge_requests_path(@project) },
{ category: "In this project", label: "Milestones", url: project_milestones_path(@project) },
{ category: "In this project", label: "Snippets", url: project_snippets_path(@project) },
{ category: "In this project", label: "Members", url: project_project_members_path(@project) },
{ category: "In this project", label: "Wiki", url: project_wikis_path(@project) }
]
else
[]
......
......@@ -6,21 +6,19 @@
- group_data_attrs = { group_path: j(@group.path), name: @group.name, issues_path: issues_group_path(j(@group.path)), mr_path: merge_requests_group_path(j(@group.path)) }
- if @project && @project.persisted?
- project_data_attrs = { project_path: j(@project.path), name: j(@project.name), issues_path: project_issues_path(@project), mr_path: project_merge_requests_path(@project), issues_disabled: !@project.issues_enabled? }
.search.search-form{ class: "#{'has-location-badge' if label.present?}" }
.search.search-form
= form_tag search_path, method: :get, class: 'form-inline' do |f|
.search-input-container
- if label.present?
.location-badge= label
.search-input-wrap
.dropdown{ data: { url: search_autocomplete_path } }
= search_field_tag 'search', nil, placeholder: _('Search'),
= search_field_tag 'search', nil, placeholder: _('Search or jump to…'),
class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options',
spellcheck: false,
tabindex: '1',
autocomplete: 'off',
data: { issues_path: issues_dashboard_path,
mr_path: merge_requests_dashboard_path },
aria: { label: _('Search') }
aria: { label: _('Search or jump to…') }
%button.hidden.js-dropdown-search-toggle{ type: 'button', data: { toggle: 'dropdown' } }
.dropdown-menu.dropdown-select
= dropdown_content do
......
---
title: UX improvements to top nav search bar
merge_request: 20537
author:
type: changed
......@@ -4606,12 +4606,39 @@ msgstr ""
msgid "Search milestones"
msgstr ""
msgid "Search or jump to…"
msgstr ""
msgid "Search project"
msgstr ""
msgid "Search users"
msgstr ""
msgid "SearchAutocomplete|All GitLab"
msgstr ""
msgid "SearchAutocomplete|Issues I've created"
msgstr ""
msgid "SearchAutocomplete|Issues assigned to me"
msgstr ""
msgid "SearchAutocomplete|Merge requests I've created"
msgstr ""
msgid "SearchAutocomplete|Merge requests assigned to me"
msgstr ""
msgid "SearchAutocomplete|in all GitLab"
msgstr ""
msgid "SearchAutocomplete|in this group"
msgstr ""
msgid "SearchAutocomplete|in this project"
msgstr ""
msgid "Seconds before reseting failure information"
msgstr ""
......
......@@ -62,10 +62,6 @@ describe 'User uses header search field' do
end
end
it 'contains location badge' do
expect(page).to have_selector('.has-location-badge')
end
context 'when clicking the search field', :js do
before do
page.find('#search').click
......
.search.search-form.has-location-badge
%form.navbar-form
.search.search-form
%form.form-inline
.search-input-container
%div.location-badge
This project
.search-input-wrap
.dropdown
%input#search.search-input.dropdown-menu-toggle
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册