2018-03-17 18:26:18 +05:30
|
|
|
import _ from 'underscore';
|
2018-12-13 13:39:08 +05:30
|
|
|
import { getParameterByName, getUrlParamsArray } from '~/lib/utils/common_utils';
|
2018-12-05 23:21:45 +05:30
|
|
|
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
|
2019-05-18 00:54:41 +05:30
|
|
|
import recentSearchesStorageKeys from 'ee_else_ce/filtered_search/recent_searches_storage_keys';
|
2018-03-17 18:26:18 +05:30
|
|
|
import { visitUrl } from '../lib/utils/url_utility';
|
|
|
|
import Flash from '../flash';
|
2017-08-17 22:00:37 +05:30
|
|
|
import FilteredSearchContainer from './container';
|
2018-03-27 19:54:05 +05:30
|
|
|
import RecentSearchesRoot from './recent_searches_root';
|
2017-08-17 22:00:37 +05:30
|
|
|
import RecentSearchesStore from './stores/recent_searches_store';
|
|
|
|
import RecentSearchesService from './services/recent_searches_service';
|
|
|
|
import eventHub from './event_hub';
|
2017-09-10 17:25:29 +05:30
|
|
|
import { addClassIfElementExists } from '../lib/utils/dom_utils';
|
2018-03-27 19:54:05 +05:30
|
|
|
import FilteredSearchTokenizer from './filtered_search_tokenizer';
|
|
|
|
import FilteredSearchDropdownManager from './filtered_search_dropdown_manager';
|
|
|
|
import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
|
|
|
|
import DropdownUtils from './dropdown_utils';
|
2017-08-17 22:00:37 +05:30
|
|
|
|
2018-03-27 19:54:05 +05:30
|
|
|
export default class FilteredSearchManager {
|
2018-03-17 18:26:18 +05:30
|
|
|
constructor({
|
|
|
|
page,
|
2018-03-27 19:54:05 +05:30
|
|
|
isGroup = false,
|
2018-05-09 12:01:36 +05:30
|
|
|
isGroupAncestor = true,
|
2018-03-27 19:54:05 +05:30
|
|
|
isGroupDecendent = false,
|
2018-12-05 23:21:45 +05:30
|
|
|
filteredSearchTokenKeys = IssuableFilteredSearchTokenKeys,
|
2018-03-17 18:26:18 +05:30
|
|
|
stateFiltersSelector = '.issues-state-filters',
|
|
|
|
}) {
|
2018-03-27 19:54:05 +05:30
|
|
|
this.isGroup = isGroup;
|
|
|
|
this.isGroupAncestor = isGroupAncestor;
|
|
|
|
this.isGroupDecendent = isGroupDecendent;
|
2018-03-17 18:26:18 +05:30
|
|
|
this.states = ['opened', 'closed', 'merged', 'all'];
|
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
this.page = page;
|
2017-08-17 22:00:37 +05:30
|
|
|
this.container = FilteredSearchContainer.container;
|
|
|
|
this.filteredSearchInput = this.container.querySelector('.filtered-search');
|
|
|
|
this.filteredSearchInputForm = this.filteredSearchInput.form;
|
|
|
|
this.clearSearchButton = this.container.querySelector('.clear-search');
|
|
|
|
this.tokensContainer = this.container.querySelector('.tokens-container');
|
2018-03-17 18:26:18 +05:30
|
|
|
this.filteredSearchTokenKeys = filteredSearchTokenKeys;
|
|
|
|
this.stateFiltersSelector = stateFiltersSelector;
|
2019-05-18 00:54:41 +05:30
|
|
|
|
|
|
|
const { multipleAssignees } = this.filteredSearchInput.dataset;
|
|
|
|
if (multipleAssignees && this.filteredSearchTokenKeys.enableMultipleAssignees) {
|
|
|
|
this.filteredSearchTokenKeys.enableMultipleAssignees();
|
|
|
|
}
|
2017-08-17 22:00:37 +05:30
|
|
|
|
|
|
|
this.recentSearchesStore = new RecentSearchesStore({
|
|
|
|
isLocalStorageAvailable: RecentSearchesService.isAvailable(),
|
2017-09-10 17:25:29 +05:30
|
|
|
allowedKeys: this.filteredSearchTokenKeys.getKeys(),
|
2017-08-17 22:00:37 +05:30
|
|
|
});
|
2018-12-13 13:39:08 +05:30
|
|
|
this.searchHistoryDropdownElement = document.querySelector(
|
|
|
|
'.js-filtered-search-history-dropdown',
|
|
|
|
);
|
|
|
|
const fullPath = this.searchHistoryDropdownElement
|
|
|
|
? this.searchHistoryDropdownElement.dataset.fullPath
|
|
|
|
: 'project';
|
2019-05-18 00:54:41 +05:30
|
|
|
const recentSearchesKey = `${fullPath}-${recentSearchesStorageKeys[this.page]}`;
|
2017-08-17 22:00:37 +05:30
|
|
|
this.recentSearchesService = new RecentSearchesService(recentSearchesKey);
|
2017-09-10 17:25:29 +05:30
|
|
|
}
|
2017-08-17 22:00:37 +05:30
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
setup() {
|
2017-08-17 22:00:37 +05:30
|
|
|
// Fetch recent searches from localStorage
|
2018-12-13 13:39:08 +05:30
|
|
|
this.fetchingRecentSearchesPromise = this.recentSearchesService
|
|
|
|
.fetch()
|
|
|
|
.catch(error => {
|
2017-08-17 22:00:37 +05:30
|
|
|
if (error.name === 'RecentSearchesServiceError') return undefined;
|
|
|
|
// eslint-disable-next-line no-new
|
2018-03-17 18:26:18 +05:30
|
|
|
new Flash('An error occurred while parsing recent searches');
|
2017-08-17 22:00:37 +05:30
|
|
|
// Gracefully fail to empty array
|
|
|
|
return [];
|
|
|
|
})
|
2018-12-13 13:39:08 +05:30
|
|
|
.then(searches => {
|
2017-09-10 17:25:29 +05:30
|
|
|
if (!searches) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-08-17 22:00:37 +05:30
|
|
|
// Put any searches that may have come in before
|
|
|
|
// we fetched the saved searches ahead of the already saved ones
|
|
|
|
const resultantSearches = this.recentSearchesStore.setRecentSearches(
|
|
|
|
this.recentSearchesStore.state.recentSearches.concat(searches),
|
|
|
|
);
|
|
|
|
this.recentSearchesService.save(resultantSearches);
|
|
|
|
});
|
|
|
|
|
|
|
|
if (this.filteredSearchInput) {
|
2018-03-27 19:54:05 +05:30
|
|
|
this.tokenizer = FilteredSearchTokenizer;
|
|
|
|
this.dropdownManager = new FilteredSearchDropdownManager({
|
|
|
|
baseEndpoint: this.filteredSearchInput.getAttribute('data-base-endpoint') || '',
|
|
|
|
tokenizer: this.tokenizer,
|
|
|
|
page: this.page,
|
|
|
|
isGroup: this.isGroup,
|
|
|
|
isGroupAncestor: this.isGroupAncestor,
|
2018-05-09 12:01:36 +05:30
|
|
|
isGroupDecendent: this.isGroupDecendent,
|
2018-03-27 19:54:05 +05:30
|
|
|
filteredSearchTokenKeys: this.filteredSearchTokenKeys,
|
|
|
|
});
|
2017-08-17 22:00:37 +05:30
|
|
|
|
|
|
|
this.recentSearchesRoot = new RecentSearchesRoot(
|
|
|
|
this.recentSearchesStore,
|
|
|
|
this.recentSearchesService,
|
2017-09-10 17:25:29 +05:30
|
|
|
this.searchHistoryDropdownElement,
|
2017-08-17 22:00:37 +05:30
|
|
|
);
|
|
|
|
this.recentSearchesRoot.init();
|
|
|
|
|
|
|
|
this.bindEvents();
|
|
|
|
this.loadSearchParamsFromURL();
|
|
|
|
this.dropdownManager.setDropdown();
|
|
|
|
this.cleanupWrapper = this.cleanup.bind(this);
|
|
|
|
document.addEventListener('beforeunload', this.cleanupWrapper);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
cleanup() {
|
|
|
|
this.unbindEvents();
|
|
|
|
document.removeEventListener('beforeunload', this.cleanupWrapper);
|
|
|
|
|
|
|
|
if (this.recentSearchesRoot) {
|
|
|
|
this.recentSearchesRoot.destroy();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
bindStateEvents() {
|
2018-03-17 18:26:18 +05:30
|
|
|
this.stateFilters = document.querySelector(`.container-fluid ${this.stateFiltersSelector}`);
|
2017-09-10 17:25:29 +05:30
|
|
|
|
|
|
|
if (this.stateFilters) {
|
|
|
|
this.searchStateWrapper = this.searchState.bind(this);
|
|
|
|
|
2018-12-13 13:39:08 +05:30
|
|
|
this.applyToStateFilters(filterEl => {
|
2018-03-17 18:26:18 +05:30
|
|
|
filterEl.addEventListener('click', this.searchStateWrapper);
|
|
|
|
});
|
2017-09-10 17:25:29 +05:30
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
unbindStateEvents() {
|
|
|
|
if (this.stateFilters) {
|
2018-12-13 13:39:08 +05:30
|
|
|
this.applyToStateFilters(filterEl => {
|
2018-03-17 18:26:18 +05:30
|
|
|
filterEl.removeEventListener('click', this.searchStateWrapper);
|
|
|
|
});
|
2017-09-10 17:25:29 +05:30
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-17 18:26:18 +05:30
|
|
|
applyToStateFilters(callback) {
|
2018-12-13 13:39:08 +05:30
|
|
|
this.stateFilters.querySelectorAll('a[data-state]').forEach(filterEl => {
|
2018-03-17 18:26:18 +05:30
|
|
|
if (this.states.indexOf(filterEl.dataset.state) > -1) {
|
|
|
|
callback(filterEl);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-08-17 22:00:37 +05:30
|
|
|
bindEvents() {
|
|
|
|
this.handleFormSubmit = this.handleFormSubmit.bind(this);
|
|
|
|
this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager);
|
|
|
|
this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this);
|
|
|
|
this.handleInputPlaceholderWrapper = this.handleInputPlaceholder.bind(this);
|
|
|
|
this.handleInputVisualTokenWrapper = this.handleInputVisualToken.bind(this);
|
|
|
|
this.checkForEnterWrapper = this.checkForEnter.bind(this);
|
|
|
|
this.onClearSearchWrapper = this.onClearSearch.bind(this);
|
2018-03-17 18:26:18 +05:30
|
|
|
this.checkForBackspaceWrapper = this.checkForBackspace.call(this);
|
2017-08-17 22:00:37 +05:30
|
|
|
this.removeSelectedTokenKeydownWrapper = this.removeSelectedTokenKeydown.bind(this);
|
|
|
|
this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this);
|
|
|
|
this.editTokenWrapper = this.editToken.bind(this);
|
|
|
|
this.tokenChange = this.tokenChange.bind(this);
|
|
|
|
this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this);
|
|
|
|
this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this);
|
|
|
|
this.onrecentSearchesItemSelectedWrapper = this.onrecentSearchesItemSelected.bind(this);
|
|
|
|
this.removeTokenWrapper = this.removeToken.bind(this);
|
|
|
|
|
|
|
|
this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit);
|
|
|
|
this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
|
|
|
|
this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper);
|
|
|
|
this.filteredSearchInput.addEventListener('input', this.handleInputPlaceholderWrapper);
|
|
|
|
this.filteredSearchInput.addEventListener('input', this.handleInputVisualTokenWrapper);
|
|
|
|
this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper);
|
|
|
|
this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper);
|
|
|
|
this.filteredSearchInput.addEventListener('click', this.tokenChange);
|
|
|
|
this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
|
|
|
|
this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper);
|
|
|
|
this.tokensContainer.addEventListener('click', this.removeTokenWrapper);
|
2017-09-10 17:25:29 +05:30
|
|
|
this.tokensContainer.addEventListener('click', this.editTokenWrapper);
|
2017-08-17 22:00:37 +05:30
|
|
|
this.clearSearchButton.addEventListener('click', this.onClearSearchWrapper);
|
|
|
|
document.addEventListener('click', this.unselectEditTokensWrapper);
|
|
|
|
document.addEventListener('click', this.removeInputContainerFocusWrapper);
|
|
|
|
document.addEventListener('keydown', this.removeSelectedTokenKeydownWrapper);
|
|
|
|
eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
|
2017-09-10 17:25:29 +05:30
|
|
|
|
|
|
|
this.bindStateEvents();
|
2017-08-17 22:00:37 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
unbindEvents() {
|
|
|
|
this.filteredSearchInputForm.removeEventListener('submit', this.handleFormSubmit);
|
|
|
|
this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper);
|
|
|
|
this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper);
|
|
|
|
this.filteredSearchInput.removeEventListener('input', this.handleInputPlaceholderWrapper);
|
|
|
|
this.filteredSearchInput.removeEventListener('input', this.handleInputVisualTokenWrapper);
|
|
|
|
this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper);
|
|
|
|
this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper);
|
|
|
|
this.filteredSearchInput.removeEventListener('click', this.tokenChange);
|
|
|
|
this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
|
|
|
|
this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper);
|
|
|
|
this.tokensContainer.removeEventListener('click', this.removeTokenWrapper);
|
2017-09-10 17:25:29 +05:30
|
|
|
this.tokensContainer.removeEventListener('click', this.editTokenWrapper);
|
2017-08-17 22:00:37 +05:30
|
|
|
this.clearSearchButton.removeEventListener('click', this.onClearSearchWrapper);
|
|
|
|
document.removeEventListener('click', this.unselectEditTokensWrapper);
|
|
|
|
document.removeEventListener('click', this.removeInputContainerFocusWrapper);
|
|
|
|
document.removeEventListener('keydown', this.removeSelectedTokenKeydownWrapper);
|
|
|
|
eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
|
2017-09-10 17:25:29 +05:30
|
|
|
|
|
|
|
this.unbindStateEvents();
|
2017-08-17 22:00:37 +05:30
|
|
|
}
|
|
|
|
|
2018-03-17 18:26:18 +05:30
|
|
|
checkForBackspace() {
|
|
|
|
let backspaceCount = 0;
|
|
|
|
|
|
|
|
// closure for keeping track of the number of backspace keystrokes
|
2018-12-13 13:39:08 +05:30
|
|
|
return e => {
|
2018-03-17 18:26:18 +05:30
|
|
|
// 8 = Backspace Key
|
|
|
|
// 46 = Delete Key
|
|
|
|
if (e.keyCode === 8 || e.keyCode === 46) {
|
2018-03-27 19:54:05 +05:30
|
|
|
const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
|
|
|
|
const { tokenName, tokenValue } = DropdownUtils.getVisualTokenValues(lastVisualToken);
|
2018-03-17 18:26:18 +05:30
|
|
|
const canEdit = tokenName && this.canEdit && this.canEdit(tokenName, tokenValue);
|
|
|
|
|
|
|
|
if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) {
|
|
|
|
backspaceCount += 1;
|
|
|
|
|
|
|
|
if (backspaceCount === 2) {
|
|
|
|
backspaceCount = 0;
|
2018-03-27 19:54:05 +05:30
|
|
|
this.filteredSearchInput.value = FilteredSearchVisualTokens.getLastTokenPartial();
|
|
|
|
FilteredSearchVisualTokens.removeLastTokenPartial();
|
2018-03-17 18:26:18 +05:30
|
|
|
}
|
|
|
|
}
|
2017-08-17 22:00:37 +05:30
|
|
|
|
2018-03-17 18:26:18 +05:30
|
|
|
// Reposition dropdown so that it is aligned with cursor
|
|
|
|
this.dropdownManager.updateCurrentDropdownOffset();
|
|
|
|
} else {
|
|
|
|
backspaceCount = 0;
|
2017-08-17 22:00:37 +05:30
|
|
|
}
|
2018-03-17 18:26:18 +05:30
|
|
|
};
|
2017-08-17 22:00:37 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
checkForEnter(e) {
|
|
|
|
if (e.keyCode === 38 || e.keyCode === 40) {
|
2018-11-08 19:23:39 +05:30
|
|
|
const { selectionStart } = this.filteredSearchInput;
|
2017-08-17 22:00:37 +05:30
|
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
this.filteredSearchInput.setSelectionRange(selectionStart, selectionStart);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (e.keyCode === 13) {
|
|
|
|
const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
|
|
|
|
const dropdownEl = dropdown.element;
|
|
|
|
const activeElements = dropdownEl.querySelectorAll('.droplab-item-active');
|
|
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
if (!activeElements.length) {
|
|
|
|
if (this.isHandledAsync) {
|
|
|
|
e.stopImmediatePropagation();
|
|
|
|
|
|
|
|
this.filteredSearchInput.blur();
|
|
|
|
this.dropdownManager.resetDropdowns();
|
|
|
|
} else {
|
|
|
|
// Prevent droplab from opening dropdown
|
|
|
|
this.dropdownManager.destroyDroplab();
|
|
|
|
}
|
|
|
|
|
|
|
|
this.search();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
addInputContainerFocus() {
|
2017-09-10 17:25:29 +05:30
|
|
|
addClassIfElementExists(this.filteredSearchInput.closest('.filtered-search-box'), 'focus');
|
2017-08-17 22:00:37 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
removeInputContainerFocus(e) {
|
|
|
|
const inputContainer = this.filteredSearchInput.closest('.filtered-search-box');
|
|
|
|
const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
|
|
|
|
const isElementInDynamicFilterDropdown = e.target.closest('.filter-dropdown') !== null;
|
|
|
|
const isElementInStaticFilterDropdown = e.target.closest('ul[data-dropdown]') !== null;
|
|
|
|
|
2018-12-13 13:39:08 +05:30
|
|
|
if (
|
|
|
|
!isElementInFilteredSearch &&
|
|
|
|
!isElementInDynamicFilterDropdown &&
|
|
|
|
!isElementInStaticFilterDropdown &&
|
|
|
|
inputContainer
|
|
|
|
) {
|
2017-08-17 22:00:37 +05:30
|
|
|
inputContainer.classList.remove('focus');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
removeToken(e) {
|
|
|
|
const removeButtonSelected = e.target.closest('.remove-token');
|
|
|
|
|
|
|
|
if (removeButtonSelected) {
|
|
|
|
e.preventDefault();
|
2017-09-10 17:25:29 +05:30
|
|
|
// Prevent editToken from being triggered after token is removed
|
|
|
|
e.stopImmediatePropagation();
|
2017-08-17 22:00:37 +05:30
|
|
|
|
|
|
|
const button = e.target.closest('.selectable');
|
2018-03-27 19:54:05 +05:30
|
|
|
FilteredSearchVisualTokens.selectToken(button, true);
|
2017-08-17 22:00:37 +05:30
|
|
|
this.removeSelectedToken();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
unselectEditTokens(e) {
|
|
|
|
const inputContainer = this.container.querySelector('.filtered-search-box');
|
|
|
|
const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
|
|
|
|
const isElementInFilterDropdown = e.target.closest('.filter-dropdown') !== null;
|
|
|
|
const isElementTokensContainer = e.target.classList.contains('tokens-container');
|
|
|
|
|
|
|
|
if ((!isElementInFilteredSearch && !isElementInFilterDropdown) || isElementTokensContainer) {
|
2018-03-27 19:54:05 +05:30
|
|
|
FilteredSearchVisualTokens.moveInputToTheRight();
|
2017-08-17 22:00:37 +05:30
|
|
|
this.dropdownManager.resetDropdowns();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
editToken(e) {
|
|
|
|
const token = e.target.closest('.js-visual-token');
|
2017-09-10 17:25:29 +05:30
|
|
|
const sanitizedTokenName = token && token.querySelector('.name').textContent.trim();
|
|
|
|
const canEdit = this.canEdit && this.canEdit(sanitizedTokenName);
|
2017-08-17 22:00:37 +05:30
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
if (token && canEdit) {
|
|
|
|
e.preventDefault();
|
|
|
|
e.stopPropagation();
|
2018-03-27 19:54:05 +05:30
|
|
|
FilteredSearchVisualTokens.editToken(token);
|
2017-08-17 22:00:37 +05:30
|
|
|
this.tokenChange();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
toggleClearSearchButton() {
|
2018-03-27 19:54:05 +05:30
|
|
|
const query = DropdownUtils.getSearchQuery();
|
2017-08-17 22:00:37 +05:30
|
|
|
const hidden = 'hidden';
|
|
|
|
const hasHidden = this.clearSearchButton.classList.contains(hidden);
|
|
|
|
|
|
|
|
if (query.length === 0 && !hasHidden) {
|
|
|
|
this.clearSearchButton.classList.add(hidden);
|
|
|
|
} else if (query.length && hasHidden) {
|
|
|
|
this.clearSearchButton.classList.remove(hidden);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
handleInputPlaceholder() {
|
2018-03-27 19:54:05 +05:30
|
|
|
const query = DropdownUtils.getSearchQuery();
|
2017-08-17 22:00:37 +05:30
|
|
|
const placeholder = 'Search or filter results...';
|
|
|
|
const currentPlaceholder = this.filteredSearchInput.placeholder;
|
|
|
|
|
|
|
|
if (query.length === 0 && currentPlaceholder !== placeholder) {
|
|
|
|
this.filteredSearchInput.placeholder = placeholder;
|
|
|
|
} else if (query.length > 0 && currentPlaceholder !== '') {
|
|
|
|
this.filteredSearchInput.placeholder = '';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
removeSelectedTokenKeydown(e) {
|
|
|
|
// 8 = Backspace Key
|
|
|
|
// 46 = Delete Key
|
|
|
|
if (e.keyCode === 8 || e.keyCode === 46) {
|
|
|
|
this.removeSelectedToken();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
removeSelectedToken() {
|
2018-03-27 19:54:05 +05:30
|
|
|
FilteredSearchVisualTokens.removeSelectedToken();
|
2017-08-17 22:00:37 +05:30
|
|
|
this.handleInputPlaceholder();
|
|
|
|
this.toggleClearSearchButton();
|
|
|
|
this.dropdownManager.updateCurrentDropdownOffset();
|
|
|
|
}
|
|
|
|
|
|
|
|
onClearSearch(e) {
|
|
|
|
e.preventDefault();
|
|
|
|
this.clearSearch();
|
|
|
|
}
|
|
|
|
|
|
|
|
clearSearch() {
|
|
|
|
this.filteredSearchInput.value = '';
|
|
|
|
|
|
|
|
const removeElements = [];
|
|
|
|
|
2018-12-13 13:39:08 +05:30
|
|
|
[].forEach.call(this.tokensContainer.children, t => {
|
2018-03-17 18:26:18 +05:30
|
|
|
let canClearToken = t.classList.contains('js-visual-token');
|
|
|
|
|
|
|
|
if (canClearToken) {
|
2018-03-27 19:54:05 +05:30
|
|
|
const { tokenName, tokenValue } = DropdownUtils.getVisualTokenValues(t);
|
2018-03-17 18:26:18 +05:30
|
|
|
canClearToken = this.canEdit && this.canEdit(tokenName, tokenValue);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (canClearToken) {
|
2017-08-17 22:00:37 +05:30
|
|
|
removeElements.push(t);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2018-12-13 13:39:08 +05:30
|
|
|
removeElements.forEach(el => {
|
2017-08-17 22:00:37 +05:30
|
|
|
el.parentElement.removeChild(el);
|
|
|
|
});
|
|
|
|
|
|
|
|
this.clearSearchButton.classList.add('hidden');
|
|
|
|
this.handleInputPlaceholder();
|
|
|
|
|
|
|
|
this.dropdownManager.resetDropdowns();
|
|
|
|
|
|
|
|
if (this.isHandledAsync) {
|
|
|
|
this.search();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
handleInputVisualToken() {
|
|
|
|
const input = this.filteredSearchInput;
|
2018-12-13 13:39:08 +05:30
|
|
|
const { tokens, searchToken } = this.tokenizer.processTokens(
|
|
|
|
input.value,
|
|
|
|
this.filteredSearchTokenKeys.getKeys(),
|
|
|
|
);
|
|
|
|
const { isLastVisualTokenValid } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
|
2017-08-17 22:00:37 +05:30
|
|
|
|
|
|
|
if (isLastVisualTokenValid) {
|
2018-12-13 13:39:08 +05:30
|
|
|
tokens.forEach(t => {
|
2017-08-17 22:00:37 +05:30
|
|
|
input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, '');
|
2018-12-05 23:21:45 +05:30
|
|
|
FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`, {
|
|
|
|
uppercaseTokenName: this.filteredSearchTokenKeys.shouldUppercaseTokenName(t.key),
|
|
|
|
capitalizeTokenValue: this.filteredSearchTokenKeys.shouldCapitalizeTokenValue(t.key),
|
|
|
|
});
|
2017-08-17 22:00:37 +05:30
|
|
|
});
|
|
|
|
|
|
|
|
const fragments = searchToken.split(':');
|
|
|
|
if (fragments.length > 1) {
|
|
|
|
const inputValues = fragments[0].split(' ');
|
2017-09-10 17:25:29 +05:30
|
|
|
const tokenKey = _.last(inputValues);
|
2017-08-17 22:00:37 +05:30
|
|
|
|
|
|
|
if (inputValues.length > 1) {
|
|
|
|
inputValues.pop();
|
|
|
|
const searchTerms = inputValues.join(' ');
|
|
|
|
|
|
|
|
input.value = input.value.replace(searchTerms, '');
|
2018-03-27 19:54:05 +05:30
|
|
|
FilteredSearchVisualTokens.addSearchVisualToken(searchTerms);
|
2017-08-17 22:00:37 +05:30
|
|
|
}
|
|
|
|
|
2018-12-05 23:21:45 +05:30
|
|
|
FilteredSearchVisualTokens.addFilterVisualToken(tokenKey, null, {
|
|
|
|
uppercaseTokenName: this.filteredSearchTokenKeys.shouldUppercaseTokenName(tokenKey),
|
|
|
|
capitalizeTokenValue: this.filteredSearchTokenKeys.shouldCapitalizeTokenValue(tokenKey),
|
|
|
|
});
|
2017-08-17 22:00:37 +05:30
|
|
|
input.value = input.value.replace(`${tokenKey}:`, '');
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Keep listening to token until we determine that the user is done typing the token value
|
|
|
|
const valueCompletedRegex = /([~%@]{0,1}".+")|([~%@]{0,1}'.+')|^((?![~%@]')(?![~%@]")(?!')(?!")).*/g;
|
|
|
|
|
|
|
|
if (searchToken.match(valueCompletedRegex) && input.value[input.value.length - 1] === ' ') {
|
2018-12-05 23:21:45 +05:30
|
|
|
const tokenKey = FilteredSearchVisualTokens.getLastTokenPartial();
|
|
|
|
FilteredSearchVisualTokens.addFilterVisualToken(searchToken, null, {
|
|
|
|
capitalizeTokenValue: this.filteredSearchTokenKeys.shouldCapitalizeTokenValue(tokenKey),
|
|
|
|
});
|
2017-08-17 22:00:37 +05:30
|
|
|
|
|
|
|
// Trim the last space as seen in the if statement above
|
|
|
|
input.value = input.value.replace(searchToken, '').trim();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
handleFormSubmit(e) {
|
|
|
|
e.preventDefault();
|
|
|
|
this.search();
|
|
|
|
}
|
|
|
|
|
|
|
|
saveCurrentSearchQuery() {
|
|
|
|
// Don't save before we have fetched the already saved searches
|
2018-12-13 13:39:08 +05:30
|
|
|
this.fetchingRecentSearchesPromise
|
|
|
|
.then(() => {
|
|
|
|
const searchQuery = DropdownUtils.getSearchQuery();
|
|
|
|
if (searchQuery.length > 0) {
|
|
|
|
const resultantSearches = this.recentSearchesStore.addRecentSearch(searchQuery);
|
|
|
|
this.recentSearchesService.save(resultantSearches);
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.catch(() => {
|
|
|
|
// https://gitlab.com/gitlab-org/gitlab-ce/issues/30821
|
|
|
|
});
|
2017-08-17 22:00:37 +05:30
|
|
|
}
|
|
|
|
|
2018-03-17 18:26:18 +05:30
|
|
|
// allows for modifying params array when a param can't be included in the URL (e.g. Service Desk)
|
|
|
|
getAllParams(urlParams) {
|
|
|
|
return this.modifyUrlParams ? this.modifyUrlParams(urlParams) : urlParams;
|
|
|
|
}
|
|
|
|
|
2017-08-17 22:00:37 +05:30
|
|
|
loadSearchParamsFromURL() {
|
2018-03-27 19:54:05 +05:30
|
|
|
const urlParams = getUrlParamsArray();
|
2018-03-17 18:26:18 +05:30
|
|
|
const params = this.getAllParams(urlParams);
|
2017-08-17 22:00:37 +05:30
|
|
|
const usernameParams = this.getUsernameParams();
|
|
|
|
let hasFilteredSearch = false;
|
|
|
|
|
2018-12-13 13:39:08 +05:30
|
|
|
params.forEach(p => {
|
2017-08-17 22:00:37 +05:30
|
|
|
const split = p.split('=');
|
|
|
|
const keyParam = decodeURIComponent(split[0]);
|
|
|
|
const value = split[1];
|
|
|
|
|
|
|
|
// Check if it matches edge conditions listed in this.filteredSearchTokenKeys
|
|
|
|
const condition = this.filteredSearchTokenKeys.searchByConditionUrl(p);
|
|
|
|
|
|
|
|
if (condition) {
|
|
|
|
hasFilteredSearch = true;
|
2017-09-10 17:25:29 +05:30
|
|
|
const canEdit = this.canEdit && this.canEdit(condition.tokenKey);
|
2018-12-13 13:39:08 +05:30
|
|
|
FilteredSearchVisualTokens.addFilterVisualToken(condition.tokenKey, condition.value, {
|
|
|
|
canEdit,
|
|
|
|
});
|
2017-08-17 22:00:37 +05:30
|
|
|
} else {
|
|
|
|
// Sanitize value since URL converts spaces into +
|
|
|
|
// Replace before decode so that we know what was originally + versus the encoded +
|
|
|
|
const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value;
|
|
|
|
const match = this.filteredSearchTokenKeys.searchByKeyParam(keyParam);
|
|
|
|
|
|
|
|
if (match) {
|
2019-05-18 00:54:41 +05:30
|
|
|
const { key, symbol } = match;
|
2017-08-17 22:00:37 +05:30
|
|
|
let quotationsToUse = '';
|
|
|
|
|
|
|
|
if (sanitizedValue.indexOf(' ') !== -1) {
|
|
|
|
// Prefer ", but use ' if required
|
2018-12-13 13:39:08 +05:30
|
|
|
quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : "'";
|
2017-08-17 22:00:37 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
hasFilteredSearch = true;
|
2019-05-18 00:54:41 +05:30
|
|
|
const canEdit = this.canEdit && this.canEdit(key, sanitizedValue);
|
2018-12-05 23:21:45 +05:30
|
|
|
const { uppercaseTokenName, capitalizeTokenValue } = match;
|
2018-03-27 19:54:05 +05:30
|
|
|
FilteredSearchVisualTokens.addFilterVisualToken(
|
2019-05-18 00:54:41 +05:30
|
|
|
key,
|
2017-09-10 17:25:29 +05:30
|
|
|
`${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`,
|
2018-12-05 23:21:45 +05:30
|
|
|
{
|
|
|
|
canEdit,
|
|
|
|
uppercaseTokenName,
|
|
|
|
capitalizeTokenValue,
|
|
|
|
},
|
2017-09-10 17:25:29 +05:30
|
|
|
);
|
2017-08-17 22:00:37 +05:30
|
|
|
} else if (!match && keyParam === 'assignee_id') {
|
|
|
|
const id = parseInt(value, 10);
|
|
|
|
if (usernameParams[id]) {
|
|
|
|
hasFilteredSearch = true;
|
2017-09-10 17:25:29 +05:30
|
|
|
const tokenName = 'assignee';
|
|
|
|
const canEdit = this.canEdit && this.canEdit(tokenName);
|
2018-12-13 13:39:08 +05:30
|
|
|
FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, {
|
|
|
|
canEdit,
|
|
|
|
});
|
2017-08-17 22:00:37 +05:30
|
|
|
}
|
|
|
|
} else if (!match && keyParam === 'author_id') {
|
|
|
|
const id = parseInt(value, 10);
|
|
|
|
if (usernameParams[id]) {
|
|
|
|
hasFilteredSearch = true;
|
2017-09-10 17:25:29 +05:30
|
|
|
const tokenName = 'author';
|
|
|
|
const canEdit = this.canEdit && this.canEdit(tokenName);
|
2018-12-13 13:39:08 +05:30
|
|
|
FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, {
|
|
|
|
canEdit,
|
|
|
|
});
|
2017-08-17 22:00:37 +05:30
|
|
|
}
|
|
|
|
} else if (!match && keyParam === 'search') {
|
|
|
|
hasFilteredSearch = true;
|
|
|
|
this.filteredSearchInput.value = sanitizedValue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
this.saveCurrentSearchQuery();
|
|
|
|
|
|
|
|
if (hasFilteredSearch) {
|
|
|
|
this.clearSearchButton.classList.remove('hidden');
|
|
|
|
this.handleInputPlaceholder();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
searchState(e) {
|
|
|
|
e.preventDefault();
|
|
|
|
const target = e.currentTarget;
|
|
|
|
// remove focus outline after click
|
|
|
|
target.blur();
|
|
|
|
|
|
|
|
const state = target.dataset && target.dataset.state;
|
|
|
|
|
|
|
|
if (state) {
|
|
|
|
this.search(state);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
search(state = null) {
|
2017-08-17 22:00:37 +05:30
|
|
|
const paths = [];
|
2018-03-27 19:54:05 +05:30
|
|
|
const searchQuery = DropdownUtils.getSearchQuery();
|
2017-08-17 22:00:37 +05:30
|
|
|
|
|
|
|
this.saveCurrentSearchQuery();
|
|
|
|
|
2018-12-05 23:21:45 +05:30
|
|
|
const tokenKeys = this.filteredSearchTokenKeys.getKeys();
|
|
|
|
const { tokens, searchToken } = this.tokenizer.processTokens(searchQuery, tokenKeys);
|
2018-03-27 19:54:05 +05:30
|
|
|
const currentState = state || getParameterByName('state') || 'opened';
|
2017-08-17 22:00:37 +05:30
|
|
|
paths.push(`state=${currentState}`);
|
|
|
|
|
2018-12-13 13:39:08 +05:30
|
|
|
tokens.forEach(token => {
|
|
|
|
const condition = this.filteredSearchTokenKeys.searchByConditionKeyValue(
|
|
|
|
token.key,
|
2019-03-02 22:35:43 +05:30
|
|
|
token.value,
|
2018-12-13 13:39:08 +05:30
|
|
|
);
|
2018-12-05 23:21:45 +05:30
|
|
|
const tokenConfig = this.filteredSearchTokenKeys.searchByKey(token.key) || {};
|
|
|
|
const { param } = tokenConfig;
|
|
|
|
|
2018-03-17 18:26:18 +05:30
|
|
|
// Replace hyphen with underscore to use as request parameter
|
|
|
|
// e.g. 'my-reaction' => 'my_reaction'
|
|
|
|
const underscoredKey = token.key.replace('-', '_');
|
|
|
|
const keyParam = param ? `${underscoredKey}_${param}` : underscoredKey;
|
2017-08-17 22:00:37 +05:30
|
|
|
let tokenPath = '';
|
|
|
|
|
|
|
|
if (condition) {
|
|
|
|
tokenPath = condition.url;
|
|
|
|
} else {
|
|
|
|
let tokenValue = token.value;
|
|
|
|
|
2018-12-05 23:21:45 +05:30
|
|
|
if (tokenConfig.lowercaseValueOnSubmit) {
|
|
|
|
tokenValue = tokenValue.toLowerCase();
|
|
|
|
}
|
|
|
|
|
2018-12-13 13:39:08 +05:30
|
|
|
if (
|
|
|
|
(tokenValue[0] === "'" && tokenValue[tokenValue.length - 1] === "'") ||
|
|
|
|
(tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')
|
|
|
|
) {
|
2017-08-17 22:00:37 +05:30
|
|
|
tokenValue = tokenValue.slice(1, tokenValue.length - 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
tokenPath = `${keyParam}=${encodeURIComponent(tokenValue)}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
paths.push(tokenPath);
|
|
|
|
});
|
|
|
|
|
|
|
|
if (searchToken) {
|
2018-12-13 13:39:08 +05:30
|
|
|
const sanitized = searchToken
|
|
|
|
.split(' ')
|
|
|
|
.map(t => encodeURIComponent(t))
|
|
|
|
.join('+');
|
2017-08-17 22:00:37 +05:30
|
|
|
paths.push(`search=${sanitized}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
const parameterizedUrl = `?scope=all&utf8=%E2%9C%93&${paths.join('&')}`;
|
|
|
|
|
|
|
|
if (this.updateObject) {
|
|
|
|
this.updateObject(parameterizedUrl);
|
|
|
|
} else {
|
2018-03-17 18:26:18 +05:30
|
|
|
visitUrl(parameterizedUrl);
|
2017-08-17 22:00:37 +05:30
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
getUsernameParams() {
|
|
|
|
const usernamesById = {};
|
|
|
|
try {
|
|
|
|
const attribute = this.filteredSearchInput.getAttribute('data-username-params');
|
2018-12-13 13:39:08 +05:30
|
|
|
JSON.parse(attribute).forEach(user => {
|
2017-08-17 22:00:37 +05:30
|
|
|
usernamesById[user.id] = user.username;
|
|
|
|
});
|
|
|
|
} catch (e) {
|
|
|
|
// do nothing
|
|
|
|
}
|
|
|
|
return usernamesById;
|
|
|
|
}
|
|
|
|
|
|
|
|
tokenChange() {
|
|
|
|
const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
|
|
|
|
|
|
|
|
if (dropdown) {
|
|
|
|
const currentDropdownRef = dropdown.reference;
|
|
|
|
|
|
|
|
this.setDropdownWrapper();
|
|
|
|
currentDropdownRef.dispatchInputEvent();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
onrecentSearchesItemSelected(text) {
|
|
|
|
this.clearSearch();
|
|
|
|
this.filteredSearchInput.value = text;
|
|
|
|
this.filteredSearchInput.dispatchEvent(new CustomEvent('input'));
|
|
|
|
this.search();
|
|
|
|
}
|
2017-09-10 17:25:29 +05:30
|
|
|
|
|
|
|
// eslint-disable-next-line class-methods-use-this
|
|
|
|
canEdit() {
|
|
|
|
return true;
|
|
|
|
}
|
2017-08-17 22:00:37 +05:30
|
|
|
}
|