gitlab-org--gitlab-foss/app/assets/javascripts/search_autocomplete.js
Jan Provaznik c1b71e2fa1 Check if at least one filter is set on dashboard
When listing issues and merge requests on dasboard page,
make sure that at least one filter is enabled.

User's id is used in search autocomplete widget instead
of username, which allows presetting user in filter dropdowns.

Related to #43246
2018-04-03 20:19:09 +02:00

462 lines
14 KiB
JavaScript

/* eslint-disable no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, no-cond-assign, consistent-return, object-shorthand, prefer-arrow-callback, func-names, space-before-function-paren, prefer-template, quotes, class-methods-use-this, no-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, max-len */
import $ from 'jquery';
import axios from './lib/utils/axios_utils';
import DropdownUtils from './filtered_search/dropdown_utils';
import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from './lib/utils/common_utils';
/**
* Search input in top navigation bar.
* On click, opens a dropdown
* As the user types it filters the results
* When the user clicks `x` button it cleans the input and closes the dropdown.
*/
const KEYCODE = {
ESCAPE: 27,
BACKSPACE: 8,
ENTER: 13,
UP: 38,
DOWN: 40,
};
function setSearchOptions() {
var $projectOptionsDataEl = $('.js-search-project-options');
var $groupOptionsDataEl = $('.js-search-group-options');
var $dashboardOptionsDataEl = $('.js-search-dashboard-options');
if ($projectOptionsDataEl.length) {
gl.projectOptions = gl.projectOptions || {};
var projectPath = $projectOptionsDataEl.data('projectPath');
gl.projectOptions[projectPath] = {
name: $projectOptionsDataEl.data('name'),
issuesPath: $projectOptionsDataEl.data('issuesPath'),
issuesDisabled: $projectOptionsDataEl.data('issuesDisabled'),
mrPath: $projectOptionsDataEl.data('mrPath'),
};
}
if ($groupOptionsDataEl.length) {
gl.groupOptions = gl.groupOptions || {};
var groupPath = $groupOptionsDataEl.data('groupPath');
gl.groupOptions[groupPath] = {
name: $groupOptionsDataEl.data('name'),
issuesPath: $groupOptionsDataEl.data('issuesPath'),
mrPath: $groupOptionsDataEl.data('mrPath'),
};
}
if ($dashboardOptionsDataEl.length) {
gl.dashboardOptions = {
issuesPath: $dashboardOptionsDataEl.data('issuesPath'),
mrPath: $dashboardOptionsDataEl.data('mrPath'),
};
}
}
export default class SearchAutocomplete {
constructor({ wrap, optsEl, autocompletePath, projectId, projectRef } = {}) {
setSearchOptions();
this.bindEventContext();
this.wrap = wrap || $('.search');
this.optsEl = optsEl || this.wrap.find('.search-autocomplete-opts');
this.autocompletePath = autocompletePath || this.optsEl.data('autocompletePath');
this.projectId = projectId || (this.optsEl.data('autocompleteProjectId') || '');
this.projectRef = projectRef || (this.optsEl.data('autocompleteProjectRef') || '');
this.dropdown = this.wrap.find('.dropdown');
this.dropdownToggle = this.wrap.find('.js-dropdown-search-toggle');
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');
this.groupInputEl = this.getElement('#group_id');
this.searchCodeInputEl = this.getElement('#search_code');
this.repositoryInputEl = this.getElement('#repository_ref');
this.clearInput = this.getElement('.js-clear-input');
this.saveOriginalState();
// Only when user is logged in
if (gon.current_user_id) {
this.createAutocomplete();
}
this.searchInput.addClass('disabled');
this.saveTextLength();
this.bindEvents();
this.dropdownToggle.dropdown();
}
// Finds an element inside wrapper element
bindEventContext() {
this.onSearchInputBlur = this.onSearchInputBlur.bind(this);
this.onClearInputClick = this.onClearInputClick.bind(this);
this.onSearchInputFocus = this.onSearchInputFocus.bind(this);
this.onSearchInputKeyUp = this.onSearchInputKeyUp.bind(this);
this.onSearchInputKeyDown = this.onSearchInputKeyDown.bind(this);
}
getElement(selector) {
return this.wrap.find(selector);
}
saveOriginalState() {
return this.originalState = this.serializeState();
}
saveTextLength() {
return this.lastTextLength = this.searchInput.val().length;
}
createAutocomplete() {
return this.searchInput.glDropdown({
filterInputBlur: false,
filterable: true,
filterRemote: true,
highlight: true,
enterCallback: false,
filterInput: 'input#search',
search: {
fields: ['text'],
},
id: this.getSearchText,
data: this.getData.bind(this),
selectable: true,
clicked: this.onClick.bind(this),
});
}
getSearchText(selectedObject, el) {
return selectedObject.id ? selectedObject.text : '';
}
getData(term, callback) {
if (!term) {
const contents = this.getCategoryContents();
if (contents) {
this.searchInput.data('glDropdown').filter.options.callback(contents);
this.enableAutocomplete();
}
return;
}
// Prevent multiple ajax calls
if (this.loadingSuggestions) {
return;
}
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;
}
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({
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()}`,
});
}
callback(data);
this.loadingSuggestions = false;
}).catch(() => {
this.loadingSuggestions = false;
});
}
getCategoryContents() {
const userId = gon.current_user_id;
const userName = gon.current_username;
const { projectOptions, groupOptions, dashboardOptions } = gl;
// Get options
let options;
if (isInGroupsPage() && groupOptions) {
options = groupOptions[getGroupSlug()];
} else if (isInProjectPage() && projectOptions) {
options = projectOptions[getProjectSlug()];
} else if (dashboardOptions) {
options = dashboardOptions;
}
const { issuesPath, mrPath, name, issuesDisabled } = options;
const baseItems = [];
if (name) {
baseItems.push({
header: `${name}`,
});
}
const issueItems = [
{
text: 'Issues assigned to me',
url: `${issuesPath}/?assignee_id=${userId}`,
},
{
text: "Issues I've created",
url: `${issuesPath}/?author_id=${userId}`,
},
];
const mergeRequestItems = [
{
text: 'Merge requests assigned to me',
url: `${mrPath}/?assignee_id=${userId}`,
},
{
text: "Merge requests I've created",
url: `${mrPath}/?author_id=${userId}`,
},
];
let items;
if (issuesDisabled) {
items = baseItems.concat(mergeRequestItems);
} else {
items = baseItems.concat(...issueItems, 'separator', ...mergeRequestItems);
}
return items;
}
serializeState() {
return {
// Search Criteria
search_project_id: this.projectInputEl.val(),
group_id: this.groupInputEl.val(),
search_code: this.searchCodeInputEl.val(),
repository_ref: this.repositoryInputEl.val(),
scope: this.scopeInputEl.val(),
// Location badge
_location: this.locationBadgeEl.text(),
};
}
bindEvents() {
this.searchInput.on('keydown', this.onSearchInputKeyDown);
this.searchInput.on('keyup', this.onSearchInputKeyUp);
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());
}
enableAutocomplete() {
// No need to enable anything if user is not logged in
if (!gon.current_user_id) {
return;
}
// If the dropdown is closed, we'll open it
if (!this.dropdown.hasClass('open')) {
this.loadingSuggestions = false;
this.dropdownToggle.dropdown('toggle');
return this.searchInput.removeClass('disabled');
}
}
// Saves last length of the entered text
onSearchInputKeyDown() {
return this.saveTextLength();
}
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();
}
// When removing any character from existin value
if (this.lastTextLength > 1) {
this.enableAutocomplete();
}
break;
case KEYCODE.ESCAPE:
this.restoreOriginalState();
break;
case KEYCODE.ENTER:
this.disableAutocomplete();
break;
case KEYCODE.UP:
case KEYCODE.DOWN:
return;
default:
// Handle the case when deleting the input value other than backspace
// e.g. Pressing ctrl + backspace or ctrl + x
if (this.searchInput.val() === '') {
this.disableAutocomplete();
} else {
// We should display the menu only when input is not empty
if (e.keyCode !== KEYCODE.ENTER) {
this.enableAutocomplete();
}
}
}
this.wrap.toggleClass('has-value', !!e.target.value);
}
onSearchInputFocus() {
this.isFocused = true;
this.wrap.addClass('search-active');
if (this.getValue() === '') {
return this.getData();
}
}
getValue() {
return this.searchInput.val();
}
onClearInputClick(e) {
e.preventDefault();
this.wrap.toggleClass('has-value', !!e.target.value);
return this.searchInput.val('').focus();
}
onSearchInputBlur(e) {
this.isFocused = false;
this.wrap.removeClass('search-active');
// If input is blank then restore state
if (this.searchInput.val() === '') {
return this.restoreOriginalState();
}
}
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]);
}
if (this.originalState._location === '') {
return this.locationBadgeEl.hide();
} else {
return this.addLocationBadge({
value: this.originalState._location,
});
}
}
badgePresent() {
return this.locationBadgeEl.length;
}
resetSearchState() {
var i, input, inputs, len, results;
inputs = Object.keys(this.originalState);
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(''));
}
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('open')) {
this.searchInput.addClass('disabled');
this.dropdown.removeClass('open').trigger('hidden.bs.dropdown');
this.restoreMenu();
}
}
restoreMenu() {
var html;
html = '<ul><li class="dropdown-menu-empty-item"><a>Loading...</a></li></ul>';
return this.dropdownContent.html(html);
}
onClick(item, $el, e) {
if (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',
});
}
}
$el.removeClass('is-active');
this.disableAutocomplete();
return this.searchInput.val('').focus();
}
}
}