summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_hint.js.es658
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_user.js.es69
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_utils.js.es630
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es62
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es634
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js.es614
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6178
7 files changed, 90 insertions, 235 deletions
diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6
index 7bf30143d78..c5ab9c52d76 100644
--- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6
+++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6
@@ -3,31 +3,13 @@
/* global droplabFilter */
(() => {
- const dropdownData = [{
- icon: 'fa-pencil',
- hint: 'author:',
- tag: '<author>',
- }, {
- icon: 'fa-user',
- hint: 'assignee:',
- tag: '<assignee>',
- }, {
- icon: 'fa-clock-o',
- hint: 'milestone:',
- tag: '<milestone>',
- }, {
- icon: 'fa-tag',
- hint: 'label:',
- tag: '<label>',
- }];
-
class DropdownHint extends gl.FilteredSearchDropdown {
constructor(droplab, dropdown, input) {
super(droplab, dropdown, input);
this.config = {
droplabFilter: {
template: 'hint',
- filterFunction: gl.DropdownUtils.filterMethod,
+ filterFunction: gl.DropdownUtils.filterHint,
},
};
}
@@ -43,8 +25,7 @@
const tag = selected.querySelector('.js-filter-tag').innerText.trim();
if (tag.length) {
- gl.FilteredSearchDropdownManager
- .addWordToInput(this.getSelectedTextWithoutEscaping(token));
+ gl.FilteredSearchDropdownManager.addWordToInput(token);
}
this.dismissDropdown();
this.dispatchInputEvent();
@@ -52,24 +33,27 @@
}
}
- getSelectedTextWithoutEscaping(selectedToken) {
- const lastWord = this.input.value.split(' ').last();
- const lastWordIndex = selectedToken.indexOf(lastWord);
-
- return lastWordIndex === -1 ? selectedToken : selectedToken.slice(lastWord.length);
- }
-
renderContent() {
- this.droplab.changeHookList(this.hookId, this.dropdown, [droplabFilter], this.config);
-
- // Clone dropdownData to prevent it from being
- // changed due to pass by reference
- const data = [];
- dropdownData.forEach((item) => {
- data.push(Object.assign({}, item));
- });
+ const dropdownData = [{
+ icon: 'fa-pencil',
+ hint: 'author:',
+ tag: '<author>',
+ }, {
+ icon: 'fa-user',
+ hint: 'assignee:',
+ tag: '<assignee>',
+ }, {
+ icon: 'fa-clock-o',
+ hint: 'milestone:',
+ tag: '<milestone>',
+ }, {
+ icon: 'fa-tag',
+ hint: 'label:',
+ tag: '<label>',
+ }];
- this.droplab.setData(this.hookId, data);
+ this.droplab.changeHookList(this.hookId, this.dropdown, [droplabFilter], this.config);
+ this.droplab.setData(this.hookId, dropdownData);
}
init() {
diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6
index 7a566907312..49581e3bfbd 100644
--- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6
+++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6
@@ -37,13 +37,10 @@
}
getSearchInput() {
- const query = this.input.value;
- const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query);
- const valueWithoutColon = value.slice(1);
- const hasPrefix = valueWithoutColon[0] === '@';
- const valueWithoutPrefix = valueWithoutColon.slice(1);
+ const query = this.input.value.trim();
+ const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
- return hasPrefix ? valueWithoutPrefix : valueWithoutColon;
+ return lastToken.value || '';
}
init() {
diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6
index 3837b020fd3..d246000ff52 100644
--- a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6
+++ b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6
@@ -22,30 +22,32 @@
static filterWithSymbol(filterSymbol, item, query) {
const updatedItem = item;
- const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query);
- const valueWithoutColon = value.slice(1).toLowerCase();
- const prefix = valueWithoutColon[0];
- const valueWithoutPrefix = valueWithoutColon.slice(1);
+ const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(query);
- const title = updatedItem.title.toLowerCase();
+ if (lastToken !== searchToken) {
+ const value = lastToken.value.toLowerCase();
+ const title = updatedItem.title.toLowerCase();
- // Eg. filterSymbol = ~ for labels
- const matchWithoutPrefix =
- prefix === filterSymbol && title.indexOf(valueWithoutPrefix) !== -1;
- const match = title.indexOf(valueWithoutColon) !== -1;
+ // Eg. filterSymbol = ~ for labels
+ const matchWithoutSymbol = lastToken.symbol === filterSymbol && title.indexOf(value) !== -1;
+ const match = title.indexOf(`${lastToken.symbol}${value}`) !== -1;
+
+ updatedItem.droplab_hidden = !match && !matchWithoutSymbol;
+ } else {
+ updatedItem.droplab_hidden = false;
+ }
- updatedItem.droplab_hidden = !match && !matchWithoutPrefix;
return updatedItem;
}
- static filterMethod(item, query) {
+ static filterHint(item, query) {
const updatedItem = item;
- const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query);
+ const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
- if (value === '') {
+ if (!lastToken) {
updatedItem.droplab_hidden = false;
} else {
- updatedItem.droplab_hidden = updatedItem.hint.indexOf(value) === -1;
+ updatedItem.droplab_hidden = updatedItem.hint.indexOf(lastToken.toLowerCase()) === -1;
}
return updatedItem;
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6
index 68014e27462..56147ad93c9 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6
@@ -29,7 +29,7 @@
itemClicked(e, getValueFunction) {
const { selected } = e.detail;
- if (selected.tagName === 'LI') {
+ if (selected.tagName === 'LI' && selected.innerHTML) {
const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(selected);
if (!dataValueSet) {
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6
index ac71b5e4434..b67176267fb 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6
@@ -57,17 +57,25 @@
static addWordToInput(word, addSpace = false) {
const input = document.querySelector('.filtered-search');
+ input.value = input.value.trim();
+
const value = input.value;
const hasExistingValue = value.length !== 0;
- const { lastToken } = gl.FilteredSearchTokenizer.processTokens(value);
+ const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(value);
+
+ // Find out what part of the token value the user has typed
+ // and remove it from input before appending the selected token value
+ if (lastToken !== searchToken) {
+ const lastTokenString = `${lastToken.symbol}${lastToken.value}`;
- if ({}.hasOwnProperty.call(lastToken, 'key')) {
// Spaces inside the token means that the token value will be escaped by quotes
- const hasQuotes = lastToken.value.indexOf(' ') !== -1;
+ const hasQuotes = lastTokenString.indexOf(' ') !== -1;
// Add 2 length to account for the length of the front and back quotes
- const lengthToRemove = hasQuotes ? lastToken.value.length + 2 : lastToken.value.length;
+ const lengthToRemove = hasQuotes ? lastTokenString.length + 2 : lastTokenString.length;
input.value = value.slice(0, -1 * (lengthToRemove));
+ } else if (searchToken !== '' && word.indexOf(searchToken) !== -1) {
+ input.value = value.slice(0, -1 * searchToken.length);
}
input.value += hasExistingValue && addSpace ? ` ${word}` : word;
@@ -129,27 +137,25 @@
const match = gl.FilteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase());
const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key
- && {}.hasOwnProperty.call(this.mapping, match.key);
+ && this.mapping[match.key];
const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint';
if (shouldOpenFilterDropdown || shouldOpenHintDropdown) {
- // `hint` is not listed as a tokenKey (since it is not a real `filter`)
- const key = match && {}.hasOwnProperty.call(match, 'key') ? match.key : 'hint';
+ const key = match && match.key ? match.key : 'hint';
this.load(key, firstLoad);
}
-
- gl.droplab = this.droplab;
}
setDropdown() {
- const { lastToken } = this.tokenizer.processTokens(this.filteredSearchInput.value);
+ const { lastToken, searchToken } = this.tokenizer
+ .processTokens(this.filteredSearchInput.value);
- if (typeof lastToken === 'string') {
+ if (lastToken === searchToken) {
// Token is not fully initialized yet because it has no value
// Eg. token = 'label:'
- const { tokenKey } = this.tokenizer.parseToken(lastToken);
- this.loadDropdown(tokenKey);
- } else if ({}.hasOwnProperty.call(lastToken, 'key')) {
+ const split = lastToken.split(':');
+ this.loadDropdown(split.length > 1 ? split[0] : '');
+ } else if (lastToken) {
// Token has been initialized into an object because it has a value
this.loadDropdown(lastToken.key);
} else {
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
index 565f2347072..d2ea4de18aa 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
@@ -136,21 +136,13 @@
const condition = gl.FilteredSearchTokenKeys
.searchByConditionKeyValue(token.key, token.value.toLowerCase());
const { param } = gl.FilteredSearchTokenKeys.searchByKey(token.key);
+ const keyParam = param ? `${token.key}_${param}` : token.key;
let tokenPath = '';
- let keyParam = token.key;
- if (param) {
- keyParam += `_${param}`;
- }
-
- if (token.wildcard && condition) {
+ if (condition) {
tokenPath = condition.url;
- } else if (token.wildcard) {
- // wildcard means that the token does not have a symbol
- tokenPath = `${keyParam}=${encodeURIComponent(token.value)}`;
} else {
- // Remove the token symbol
- tokenPath = `${keyParam}=${encodeURIComponent(token.value.slice(1))}`;
+ tokenPath = `${keyParam}=${encodeURIComponent(token.value)}`;
}
paths.push(tokenPath);
diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6
index 57c0e8fc359..14ca78e139b 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6
+++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6
@@ -1,165 +1,39 @@
(() => {
class FilteredSearchTokenizer {
- static parseToken(input) {
- const colonIndex = input.indexOf(':');
- let tokenKey;
- let tokenValue;
- let tokenSymbol;
-
- if (colonIndex !== -1) {
- tokenKey = input.slice(0, colonIndex).toLowerCase();
- tokenValue = input.slice(colonIndex + 1);
- tokenSymbol = tokenValue[0];
- }
-
- return {
- tokenKey,
- tokenValue,
- tokenSymbol,
- };
- }
-
- static getLastTokenObject(input) {
- const token = FilteredSearchTokenizer.getLastToken(input);
- const colonIndex = token.indexOf(':');
-
- const key = colonIndex !== -1 ? token.slice(0, colonIndex) : '';
- const value = colonIndex !== -1 ? token.slice(colonIndex) : token;
-
- return {
- key,
- value,
- };
- }
-
- static getLastToken(input) {
- let completeToken = false;
- let completeQuotation = true;
- let lastQuotation = '';
- let i = input.length;
-
- const doubleQuote = '"';
- const singleQuote = '\'';
- while (!completeToken && i >= 0) {
- const isDoubleQuote = input[i] === doubleQuote;
- const isSingleQuote = input[i] === singleQuote;
-
- // If the second quotation is found
- if ((lastQuotation === doubleQuote && isDoubleQuote) ||
- (lastQuotation === singleQuote && isSingleQuote)) {
- completeQuotation = true;
- }
-
- // Save the first quotation
- if ((isDoubleQuote && lastQuotation === '') ||
- (isSingleQuote && lastQuotation === '')) {
- lastQuotation = input[i];
- completeQuotation = false;
- }
-
- if (completeQuotation && input[i] === ' ') {
- completeToken = true;
- } else {
- i -= 1;
- }
- }
-
- // Adjust by 1 because of empty space
- return input.slice(i + 1);
- }
-
static processTokens(input) {
+ const tokenRegex = /(\w+):([~%@]?)(?:"(.*?)"|'(.*?)'|(\S+))/g;
const tokens = [];
- let searchToken = '';
- let lastToken = '';
-
- const inputs = input.split(' ');
- let searchTerms = '';
- let lastQuotation = '';
- let incompleteToken = false;
-
- // Iterate through each word (broken up by spaces)
- inputs.forEach((i) => {
- if (incompleteToken) {
- // Continue previous token as it had an escaped
- // quote in the beginning
- const prevToken = tokens.last();
- prevToken.value += ` ${i}`;
-
- // Remove last quotation from the value
- const lastQuotationRegex = new RegExp(lastQuotation, 'g');
- prevToken.value = prevToken.value.replace(lastQuotationRegex, '');
- tokens[tokens.length - 1] = prevToken;
-
- // Check to see if this quotation completes the token value
- if (i.indexOf(lastQuotation) !== -1) {
- lastToken = tokens.last();
- incompleteToken = !incompleteToken;
- }
-
- return;
- }
-
- const colonIndex = i.indexOf(':');
-
- if (colonIndex !== -1) {
- const { tokenKey, tokenValue, tokenSymbol } = gl.FilteredSearchTokenizer.parseToken(i);
-
- const keyMatch = gl.FilteredSearchTokenKeys.searchByKey(tokenKey);
- const symbolMatch = gl.FilteredSearchTokenKeys.searchBySymbol(tokenSymbol);
-
- const doubleQuoteOccurrences = tokenValue.split('"').length - 1;
- const singleQuoteOccurrences = tokenValue.split('\'').length - 1;
-
- const doubleQuoteIndex = tokenValue.indexOf('"');
- const singleQuoteIndex = tokenValue.indexOf('\'');
-
- const doubleQuoteExist = doubleQuoteIndex !== -1;
- const singleQuoteExist = singleQuoteIndex !== -1;
-
- const doubleQuoteExistOnly = doubleQuoteExist && !singleQuoteExist;
- const doubleQuoteIsBeforeSingleQuote =
- doubleQuoteExist && singleQuoteExist && doubleQuoteIndex < singleQuoteIndex;
-
- const singleQuoteExistOnly = singleQuoteExist && !doubleQuoteExist;
- const singleQuoteIsBeforeDoubleQuote =
- doubleQuoteExist && singleQuoteExist && singleQuoteIndex < doubleQuoteIndex;
-
- if ((doubleQuoteExistOnly || doubleQuoteIsBeforeSingleQuote)
- && doubleQuoteOccurrences % 2 !== 0) {
- // " is found and is in front of ' (if any)
- lastQuotation = '"';
- incompleteToken = true;
- } else if ((singleQuoteExistOnly || singleQuoteIsBeforeDoubleQuote)
- && singleQuoteOccurrences % 2 !== 0) {
- // ' is found and is in front of " (if any)
- lastQuotation = '\'';
- incompleteToken = true;
- }
-
- if (keyMatch && tokenValue.length > 0) {
- tokens.push({
- key: keyMatch.key,
- value: tokenValue,
- wildcard: !symbolMatch,
- });
- lastToken = tokens.last();
-
- return;
- }
+ let lastToken = null;
+ const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => {
+ let tokenValue = v1 || v2 || v3;
+ let tokenSymbol = symbol;
+
+ if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') {
+ tokenSymbol = tokenValue;
+ tokenValue = '';
}
- // Add space for next term
- searchTerms += `${i} `;
- lastToken = i;
- }, this);
-
- searchToken = searchTerms.trim();
+ tokens.push({
+ key,
+ value: tokenValue || '',
+ symbol: tokenSymbol || '',
+ });
+ return '';
+ }).replace(/\s{2,}/g, ' ').trim() || '';
+
+ if (tokens.length > 0) {
+ const last = tokens[tokens.length - 1];
+ const lastString = `${last.key}:${last.symbol}${last.value}`;
+ lastToken = input.lastIndexOf(lastString) ===
+ input.length - lastString.length ? last : searchToken;
+ } else {
+ lastToken = searchToken;
+ }
return {
tokens,
- searchToken,
lastToken,
+ searchToken,
};
}
}