summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/filtered_search/filtered_search_manager.js
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/filtered_search/filtered_search_manager.js')
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js161
1 files changed, 117 insertions, 44 deletions
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 36af0674ac6..a31be2b0bc7 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -1,13 +1,13 @@
-/* global Flash */
-
import FilteredSearchContainer from './container';
import RecentSearchesRoot from './recent_searches_root';
import RecentSearchesStore from './stores/recent_searches_store';
import RecentSearchesService from './services/recent_searches_service';
import eventHub from './event_hub';
+import { addClassIfElementExists } from '../lib/utils/dom_utils';
class FilteredSearchManager {
constructor(page) {
+ this.page = page;
this.container = FilteredSearchContainer.container;
this.filteredSearchInput = this.container.querySelector('.filtered-search');
this.filteredSearchInputForm = this.filteredSearchInput.form;
@@ -15,22 +15,36 @@ class FilteredSearchManager {
this.tokensContainer = this.container.querySelector('.tokens-container');
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
- this.recentSearchesStore = new RecentSearchesStore();
- let recentSearchesKey = 'issue-recent-searches';
- if (page === 'merge_requests') {
- recentSearchesKey = 'merge-request-recent-searches';
+ this.recentSearchesStore = new RecentSearchesStore({
+ isLocalStorageAvailable: RecentSearchesService.isAvailable(),
+ allowedKeys: this.filteredSearchTokenKeys.getKeys(),
+ });
+ this.searchHistoryDropdownElement = document.querySelector('.js-filtered-search-history-dropdown');
+ const fullPath = this.searchHistoryDropdownElement ?
+ this.searchHistoryDropdownElement.dataset.fullPath : 'project';
+ let recentSearchesPagePrefix = 'issue-recent-searches';
+ if (this.page === 'merge_requests') {
+ recentSearchesPagePrefix = 'merge-request-recent-searches';
}
+ const recentSearchesKey = `${fullPath}-${recentSearchesPagePrefix}`;
this.recentSearchesService = new RecentSearchesService(recentSearchesKey);
+ }
+ setup() {
// Fetch recent searches from localStorage
this.fetchingRecentSearchesPromise = this.recentSearchesService.fetch()
- .catch(() => {
+ .catch((error) => {
+ if (error.name === 'RecentSearchesServiceError') return undefined;
// eslint-disable-next-line no-new
- new Flash('An error occured while parsing recent searches');
+ new window.Flash('An error occured while parsing recent searches');
// Gracefully fail to empty array
return [];
})
.then((searches) => {
+ if (!searches) {
+ return;
+ }
+
// 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(
@@ -41,12 +55,12 @@ class FilteredSearchManager {
if (this.filteredSearchInput) {
this.tokenizer = gl.FilteredSearchTokenizer;
- this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '', page);
+ this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '', this.tokenizer, this.page);
this.recentSearchesRoot = new RecentSearchesRoot(
this.recentSearchesStore,
this.recentSearchesService,
- document.querySelector('.js-filtered-search-history-dropdown'),
+ this.searchHistoryDropdownElement,
);
this.recentSearchesRoot.init();
@@ -68,6 +82,41 @@ class FilteredSearchManager {
}
}
+ bindStateEvents() {
+ this.stateFilters = document.querySelector('.container-fluid .issues-state-filters');
+
+ if (this.stateFilters) {
+ this.searchStateWrapper = this.searchState.bind(this);
+
+ this.stateFilters.querySelector('[data-state="opened"]')
+ .addEventListener('click', this.searchStateWrapper);
+ this.stateFilters.querySelector('[data-state="closed"]')
+ .addEventListener('click', this.searchStateWrapper);
+ this.stateFilters.querySelector('[data-state="all"]')
+ .addEventListener('click', this.searchStateWrapper);
+
+ this.mergedState = this.stateFilters.querySelector('[data-state="merged"]');
+ if (this.mergedState) {
+ this.mergedState.addEventListener('click', this.searchStateWrapper);
+ }
+ }
+ }
+
+ unbindStateEvents() {
+ if (this.stateFilters) {
+ this.stateFilters.querySelector('[data-state="opened"]')
+ .removeEventListener('click', this.searchStateWrapper);
+ this.stateFilters.querySelector('[data-state="closed"]')
+ .removeEventListener('click', this.searchStateWrapper);
+ this.stateFilters.querySelector('[data-state="all"]')
+ .removeEventListener('click', this.searchStateWrapper);
+
+ if (this.mergedState) {
+ this.mergedState.removeEventListener('click', this.searchStateWrapper);
+ }
+ }
+ }
+
bindEvents() {
this.handleFormSubmit = this.handleFormSubmit.bind(this);
this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager);
@@ -96,15 +145,15 @@ class FilteredSearchManager {
this.filteredSearchInput.addEventListener('click', this.tokenChange);
this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper);
- this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken);
this.tokensContainer.addEventListener('click', this.removeTokenWrapper);
- this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper);
+ this.tokensContainer.addEventListener('click', this.editTokenWrapper);
this.clearSearchButton.addEventListener('click', this.onClearSearchWrapper);
- document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
document.addEventListener('click', this.unselectEditTokensWrapper);
document.addEventListener('click', this.removeInputContainerFocusWrapper);
document.addEventListener('keydown', this.removeSelectedTokenKeydownWrapper);
eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
+
+ this.bindStateEvents();
}
unbindEvents() {
@@ -118,15 +167,15 @@ class FilteredSearchManager {
this.filteredSearchInput.removeEventListener('click', this.tokenChange);
this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper);
- this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken);
this.tokensContainer.removeEventListener('click', this.removeTokenWrapper);
- this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper);
+ this.tokensContainer.removeEventListener('click', this.editTokenWrapper);
this.clearSearchButton.removeEventListener('click', this.onClearSearchWrapper);
- document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
document.removeEventListener('click', this.unselectEditTokensWrapper);
document.removeEventListener('click', this.removeInputContainerFocusWrapper);
document.removeEventListener('keydown', this.removeSelectedTokenKeydownWrapper);
eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
+
+ this.unbindStateEvents();
}
checkForBackspace(e) {
@@ -135,7 +184,9 @@ class FilteredSearchManager {
if (e.keyCode === 8 || e.keyCode === 46) {
const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
- if (this.filteredSearchInput.value === '' && lastVisualToken) {
+ const sanitizedTokenName = lastVisualToken && lastVisualToken.querySelector('.name').textContent.trim();
+ const canEdit = sanitizedTokenName && this.canEdit && this.canEdit(sanitizedTokenName);
+ if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) {
this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
gl.FilteredSearchVisualTokens.removeLastTokenPartial();
}
@@ -177,11 +228,7 @@ class FilteredSearchManager {
}
addInputContainerFocus() {
- const inputContainer = this.filteredSearchInput.closest('.filtered-search-box');
-
- if (inputContainer) {
- inputContainer.classList.add('focus');
- }
+ addClassIfElementExists(this.filteredSearchInput.closest('.filtered-search-box'), 'focus');
}
removeInputContainerFocus(e) {
@@ -196,23 +243,13 @@ class FilteredSearchManager {
}
}
- static selectToken(e) {
- const button = e.target.closest('.selectable');
- const removeButtonSelected = e.target.closest('.remove-token');
-
- if (!removeButtonSelected && button) {
- e.preventDefault();
- e.stopPropagation();
- gl.FilteredSearchVisualTokens.selectToken(button);
- }
- }
-
removeToken(e) {
const removeButtonSelected = e.target.closest('.remove-token');
if (removeButtonSelected) {
e.preventDefault();
- e.stopPropagation();
+ // Prevent editToken from being triggered after token is removed
+ e.stopImmediatePropagation();
const button = e.target.closest('.selectable');
gl.FilteredSearchVisualTokens.selectToken(button, true);
@@ -234,8 +271,12 @@ class FilteredSearchManager {
editToken(e) {
const token = e.target.closest('.js-visual-token');
+ const sanitizedTokenName = token && token.querySelector('.name').textContent.trim();
+ const canEdit = this.canEdit && this.canEdit(sanitizedTokenName);
- if (token) {
+ if (token && canEdit) {
+ e.preventDefault();
+ e.stopPropagation();
gl.FilteredSearchVisualTokens.editToken(token);
this.tokenChange();
}
@@ -313,7 +354,7 @@ class FilteredSearchManager {
handleInputVisualToken() {
const input = this.filteredSearchInput;
const { tokens, searchToken }
- = gl.FilteredSearchTokenizer.processTokens(input.value);
+ = this.tokenizer.processTokens(input.value, this.filteredSearchTokenKeys.getKeys());
const { isLastVisualTokenValid }
= gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
@@ -326,7 +367,7 @@ class FilteredSearchManager {
const fragments = searchToken.split(':');
if (fragments.length > 1) {
const inputValues = fragments[0].split(' ');
- const tokenKey = inputValues.last();
+ const tokenKey = _.last(inputValues);
if (inputValues.length > 1) {
inputValues.pop();
@@ -385,7 +426,12 @@ class FilteredSearchManager {
if (condition) {
hasFilteredSearch = true;
- gl.FilteredSearchVisualTokens.addFilterVisualToken(condition.tokenKey, condition.value);
+ const canEdit = this.canEdit && this.canEdit(condition.tokenKey);
+ gl.FilteredSearchVisualTokens.addFilterVisualToken(
+ condition.tokenKey,
+ condition.value,
+ canEdit,
+ );
} else {
// Sanitize value since URL converts spaces into +
// Replace before decode so that we know what was originally + versus the encoded +
@@ -404,18 +450,27 @@ class FilteredSearchManager {
}
hasFilteredSearch = true;
- gl.FilteredSearchVisualTokens.addFilterVisualToken(sanitizedKey, `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`);
+ const canEdit = this.canEdit && this.canEdit(sanitizedKey);
+ gl.FilteredSearchVisualTokens.addFilterVisualToken(
+ sanitizedKey,
+ `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`,
+ canEdit,
+ );
} else if (!match && keyParam === 'assignee_id') {
const id = parseInt(value, 10);
if (usernameParams[id]) {
hasFilteredSearch = true;
- gl.FilteredSearchVisualTokens.addFilterVisualToken('assignee', `@${usernameParams[id]}`);
+ const tokenName = 'assignee';
+ const canEdit = this.canEdit && this.canEdit(tokenName);
+ gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, canEdit);
}
} else if (!match && keyParam === 'author_id') {
const id = parseInt(value, 10);
if (usernameParams[id]) {
hasFilteredSearch = true;
- gl.FilteredSearchVisualTokens.addFilterVisualToken('author', `@${usernameParams[id]}`);
+ const tokenName = 'author';
+ const canEdit = this.canEdit && this.canEdit(tokenName);
+ gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, canEdit);
}
} else if (!match && keyParam === 'search') {
hasFilteredSearch = true;
@@ -432,15 +487,28 @@ class FilteredSearchManager {
}
}
- search() {
+ 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) {
const paths = [];
const searchQuery = gl.DropdownUtils.getSearchQuery();
this.saveCurrentSearchQuery();
const { tokens, searchToken }
- = this.tokenizer.processTokens(searchQuery);
- const currentState = gl.utils.getParameterByName('state') || 'opened';
+ = this.tokenizer.processTokens(searchQuery, this.filteredSearchTokenKeys.getKeys());
+ const currentState = state || gl.utils.getParameterByName('state') || 'opened';
paths.push(`state=${currentState}`);
tokens.forEach((token) => {
@@ -510,6 +578,11 @@ class FilteredSearchManager {
this.filteredSearchInput.dispatchEvent(new CustomEvent('input'));
this.search();
}
+
+ // eslint-disable-next-line class-methods-use-this
+ canEdit() {
+ return true;
+ }
}
window.gl = window.gl || {};